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