thomascube
2010-04-01 1d773d71414316e0b9836a15c35576593427ee21
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;
64d855 49   
T 50   private $tables;
f45ec7 51
d2a9db 52
T 53   /**
54    * Object constructor
55    *
56    * @param  string  DSN for read/write operations
57    * @param  string  Optional DSN for read only operations
58    */
bad419 59   function __construct($db_dsnw, $db_dsnr='', $pconn=false)
f45ec7 60     {
d2a9db 61     if ($db_dsnr=='')
T 62       $db_dsnr=$db_dsnw;
63
64     $this->db_dsnw = $db_dsnw;
65     $this->db_dsnr = $db_dsnr;
bad419 66     $this->db_pconn = $pconn;
T 67     
d2a9db 68     $dsn_array = MDB2::parseDSN($db_dsnw);
T 69     $this->db_provider = $dsn_array['phptype'];
f45ec7 70     }
S 71
d2a9db 72
T 73   /**
74    * Connect to specific database
75    *
76    * @param  string  DSN for DB connections
77    * @return object  PEAR database handle
78    * @access private
79    */
80   function dsn_connect($dsn)
f45ec7 81     {
d2a9db 82     // Use persistent connections if available
9f23f0 83     $db_options = array(
981472 84         'persistent' => $this->db_pconn,
0ce1a6 85         'emulate_prepared' => $this->debug_mode,
981472 86         'debug' => $this->debug_mode,
T 87         'debug_handler' => 'mdb2_debug_handler',
9f23f0 88         'portability' => MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_EMPTY_TO_NULL);
A 89
90     if ($this->db_provider == 'pgsql') {
e905db 91       $db_options['disable_smart_seqname'] = true;
T 92       $db_options['seqname_format'] = '%s';
93     }
9f23f0 94
A 95     $dbh = MDB2::connect($dsn, $db_options);
d2a9db 96
7eaf7a 97     if (MDB2::isError($dbh))
d2a9db 98       {
T 99       $this->db_error = TRUE;
100       $this->db_error_msg = $dbh->getMessage();
101       
7eaf7a 102       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__,
T 103         'file' => __FILE__, 'message' => $dbh->getUserInfo()), TRUE, FALSE);
d2a9db 104       }
T 105     else if ($this->db_provider=='sqlite')
106       {
107       $dsn_array = MDB2::parseDSN($dsn);
108       if (!filesize($dsn_array['database']) && !empty($this->sqlite_initials))
109         $this->_sqlite_create_database($dbh, $this->sqlite_initials);
110       }
2273d4 111     else if ($this->db_provider!='mssql' && $this->db_provider!='sqlsrv')
b66d40 112       $dbh->setCharset('utf8');
d2a9db 113
T 114     return $dbh;
115     }
116
117
118   /**
119    * Connect to appropiate databse
120    * depending on the operation
121    *
122    * @param  string  Connection mode (r|w)
123    * @access public
124    */
125   function db_connect($mode)
126     {
127     $this->db_mode = $mode;
128
129     // Already connected
130     if ($this->db_connected)
131       {
132       // no replication, current connection is ok
133       if ($this->db_dsnw==$this->db_dsnr)
134         return;
135
136       // connected to master, current connection is ok
137       if ($this->db_mode=='w')
138         return;
139
140       // Same mode, current connection is ok
141       if ($this->db_mode==$mode)
142         return;
143       }
144
145     if ($mode=='r')
146       $dsn = $this->db_dsnr;
147     else
148       $dsn = $this->db_dsnw;
149
150     $this->db_handle = $this->dsn_connect($dsn);
151     $this->db_connected = true;
152     }
153
981472 154
T 155   /**
156    * Activate/deactivate debug mode
157    *
158    * @param boolean True if SQL queries should be logged
159    */
160   function set_debug($dbg = true)
161   {
162     $this->debug_mode = $dbg;
163     if ($this->db_connected)
0ce1a6 164     {
981472 165       $this->db_handle->setOption('debug', $dbg);
0ce1a6 166       $this->db_handle->setOption('emulate_prepared', $dbg);
T 167     }
981472 168   }
d2a9db 169
T 170     
171   /**
172    * Getter for error state
173    *
174    * @param  boolean  True on error
175    */
176   function is_error()
177     {
178     return $this->db_error ? $this->db_error_msg : FALSE;
179     }
180     
181
182   /**
26d857 183    * Connection state checker
A 184    *
185    * @param  boolean  True if in connected state
186    */
187   function is_connected()
188     {
189     return PEAR::isError($this->db_handle) ? false : true;
190     }
191
192
193   /**
d2a9db 194    * Execute a SQL query
T 195    *
196    * @param  string  SQL query to execute
197    * @param  mixed   Values to be inserted in query
198    * @return number  Query handle identifier
199    * @access public
200    */
201   function query()
202     {
26d857 203     if (!$this->is_connected())
A 204       return NULL;
205     
d2a9db 206     $params = func_get_args();
T 207     $query = array_shift($params);
208
209     return $this->_query($query, 0, 0, $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  mixed   Values to be inserted in query
220    * @return number  Query handle identifier
221    * @access public
222    */
223   function limitquery()
224     {
225     $params = func_get_args();
226     $query = array_shift($params);
227     $offset = array_shift($params);
228     $numrows = array_shift($params);
229
230     return $this->_query($query, $offset, $numrows, $params);
231     }
232
233
234   /**
235    * Execute a SQL query with limits
236    *
237    * @param  string  SQL query to execute
238    * @param  number  Offset for LIMIT statement
239    * @param  number  Number of rows for LIMIT statement
240    * @param  array   Values to be inserted in query
241    * @return number  Query handle identifier
242    * @access private
243    */
244   function _query($query, $offset, $numrows, $params)
245     {
246     // Read or write ?
bac356 247     if (strtolower(substr(trim($query),0,6))=='select')
d2a9db 248       $mode='r';
T 249     else
250       $mode='w';
251
252     $this->db_connect($mode);
253
254     if ($this->db_provider == 'sqlite')
255       $this->_sqlite_prepare();
256
257     if ($numrows || $offset)
258       $result = $this->db_handle->setLimit($numrows,$offset);
259
260     if (empty($params))
8678ce 261       $result = $mode=='r' ? $this->db_handle->query($query) : $this->db_handle->exec($query);
d2a9db 262     else
T 263       {
264       $params = (array)$params;
8678ce 265       $q = $this->db_handle->prepare($query, null, $mode=='w' ? MDB2_PREPARE_MANIP : null);
d2a9db 266       if ($this->db_handle->isError($q))
f45ec7 267         {
d2a9db 268         $this->db_error = TRUE;
T 269         $this->db_error_msg = $q->userinfo;
270
10eedb 271         raise_error(array('code' => 500, 'type' => 'db',
c0297f 272           'line' => __LINE__, 'file' => __FILE__,
10eedb 273           'message' => $this->db_error_msg), TRUE, TRUE);
f45ec7 274         }
d2a9db 275       else
f45ec7 276         {
d2a9db 277         $result = $q->execute($params);
T 278         $q->free();
f45ec7 279         }
d2a9db 280       }
T 281
282     // add result, even if it's an error
283     return $this->_add_result($result);
f45ec7 284     }
S 285
ccfda8 286
d2a9db 287   /**
T 288    * Get number of rows for a SQL query
289    * If no query handle is specified, the last query will be taken as reference
290    *
291    * @param  number  Optional query handle identifier
292    * @return mixed   Number of rows or FALSE on failure
293    * @access public
294    */
295   function num_rows($res_id=NULL)
10a699 296     {
d2a9db 297     if (!$this->db_handle)
T 298       return FALSE;
ccfda8 299
d2a9db 300     if ($result = $this->_get_result($res_id))
T 301       return $result->numRows();
302     else
303       return FALSE;
10a699 304     }
T 305
306
d2a9db 307   /**
9c5bee 308    * Get number of affected rows for the last query
d2a9db 309    *
9c5bee 310    * @param  number  Optional query handle identifier
d2a9db 311    * @return mixed   Number of rows or FALSE on failure
T 312    * @access public
313    */
9c5bee 314   function affected_rows($res_id = null)
d2a9db 315     {
T 316     if (!$this->db_handle)
317       return FALSE;
ccfda8 318
9c5bee 319     return (int) $this->_get_result($res_id);
d2a9db 320     }
ccfda8 321
d2a9db 322
T 323   /**
324    * Get last inserted record ID
325    * For Postgres databases, a sequence name is required
326    *
6b7e8e 327    * @param  string  Table name (to find the incremented sequence)
d2a9db 328    * @return mixed   ID or FALSE on failure
T 329    * @access public
330    */
6b7e8e 331   function insert_id($table = '')
d2a9db 332     {
T 333     if (!$this->db_handle || $this->db_mode=='r')
334       return FALSE;
335
e1ac21 336     if ($table) {
A 337       if ($this->db_provider == 'pgsql')
338         // find sequence name
339         $table = get_sequence_name($table);
340       else
341         // resolve table name
342         $table = get_table_name($table);
343     }
344     
6b7e8e 345     $id = $this->db_handle->lastInsertID($table);
6955c7 346     
A 347     return $this->db_handle->isError($id) ? null : $id;
d2a9db 348     }
T 349
350
351   /**
352    * Get an associative array for one row
353    * If no query handle is specified, the last query will be taken as reference
354    *
355    * @param  number  Optional query handle identifier
356    * @return mixed   Array with col values or FALSE on failure
357    * @access public
358    */
359   function fetch_assoc($res_id=NULL)
360     {
361     $result = $this->_get_result($res_id);
362     return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
363     }
364
365
366   /**
367    * Get an index array for one row
368    * If no query handle is specified, the last query will be taken as reference
369    *
370    * @param  number  Optional query handle identifier
371    * @return mixed   Array with col values or FALSE on failure
372    * @access public
373    */
374   function fetch_array($res_id=NULL)
375     {
376     $result = $this->_get_result($res_id);
377     return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
378     }
379
380
381   /**
9c5bee 382    * Get col values for a result row
d2a9db 383    *
T 384    * @param  object  Query result handle
385    * @param  number  Fetch mode identifier
386    * @return mixed   Array with col values or FALSE on failure
387    * @access private
388    */
389   function _fetch_row($result, $mode)
390     {
26d857 391     if ($result === FALSE || PEAR::isError($result) || !$this->is_connected())
d2a9db 392       return FALSE;
T 393
394     return $result->fetchRow($mode);
395     }
396
397
398   /**
c0297f 399    * Wrapper for the SHOW TABLES command
T 400    *
401    * @return array List of all tables of the current database
402    */
403   function list_tables()
404   {
405     // get tables if not cached
64d855 406     if (!$this->tables) {
cbde30 407       $this->db_handle->loadModule('Manager');
A 408       if (!PEAR::isError($result = $this->db_handle->listTables()))
409         $this->tables = $result;
410       else
411         $this->tables = array();
c0297f 412     }
T 413
64d855 414     return $this->tables;
c0297f 415   }
T 416
417
418   /**
d2a9db 419    * Formats input so it can be safely used in a query
T 420    *
421    * @param  mixed   Value to quote
422    * @return string  Quoted/converted string for use in query
423    * @access public
424    */
425   function quote($input, $type = null)
426     {
427     // create DB handle if not available
428     if (!$this->db_handle)
429       $this->db_connect('r');
430
431     // escape pear identifier chars
432     $rep_chars = array('?' => '\?',
433                        '!' => '\!',
434                        '&' => '\&');
435
436     return $this->db_handle->quote($input, $type);
437     }
438
439
440   /**
441    * Quotes a string so it can be safely used as a table or column name
442    *
443    * @param  string  Value to quote
444    * @return string  Quoted string for use in query
445    * @deprecated     Replaced by rcube_MDB2::quote_identifier
9c5bee 446    * @see            rcube_mdb2::quote_identifier
d2a9db 447    * @access public
T 448    */
449   function quoteIdentifier($str)
9c5bee 450     {
d2a9db 451     return $this->quote_identifier($str);
9c5bee 452     }
ccfda8 453
10a699 454
d2a9db 455   /**
T 456    * Quotes a string so it can be safely used as a table or column name
457    *
458    * @param  string  Value to quote
459    * @return string  Quoted string for use in query
460    * @access public
461    */
462   function quote_identifier($str)
f45ec7 463     {
d2a9db 464     if (!$this->db_handle)
T 465       $this->db_connect('r');
466
467     return $this->db_handle->quoteIdentifier($str);
f45ec7 468     }
S 469
8f4dcb 470   /**
T 471    * Escapes a string
472    *
473    * @param  string  The string to be escaped
474    * @return string  The escaped string
475    * @access public
476    * @since  0.1.1
477    */
478   function escapeSimple($str)
479     {
480     if (!$this->db_handle)
481       $this->db_connect('r');
482    
483     return $this->db_handle->escape($str);
484     }
485
f45ec7 486
d2a9db 487   /**
7139e3 488    * Return SQL function for current time and date
T 489    *
490    * @return string SQL function to use in query
491    * @access public
492    */
493   function now()
494     {
495     switch($this->db_provider)
496       {
497       case 'mssql':
2273d4 498       case 'sqlsrv':
7139e3 499         return "getdate()";
T 500
501       default:
502         return "now()";
503       }
504     }
505
506
507   /**
ad84f9 508    * Return list of elements for use with SQL's IN clause
A 509    *
510    * @param  string Input array
511    * @return string Elements list string
512    * @access public
513    */
514   function array2list($arr, $type=null)
515     {
516     if (!is_array($arr))
517       return $this->quote($arr, $type);
518     
519     $res = array();
520     foreach ($arr as $item)
521       $res[] = $this->quote($item, $type);
522
523     return implode(',', $res);
524     }
525
526
527   /**
d2a9db 528    * Return SQL statement to convert a field value into a unix timestamp
T 529    *
530    * @param  string  Field name
531    * @return string  SQL statement to use in query
532    * @access public
533    */
534   function unixtimestamp($field)
f45ec7 535     {
d2a9db 536     switch($this->db_provider)
T 537       {
538       case 'pgsql':
539         return "EXTRACT (EPOCH FROM $field)";
540         break;
541
7139e3 542       case 'mssql':
2273d4 543       case 'sqlsrv':
446533 544     return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
7139e3 545
d2a9db 546       default:
T 547         return "UNIX_TIMESTAMP($field)";
548       }
f45ec7 549     }
S 550
551
d2a9db 552   /**
T 553    * Return SQL statement to convert from a unix timestamp
554    *
555    * @param  string  Field name
556    * @return string  SQL statement to use in query
557    * @access public
558    */
559   function fromunixtime($timestamp)
f45ec7 560     {
d2a9db 561     switch($this->db_provider)
T 562       {
563       case 'mysqli':
564       case 'mysql':
565       case 'sqlite':
3978ab 566         return sprintf("FROM_UNIXTIME(%d)", $timestamp);
f45ec7 567
d2a9db 568       default:
T 569         return date("'Y-m-d H:i:s'", $timestamp);
570       }
f45ec7 571     }
S 572
d2a9db 573
T 574   /**
d8d416 575    * Return SQL statement for case insensitive LIKE
A 576    *
577    * @param  string  Field name
578    * @param  string  Search value
579    * @return string  SQL statement to use in query
580    * @access public
581    */
582   function ilike($column, $value)
583     {
584     // TODO: use MDB2's matchPattern() function
585     switch($this->db_provider)
586       {
587       case 'pgsql':
588         return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
589       default:
590         return $this->quote_identifier($column).' LIKE '.$this->quote($value);
591       }
592     }
593
594
595   /**
ac6229 596    * Encodes non-UTF-8 characters in string/array/object (recursive)
A 597    *
598    * @param  mixed  Data to fix
599    * @return mixed  Properly UTF-8 encoded data
600    * @access public
601    */
602   function encode($input)
603     {
604     if (is_object($input)) {
605       foreach (get_object_vars($input) as $idx => $value)
606         $input->$idx = $this->encode($value);
607       return $input;
608       }
609     else if (is_array($input)) {
610       foreach ($input as $idx => $value)
611         $input[$idx] = $this->encode($value);
612       return $input;    
613       }
614
615     return utf8_encode($input);
616     }
617
618
619   /**
620    * Decodes encoded UTF-8 string/object/array (recursive)
621    *
622    * @param  mixed  Input data
623    * @return mixed  Decoded data
624    * @access public
625    */
626   function decode($input)
627     {
628     if (is_object($input)) {
629       foreach (get_object_vars($input) as $idx => $value)
630         $input->$idx = $this->decode($value);
631       return $input;
632       }
633     else if (is_array($input)) {
634       foreach ($input as $idx => $value)
635         $input[$idx] = $this->decode($value);
636       return $input;    
637       }
638
639     return utf8_decode($input);
640     }
641
642
643   /**
d2a9db 644    * Adds a query result and returns a handle ID
T 645    *
646    * @param  object  Query handle
9c5bee 647    * @return mixed   Handle ID
d2a9db 648    * @access private
T 649    */
650   function _add_result($res)
f45ec7 651     {
d2a9db 652     // sql error occured
T 653     if (PEAR::isError($res))
654       {
9c5bee 655       $this->db_error = TRUE;
A 656       $this->db_error_msg = $res->getMessage();
10eedb 657       raise_error(array('code' => 500, 'type' => 'db',
A 658             'line' => __LINE__, 'file' => __FILE__,
9c5bee 659             'message' => $res->getMessage() . " Query: " 
A 660         . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
661         TRUE, FALSE);
d2a9db 662       }
9c5bee 663     
A 664     $res_id = sizeof($this->a_query_results);
665     $this->last_res_id = $res_id;
666     $this->a_query_results[$res_id] = $res;
667     return $res_id;
f45ec7 668     }
d2a9db 669
T 670
671   /**
672    * Resolves a given handle ID and returns the according query handle
9c5bee 673    * If no ID is specified, the last resource handle will be returned
d2a9db 674    *
T 675    * @param  number  Handle ID
9c5bee 676    * @return mixed   Resource handle or FALSE on failure
d2a9db 677    * @access private
T 678    */
679   function _get_result($res_id=NULL)
680     {
681     if ($res_id==NULL)
682       $res_id = $this->last_res_id;
683
9c5bee 684     if (isset($this->a_query_results[$res_id]))
A 685       if (!PEAR::isError($this->a_query_results[$res_id]))
686         return $this->a_query_results[$res_id];
687     
688     return FALSE;
d2a9db 689     }
T 690
691
692   /**
693    * Create a sqlite database from a file
694    *
695    * @param  object  SQLite database handle
696    * @param  string  File path to use for DB creation
697    * @access private
698    */
699   function _sqlite_create_database($dbh, $file_name)
700     {
701     if (empty($file_name) || !is_string($file_name))
702       return;
703
ff73e0 704     $data = file_get_contents($file_name);
d2a9db 705
T 706     if (strlen($data))
50d515 707       if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh)) 
A 708         raise_error(array('code' => 500, 'type' => 'db',
709         'line' => __LINE__, 'file' => __FILE__, 'message' => $error), TRUE, FALSE); 
d2a9db 710     }
T 711
712
713   /**
714    * Add some proprietary database functions to the current SQLite handle
715    * in order to make it MySQL compatible
716    *
717    * @access private
718    */
719   function _sqlite_prepare()
720     {
721     include_once('include/rcube_sqlite.inc');
722
723     // we emulate via callback some missing MySQL function
724     sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
725     sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
726     sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
727     sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");
728     }
729
730
731   }  // end class rcube_db
f45ec7 732
981472 733
T 734 /* this is our own debug handler for the MDB2 connection */
735 function mdb2_debug_handler(&$db, $scope, $message, $context = array())
736 {
737   if ($scope != 'prepare')
738   {
739     $debug_output = $scope . '('.$db->db_index.'): ';
740     $debug_output .= $message . $db->getOption('log_line_break');
d559cb 741     write_log('sql', $debug_output);
981472 742   }
T 743 }