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