alecpl
2010-01-26 2273d4117fd50ee44dcdaa28fd6444383dc403a0
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       }
2273d4 109     else if ($this->db_provider!='mssql' && $this->db_provider!='sqlsrv')
b66d40 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   /**
26d857 181    * Connection state checker
A 182    *
183    * @param  boolean  True if in connected state
184    */
185   function is_connected()
186     {
187     return PEAR::isError($this->db_handle) ? false : true;
188     }
189
190
191   /**
d2a9db 192    * Execute a SQL query
T 193    *
194    * @param  string  SQL query to execute
195    * @param  mixed   Values to be inserted in query
196    * @return number  Query handle identifier
197    * @access public
198    */
199   function query()
200     {
26d857 201     if (!$this->is_connected())
A 202       return NULL;
203     
d2a9db 204     $params = func_get_args();
T 205     $query = array_shift($params);
206
207     return $this->_query($query, 0, 0, $params);
208     }
209
210
211   /**
212    * Execute a SQL query with limits
213    *
214    * @param  string  SQL query to execute
215    * @param  number  Offset for LIMIT statement
216    * @param  number  Number of rows for LIMIT statement
217    * @param  mixed   Values to be inserted in query
218    * @return number  Query handle identifier
219    * @access public
220    */
221   function limitquery()
222     {
223     $params = func_get_args();
224     $query = array_shift($params);
225     $offset = array_shift($params);
226     $numrows = array_shift($params);
227
228     return $this->_query($query, $offset, $numrows, $params);
229     }
230
231
232   /**
233    * Execute a SQL query with limits
234    *
235    * @param  string  SQL query to execute
236    * @param  number  Offset for LIMIT statement
237    * @param  number  Number of rows for LIMIT statement
238    * @param  array   Values to be inserted in query
239    * @return number  Query handle identifier
240    * @access private
241    */
242   function _query($query, $offset, $numrows, $params)
243     {
244     // Read or write ?
bac356 245     if (strtolower(substr(trim($query),0,6))=='select')
d2a9db 246       $mode='r';
T 247     else
248       $mode='w';
249
250     $this->db_connect($mode);
251
252     if ($this->db_provider == 'sqlite')
253       $this->_sqlite_prepare();
254
255     if ($numrows || $offset)
256       $result = $this->db_handle->setLimit($numrows,$offset);
257
258     if (empty($params))
8678ce 259       $result = $mode=='r' ? $this->db_handle->query($query) : $this->db_handle->exec($query);
d2a9db 260     else
T 261       {
262       $params = (array)$params;
8678ce 263       $q = $this->db_handle->prepare($query, null, $mode=='w' ? MDB2_PREPARE_MANIP : null);
d2a9db 264       if ($this->db_handle->isError($q))
f45ec7 265         {
d2a9db 266         $this->db_error = TRUE;
T 267         $this->db_error_msg = $q->userinfo;
268
269         raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
270                           'message' => $this->db_error_msg), TRUE, TRUE);
f45ec7 271         }
d2a9db 272       else
f45ec7 273         {
d2a9db 274         $result = $q->execute($params);
T 275         $q->free();
f45ec7 276         }
d2a9db 277       }
T 278
279     // add result, even if it's an error
280     return $this->_add_result($result);
f45ec7 281     }
S 282
ccfda8 283
d2a9db 284   /**
T 285    * Get number of rows for a SQL query
286    * If no query handle is specified, the last query will be taken as reference
287    *
288    * @param  number  Optional query handle identifier
289    * @return mixed   Number of rows or FALSE on failure
290    * @access public
291    */
292   function num_rows($res_id=NULL)
10a699 293     {
d2a9db 294     if (!$this->db_handle)
T 295       return FALSE;
ccfda8 296
d2a9db 297     if ($result = $this->_get_result($res_id))
T 298       return $result->numRows();
299     else
300       return FALSE;
10a699 301     }
T 302
303
d2a9db 304   /**
9c5bee 305    * Get number of affected rows for the last query
d2a9db 306    *
9c5bee 307    * @param  number  Optional query handle identifier
d2a9db 308    * @return mixed   Number of rows or FALSE on failure
T 309    * @access public
310    */
9c5bee 311   function affected_rows($res_id = null)
d2a9db 312     {
T 313     if (!$this->db_handle)
314       return FALSE;
ccfda8 315
9c5bee 316     return (int) $this->_get_result($res_id);
d2a9db 317     }
ccfda8 318
d2a9db 319
T 320   /**
321    * Get last inserted record ID
322    * For Postgres databases, a sequence name is required
323    *
6b7e8e 324    * @param  string  Table name (to find the incremented sequence)
d2a9db 325    * @return mixed   ID or FALSE on failure
T 326    * @access public
327    */
6b7e8e 328   function insert_id($table = '')
d2a9db 329     {
T 330     if (!$this->db_handle || $this->db_mode=='r')
331       return FALSE;
332
6b7e8e 333     // find sequence name
A 334     if ($table && $this->db_provider == 'pgsql')
335       $table = get_sequence_name($table);
336
337     $id = $this->db_handle->lastInsertID($table);
6955c7 338     
A 339     return $this->db_handle->isError($id) ? null : $id;
d2a9db 340     }
T 341
342
343   /**
344    * Get an associative array for one row
345    * If no query handle is specified, the last query will be taken as reference
346    *
347    * @param  number  Optional query handle identifier
348    * @return mixed   Array with col values or FALSE on failure
349    * @access public
350    */
351   function fetch_assoc($res_id=NULL)
352     {
353     $result = $this->_get_result($res_id);
354     return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
355     }
356
357
358   /**
359    * Get an index array for one row
360    * If no query handle is specified, the last query will be taken as reference
361    *
362    * @param  number  Optional query handle identifier
363    * @return mixed   Array with col values or FALSE on failure
364    * @access public
365    */
366   function fetch_array($res_id=NULL)
367     {
368     $result = $this->_get_result($res_id);
369     return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
370     }
371
372
373   /**
9c5bee 374    * Get col values for a result row
d2a9db 375    *
T 376    * @param  object  Query result handle
377    * @param  number  Fetch mode identifier
378    * @return mixed   Array with col values or FALSE on failure
379    * @access private
380    */
381   function _fetch_row($result, $mode)
382     {
26d857 383     if ($result === FALSE || PEAR::isError($result) || !$this->is_connected())
d2a9db 384       return FALSE;
T 385
386     return $result->fetchRow($mode);
387     }
388
389
390   /**
391    * Formats input so it can be safely used in a query
392    *
393    * @param  mixed   Value to quote
394    * @return string  Quoted/converted string for use in query
395    * @access public
396    */
397   function quote($input, $type = null)
398     {
399     // create DB handle if not available
400     if (!$this->db_handle)
401       $this->db_connect('r');
402
403     // escape pear identifier chars
404     $rep_chars = array('?' => '\?',
405                        '!' => '\!',
406                        '&' => '\&');
407
408     return $this->db_handle->quote($input, $type);
409     }
410
411
412   /**
413    * Quotes a string so it can be safely used as a table or column name
414    *
415    * @param  string  Value to quote
416    * @return string  Quoted string for use in query
417    * @deprecated     Replaced by rcube_MDB2::quote_identifier
9c5bee 418    * @see            rcube_mdb2::quote_identifier
d2a9db 419    * @access public
T 420    */
421   function quoteIdentifier($str)
9c5bee 422     {
d2a9db 423     return $this->quote_identifier($str);
9c5bee 424     }
ccfda8 425
10a699 426
d2a9db 427   /**
T 428    * Quotes a string so it can be safely used as a table or column name
429    *
430    * @param  string  Value to quote
431    * @return string  Quoted string for use in query
432    * @access public
433    */
434   function quote_identifier($str)
f45ec7 435     {
d2a9db 436     if (!$this->db_handle)
T 437       $this->db_connect('r');
438
439     return $this->db_handle->quoteIdentifier($str);
f45ec7 440     }
S 441
8f4dcb 442   /**
T 443    * Escapes a string
444    *
445    * @param  string  The string to be escaped
446    * @return string  The escaped string
447    * @access public
448    * @since  0.1.1
449    */
450   function escapeSimple($str)
451     {
452     if (!$this->db_handle)
453       $this->db_connect('r');
454    
455     return $this->db_handle->escape($str);
456     }
457
f45ec7 458
d2a9db 459   /**
7139e3 460    * Return SQL function for current time and date
T 461    *
462    * @return string SQL function to use in query
463    * @access public
464    */
465   function now()
466     {
467     switch($this->db_provider)
468       {
469       case 'mssql':
2273d4 470       case 'sqlsrv':
7139e3 471         return "getdate()";
T 472
473       default:
474         return "now()";
475       }
476     }
477
478
479   /**
ad84f9 480    * Return list of elements for use with SQL's IN clause
A 481    *
482    * @param  string Input array
483    * @return string Elements list string
484    * @access public
485    */
486   function array2list($arr, $type=null)
487     {
488     if (!is_array($arr))
489       return $this->quote($arr, $type);
490     
491     $res = array();
492     foreach ($arr as $item)
493       $res[] = $this->quote($item, $type);
494
495     return implode(',', $res);
496     }
497
498
499   /**
d2a9db 500    * Return SQL statement to convert a field value into a unix timestamp
T 501    *
502    * @param  string  Field name
503    * @return string  SQL statement to use in query
504    * @access public
505    */
506   function unixtimestamp($field)
f45ec7 507     {
d2a9db 508     switch($this->db_provider)
T 509       {
510       case 'pgsql':
511         return "EXTRACT (EPOCH FROM $field)";
512         break;
513
7139e3 514       case 'mssql':
2273d4 515       case 'sqlsrv':
446533 516     return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
7139e3 517
d2a9db 518       default:
T 519         return "UNIX_TIMESTAMP($field)";
520       }
f45ec7 521     }
S 522
523
d2a9db 524   /**
T 525    * Return SQL statement to convert from a unix timestamp
526    *
527    * @param  string  Field name
528    * @return string  SQL statement to use in query
529    * @access public
530    */
531   function fromunixtime($timestamp)
f45ec7 532     {
d2a9db 533     switch($this->db_provider)
T 534       {
535       case 'mysqli':
536       case 'mysql':
537       case 'sqlite':
3978ab 538         return sprintf("FROM_UNIXTIME(%d)", $timestamp);
f45ec7 539
d2a9db 540       default:
T 541         return date("'Y-m-d H:i:s'", $timestamp);
542       }
f45ec7 543     }
S 544
d2a9db 545
T 546   /**
d8d416 547    * Return SQL statement for case insensitive LIKE
A 548    *
549    * @param  string  Field name
550    * @param  string  Search value
551    * @return string  SQL statement to use in query
552    * @access public
553    */
554   function ilike($column, $value)
555     {
556     // TODO: use MDB2's matchPattern() function
557     switch($this->db_provider)
558       {
559       case 'pgsql':
560         return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
561       default:
562         return $this->quote_identifier($column).' LIKE '.$this->quote($value);
563       }
564     }
565
566
567   /**
ac6229 568    * Encodes non-UTF-8 characters in string/array/object (recursive)
A 569    *
570    * @param  mixed  Data to fix
571    * @return mixed  Properly UTF-8 encoded data
572    * @access public
573    */
574   function encode($input)
575     {
576     if (is_object($input)) {
577       foreach (get_object_vars($input) as $idx => $value)
578         $input->$idx = $this->encode($value);
579       return $input;
580       }
581     else if (is_array($input)) {
582       foreach ($input as $idx => $value)
583         $input[$idx] = $this->encode($value);
584       return $input;    
585       }
586
587     return utf8_encode($input);
588     }
589
590
591   /**
592    * Decodes encoded UTF-8 string/object/array (recursive)
593    *
594    * @param  mixed  Input data
595    * @return mixed  Decoded data
596    * @access public
597    */
598   function decode($input)
599     {
600     if (is_object($input)) {
601       foreach (get_object_vars($input) as $idx => $value)
602         $input->$idx = $this->decode($value);
603       return $input;
604       }
605     else if (is_array($input)) {
606       foreach ($input as $idx => $value)
607         $input[$idx] = $this->decode($value);
608       return $input;    
609       }
610
611     return utf8_decode($input);
612     }
613
614
615   /**
d2a9db 616    * Adds a query result and returns a handle ID
T 617    *
618    * @param  object  Query handle
9c5bee 619    * @return mixed   Handle ID
d2a9db 620    * @access private
T 621    */
622   function _add_result($res)
f45ec7 623     {
d2a9db 624     // sql error occured
T 625     if (PEAR::isError($res))
626       {
9c5bee 627       $this->db_error = TRUE;
A 628       $this->db_error_msg = $res->getMessage();
d2a9db 629       raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
9c5bee 630             'message' => $res->getMessage() . " Query: " 
A 631         . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
632         TRUE, FALSE);
d2a9db 633       }
9c5bee 634     
A 635     $res_id = sizeof($this->a_query_results);
636     $this->last_res_id = $res_id;
637     $this->a_query_results[$res_id] = $res;
638     return $res_id;
f45ec7 639     }
d2a9db 640
T 641
642   /**
643    * Resolves a given handle ID and returns the according query handle
9c5bee 644    * If no ID is specified, the last resource handle will be returned
d2a9db 645    *
T 646    * @param  number  Handle ID
9c5bee 647    * @return mixed   Resource handle or FALSE on failure
d2a9db 648    * @access private
T 649    */
650   function _get_result($res_id=NULL)
651     {
652     if ($res_id==NULL)
653       $res_id = $this->last_res_id;
654
9c5bee 655     if (isset($this->a_query_results[$res_id]))
A 656       if (!PEAR::isError($this->a_query_results[$res_id]))
657         return $this->a_query_results[$res_id];
658     
659     return FALSE;
d2a9db 660     }
T 661
662
663   /**
664    * Create a sqlite database from a file
665    *
666    * @param  object  SQLite database handle
667    * @param  string  File path to use for DB creation
668    * @access private
669    */
670   function _sqlite_create_database($dbh, $file_name)
671     {
672     if (empty($file_name) || !is_string($file_name))
673       return;
674
ff73e0 675     $data = file_get_contents($file_name);
d2a9db 676
T 677     if (strlen($data))
50d515 678       if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh)) 
A 679         raise_error(array('code' => 500, 'type' => 'db',
680         'line' => __LINE__, 'file' => __FILE__, 'message' => $error), TRUE, FALSE); 
d2a9db 681     }
T 682
683
684   /**
685    * Add some proprietary database functions to the current SQLite handle
686    * in order to make it MySQL compatible
687    *
688    * @access private
689    */
690   function _sqlite_prepare()
691     {
692     include_once('include/rcube_sqlite.inc');
693
694     // we emulate via callback some missing MySQL function
695     sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
696     sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
697     sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
698     sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");
699     }
700
701
702   }  // end class rcube_db
f45ec7 703
981472 704
T 705 /* this is our own debug handler for the MDB2 connection */
706 function mdb2_debug_handler(&$db, $scope, $message, $context = array())
707 {
708   if ($scope != 'prepare')
709   {
710     $debug_output = $scope . '('.$db->db_index.'): ';
711     $debug_output .= $message . $db->getOption('log_line_break');
d559cb 712     write_log('sql', $debug_output);
981472 713   }
T 714 }