Aleksander Machniak
2013-10-14 5a2d2a6f75b115183459997cc2aa787d9a085fe8
commit | author | age
0d94fd 1 <?php
AM 2
3d231c 3 /**
0d94fd 4  +-----------------------------------------------------------------------+
AM 5  | This file is part of the Roundcube Webmail client                     |
6  | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
7  |                                                                       |
8  | Licensed under the GNU General Public License version 3 or            |
9  | any later version with exceptions for skins & plugins.                |
10  | See the README file for a full license statement.                     |
11  |                                                                       |
12  | PURPOSE:                                                              |
13  |   Database wrapper class that implements PHP PDO functions            |
14  +-----------------------------------------------------------------------+
15  | Author: Aleksander Machniak <alec@alec.pl>                            |
16  +-----------------------------------------------------------------------+
17 */
18
19 /**
9ab346 20  * Database independent query interface.
AM 21  * This is a wrapper for the PHP PDO.
0d94fd 22  *
9ab346 23  * @package   Framework
a07224 24  * @subpackage Database
0d94fd 25  */
AM 26 class rcube_db
27 {
88107d 28     public $db_provider;
TB 29
0d94fd 30     protected $db_dsnw;               // DSN for write operations
AM 31     protected $db_dsnr;               // DSN for read operations
32     protected $db_connected = false;  // Already connected ?
33     protected $db_mode;               // Connection mode
a69f99 34     protected $db_table_dsn_map = array();
0d94fd 35     protected $dbh;                   // Connection handle
92d18c 36     protected $dbhs = array();
a69f99 37     protected $table_connections = array();
0d94fd 38
a39859 39     protected $db_error     = false;
AM 40     protected $db_error_msg = '';
41     protected $conn_failure = false;
42     protected $db_index     = 0;
43     protected $last_result;
0d94fd 44     protected $tables;
c389a8 45     protected $variables;
0d94fd 46
AM 47     protected $options = array(
48         // column/table quotes
49         'identifier_start' => '"',
50         'identifier_end'   => '"',
51     );
52
9b8d22 53     const DEBUG_LINE_LENGTH = 4096;
0d94fd 54
AM 55     /**
56      * Factory, returns driver-specific instance of the class
57      *
3d231c 58      * @param string $db_dsnw DSN for read/write operations
AM 59      * @param string $db_dsnr Optional DSN for read only operations
60      * @param bool   $pconn   Enables persistent connections
0d94fd 61      *
3d231c 62      * @return rcube_db Object instance
0d94fd 63      */
AM 64     public static function factory($db_dsnw, $db_dsnr = '', $pconn = false)
65     {
66         $driver     = strtolower(substr($db_dsnw, 0, strpos($db_dsnw, ':')));
67         $driver_map = array(
68             'sqlite2' => 'sqlite',
69             'sybase'  => 'mssql',
70             'dblib'   => 'mssql',
1a2b50 71             'mysqli'  => 'mysql',
0d94fd 72         );
AM 73
74         $driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver;
75         $class  = "rcube_db_$driver";
76
41db2b 77         if (!$driver || !class_exists($class)) {
0d94fd 78             rcube::raise_error(array('code' => 600, 'type' => 'db',
AM 79                 'line' => __LINE__, 'file' => __FILE__,
80                 'message' => "Configuration error. Unsupported database driver: $driver"),
81                 true, true);
82         }
83
84         return new $class($db_dsnw, $db_dsnr, $pconn);
85     }
86
87     /**
88      * Object constructor
89      *
3d231c 90      * @param string $db_dsnw DSN for read/write operations
AM 91      * @param string $db_dsnr Optional DSN for read only operations
92      * @param bool   $pconn   Enables persistent connections
0d94fd 93      */
AM 94     public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
95     {
96         if (empty($db_dsnr)) {
97             $db_dsnr = $db_dsnw;
98         }
99
100         $this->db_dsnw  = $db_dsnw;
101         $this->db_dsnr  = $db_dsnr;
102         $this->db_pconn = $pconn;
92d18c 103         $this->db_dsnw_noread = rcube::get_instance()->config->get('db_dsnw_noread', false);
0d94fd 104
AM 105         $this->db_dsnw_array = self::parse_dsn($db_dsnw);
106         $this->db_dsnr_array = self::parse_dsn($db_dsnr);
a69f99 107
TB 108         $this->db_table_dsn_map = array_map(array($this, 'table_name'), rcube::get_instance()->config->get('db_table_dsn', array()));
0d94fd 109     }
AM 110
111     /**
112      * Connect to specific database
113      *
d18640 114      * @param array  $dsn  DSN for DB connections
AM 115      * @param string $mode Connection mode (r|w)
0d94fd 116      */
d18640 117     protected function dsn_connect($dsn, $mode)
0d94fd 118     {
AM 119         $this->db_error     = false;
120         $this->db_error_msg = null;
92d18c 121
TB 122         // return existing handle
123         if ($this->dbhs[$mode]) {
124             $this->dbh = $this->dbhs[$mode];
125             $this->db_mode = $mode;
126             return $this->dbh;
127         }
0d94fd 128
AM 129         // Get database specific connection options
130         $dsn_string  = $this->dsn_string($dsn);
131         $dsn_options = $this->dsn_options($dsn);
132
ce89ec 133         if ($this->db_pconn) {
0d94fd 134             $dsn_options[PDO::ATTR_PERSISTENT] = true;
AM 135         }
136
137         // Connect
138         try {
e6e5cb 139             // with this check we skip fatal error on PDO object creation
AM 140             if (!class_exists('PDO', false)) {
141                 throw new Exception('PDO extension not loaded. See http://php.net/manual/en/intro.pdo.php');
142             }
143
0d94fd 144             $this->conn_prepare($dsn);
AM 145
146             $dbh = new PDO($dsn_string, $dsn['username'], $dsn['password'], $dsn_options);
147
148             // don't throw exceptions or warnings
149             $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
150         }
e6e5cb 151         catch (Exception $e) {
0d94fd 152             $this->db_error     = true;
AM 153             $this->db_error_msg = $e->getMessage();
154
155             rcube::raise_error(array('code' => 500, 'type' => 'db',
156                 'line' => __LINE__, 'file' => __FILE__,
157                 'message' => $this->db_error_msg), true, false);
158
159             return null;
160         }
161
d18640 162         $this->dbh          = $dbh;
92d18c 163         $this->dbhs[$mode]  = $dbh;
d18640 164         $this->db_mode      = $mode;
AM 165         $this->db_connected = true;
0d94fd 166         $this->conn_configure($dsn, $dbh);
AM 167     }
168
3e386e 169     /**
AM 170      * Driver-specific preparation of database connection
171      *
3d231c 172      * @param array $dsn DSN for DB connections
3e386e 173      */
0d94fd 174     protected function conn_prepare($dsn)
AM 175     {
176     }
177
3e386e 178     /**
AM 179      * Driver-specific configuration of database connection
180      *
3d231c 181      * @param array $dsn DSN for DB connections
AM 182      * @param PDO   $dbh Connection handler
3e386e 183      */
0d94fd 184     protected function conn_configure($dsn, $dbh)
AM 185     {
3e386e 186     }
0d94fd 187
AM 188     /**
654ac1 189      * Connect to appropriate database depending on the operation
0d94fd 190      *
3d231c 191      * @param string $mode Connection mode (r|w)
a69f99 192      * @param boolean $force Enforce using the given mode
0d94fd 193      */
a69f99 194     public function db_connect($mode, $force = false)
0d94fd 195     {
AM 196         // previous connection failed, don't attempt to connect again
197         if ($this->conn_failure) {
198             return;
199         }
200
201         // no replication
202         if ($this->db_dsnw == $this->db_dsnr) {
203             $mode = 'w';
204         }
205
206         // Already connected
207         if ($this->db_connected) {
92d18c 208             // connected to db with the same or "higher" mode (if allowed)
a69f99 209             if ($this->db_mode == $mode || $this->db_mode == 'w' && !$force && !$this->db_dsnw_noread) {
0d94fd 210                 return;
AM 211             }
212         }
213
214         $dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
d18640 215         $this->dsn_connect($dsn, $mode);
0d94fd 216
AM 217         // use write-master when read-only fails
bc2c02 218         if (!$this->db_connected && $mode == 'r' && $this->is_replicated()) {
d18640 219             $this->dsn_connect($this->db_dsnw_array, 'w');
0d94fd 220         }
AM 221
d18640 222         $this->conn_failure = !$this->db_connected;
a69f99 223     }
TB 224
225     /**
226      * Analyze the given SQL statement and select the appropriate connection to use
227      */
228     protected function dsn_select($query)
229     {
230         // no replication
231         if ($this->db_dsnw == $this->db_dsnr) {
232             return 'w';
233         }
234
235         // Read or write ?
236         $mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
237
238         // find tables involved in this query
239         if (preg_match_all('/(?:^|\s)(from|update|into|join)\s+'.$this->options['identifier_start'].'?([a-z0-9._]+)'.$this->options['identifier_end'].'?\s+/i', $query, $matches, PREG_SET_ORDER)) {
240             foreach ($matches as $m) {
241                 $table = $m[2];
242
243                 // always use direct mapping
244                 if ($this->db_table_dsn_map[$table]) {
245                     $mode = $this->db_table_dsn_map[$table];
246                     break;  // primary table rules
247                 }
248                 else if ($mode == 'r') {
249                     // connected to db with the same or "higher" mode for this table
250                     $db_mode = $this->table_connections[$table];
251                     if ($db_mode == 'w' && !$this->db_dsnw_noread) {
252                         $mode = $db_mode;
253                     }
254                 }
255             }
256
257             // remember mode chosen (for primary table)
258             $table = $matches[0][2];
6a6992 259             $this->table_connections[$table] = $mode;
a69f99 260         }
TB 261
262         return $mode;
0d94fd 263     }
AM 264
265     /**
266      * Activate/deactivate debug mode
267      *
268      * @param boolean $dbg True if SQL queries should be logged
269      */
270     public function set_debug($dbg = true)
271     {
272         $this->options['debug_mode'] = $dbg;
273     }
274
275     /**
329eae 276      * Writes debug information/query to 'sql' log file
AM 277      *
278      * @param string $query SQL query
279      */
280     protected function debug($query)
281     {
282         if ($this->options['debug_mode']) {
9b8d22 283             if (($len = strlen($query)) > self::DEBUG_LINE_LENGTH) {
43079d 284                 $diff  = $len - self::DEBUG_LINE_LENGTH;
AM 285                 $query = substr($query, 0, self::DEBUG_LINE_LENGTH)
286                     . "... [truncated $diff bytes]";
9b8d22 287             }
329eae 288             rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';');
AM 289         }
290     }
291
292     /**
0d94fd 293      * Getter for error state
AM 294      *
a39859 295      * @param mixed $result Optional query result
ea08d4 296      *
154425 297      * @return string Error message
0d94fd 298      */
a39859 299     public function is_error($result = null)
0d94fd 300     {
a39859 301         if ($result !== null) {
AM 302             return $result === false ? $this->db_error_msg : null;
ea08d4 303         }
AM 304
154425 305         return $this->db_error ? $this->db_error_msg : null;
0d94fd 306     }
AM 307
308     /**
309      * Connection state checker
310      *
3d231c 311      * @return boolean True if in connected state
0d94fd 312      */
AM 313     public function is_connected()
314     {
315         return !is_object($this->dbh) ? false : $this->db_connected;
316     }
317
318     /**
319      * Is database replication configured?
3d231c 320      *
AM 321      * @return bool Returns true if dsnw != dsnr
0d94fd 322      */
AM 323     public function is_replicated()
324     {
325       return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr;
326     }
327
328     /**
c389a8 329      * Get database runtime variables
AM 330      *
3d231c 331      * @param string $varname Variable name
AM 332      * @param mixed  $default Default value if variable is not set
c389a8 333      *
AM 334      * @return mixed Variable value or default
335      */
336     public function get_variable($varname, $default = null)
337     {
338         // to be implemented by driver class
339         return $default;
340     }
341
342     /**
0d94fd 343      * Execute a SQL query
AM 344      *
3d231c 345      * @param string SQL query to execute
AM 346      * @param mixed  Values to be inserted in query
0d94fd 347      *
AM 348      * @return number  Query handle identifier
349      */
350     public function query()
351     {
352         $params = func_get_args();
353         $query = array_shift($params);
354
355         // Support one argument of type array, instead of n arguments
356         if (count($params) == 1 && is_array($params[0])) {
357             $params = $params[0];
358         }
359
360         return $this->_query($query, 0, 0, $params);
361     }
362
363     /**
364      * Execute a SQL query with limits
365      *
3d231c 366      * @param string SQL query to execute
AM 367      * @param int    Offset for LIMIT statement
368      * @param int    Number of rows for LIMIT statement
369      * @param mixed  Values to be inserted in query
0d94fd 370      *
a39859 371      * @return PDOStatement|bool Query handle or False on error
0d94fd 372      */
AM 373     public function limitquery()
374     {
375         $params  = func_get_args();
376         $query   = array_shift($params);
377         $offset  = array_shift($params);
378         $numrows = array_shift($params);
379
380         return $this->_query($query, $offset, $numrows, $params);
381     }
382
383     /**
384      * Execute a SQL query with limits
385      *
3d231c 386      * @param string $query   SQL query to execute
AM 387      * @param int    $offset  Offset for LIMIT statement
388      * @param int    $numrows Number of rows for LIMIT statement
389      * @param array  $params  Values to be inserted in query
390      *
a39859 391      * @return PDOStatement|bool Query handle or False on error
0d94fd 392      */
AM 393     protected function _query($query, $offset, $numrows, $params)
394     {
66407a 395         $query = trim($query);
AM 396
a69f99 397         $this->db_connect($this->dsn_select($query), true);
0d94fd 398
AM 399         // check connection before proceeding
400         if (!$this->is_connected()) {
a39859 401             return $this->last_result = false;
0d94fd 402         }
AM 403
404         if ($numrows || $offset) {
405             $query = $this->set_limit($query, $numrows, $offset);
406         }
407
408         $params = (array) $params;
409
410         // Because in Roundcube we mostly use queries that are
411         // executed only once, we will not use prepared queries
412         $pos = 0;
413         $idx = 0;
414
415         while ($pos = strpos($query, '?', $pos)) {
13969c 416             if ($query[$pos+1] == '?') {  // skip escaped ?
TB 417                 $pos += 2;
418             }
419             else {
420                 $val = $this->quote($params[$idx++]);
421                 unset($params[$idx-1]);
422                 $query = substr_replace($query, $val, $pos, 1);
423                 $pos += strlen($val);
424             }
0d94fd 425         }
AM 426
13969c 427         // replace escaped ? back to normal
TB 428         $query = rtrim(strtr($query, array('??' => '?')), ';');
0d94fd 429
329eae 430         $this->debug($query);
0d94fd 431
a61326 432         // destroy reference to previous result, required for SQLite driver (#1488874)
AM 433         $this->last_result = null;
db6f54 434         $this->db_error_msg = null;
a61326 435
AM 436         // send query
8defd7 437         $result = $this->dbh->query($query);
0d94fd 438
8defd7 439         if ($result === false) {
0ee22c 440             $result = $this->handle_error($query);
0d94fd 441         }
AM 442
8defd7 443         $this->last_result = $result;
a39859 444
8defd7 445         return $result;
0d94fd 446     }
AM 447
448     /**
0ee22c 449      * Helper method to handle DB errors.
TB 450      * This by default logs the error but could be overriden by a driver implementation
451      *
452      * @param string Query that triggered the error
453      * @return mixed Result to be stored and returned
454      */
455     protected function handle_error($query)
456     {
457         $error = $this->dbh->errorInfo();
458
459         if (empty($this->options['ignore_key_errors']) || $error[0] != '23000') {
460             $this->db_error = true;
461             $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
462
463             rcube::raise_error(array('code' => 500, 'type' => 'db',
464                 'line' => __LINE__, 'file' => __FILE__,
465                 'message' => $this->db_error_msg . " (SQL Query: $query)"
466                 ), true, false);
467         }
468
469         return false;
470     }
471
472     /**
0d94fd 473      * Get number of affected rows for the last query
AM 474      *
a39859 475      * @param mixed $result Optional query handle
3d231c 476      *
679b37 477      * @return int Number of (matching) rows
0d94fd 478      */
a39859 479     public function affected_rows($result = null)
0d94fd 480     {
a39859 481         if ($result || ($result === null && ($result = $this->last_result))) {
0d94fd 482             return $result->rowCount();
AM 483         }
484
485         return 0;
486     }
487
488     /**
d4f8a4 489      * Get number of rows for a SQL query
TB 490      * If no query handle is specified, the last query will be taken as reference
491      *
492      * @param mixed $result Optional query handle
493      * @return mixed   Number of rows or false on failure
a85d54 494      * @deprecated This method shows very poor performance and should be avoided.
d4f8a4 495      */
TB 496     public function num_rows($result = null)
497     {
498         if ($result || ($result === null && ($result = $this->last_result))) {
499             // repeat query with SELECT COUNT(*) ...
7889c5 500             if (preg_match('/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/ims', $result->queryString, $m)) {
d4f8a4 501                 $query = $this->dbh->query('SELECT COUNT(*) FROM ' . $m[1], PDO::FETCH_NUM);
TB 502                 return $query ? intval($query->fetchColumn(0)) : false;
503             }
504             else {
a85d54 505                 $num = count($result->fetchAll());
TB 506                 $result->execute();  // re-execute query because there's no seek(0)
507                 return $num;
d4f8a4 508             }
TB 509         }
510
511         return false;
512     }
513
514     /**
0d94fd 515      * Get last inserted record ID
AM 516      *
3d231c 517      * @param string $table Table name (to find the incremented sequence)
0d94fd 518      *
3d231c 519      * @return mixed ID or false on failure
0d94fd 520      */
AM 521     public function insert_id($table = '')
522     {
523         if (!$this->db_connected || $this->db_mode == 'r') {
524             return false;
525         }
526
527         if ($table) {
528             // resolve table name
529             $table = $this->table_name($table);
530         }
531
532         $id = $this->dbh->lastInsertId($table);
533
534         return $id;
535     }
536
537     /**
538      * Get an associative array for one row
539      * If no query handle is specified, the last query will be taken as reference
540      *
a39859 541      * @param mixed $result Optional query handle
0d94fd 542      *
3d231c 543      * @return mixed Array with col values or false on failure
0d94fd 544      */
a39859 545     public function fetch_assoc($result = null)
0d94fd 546     {
AM 547         return $this->_fetch_row($result, PDO::FETCH_ASSOC);
548     }
549
550     /**
551      * Get an index array for one row
552      * If no query handle is specified, the last query will be taken as reference
553      *
a39859 554      * @param mixed $result Optional query handle
0d94fd 555      *
3d231c 556      * @return mixed Array with col values or false on failure
0d94fd 557      */
a39859 558     public function fetch_array($result = null)
0d94fd 559     {
AM 560         return $this->_fetch_row($result, PDO::FETCH_NUM);
561     }
562
563     /**
564      * Get col values for a result row
565      *
a39859 566      * @param mixed $result Optional query handle
AM 567      * @param int   $mode   Fetch mode identifier
0d94fd 568      *
3d231c 569      * @return mixed Array with col values or false on failure
0d94fd 570      */
AM 571     protected function _fetch_row($result, $mode)
572     {
a39859 573         if ($result || ($result === null && ($result = $this->last_result))) {
AM 574             return $result->fetch($mode);
0d94fd 575         }
AM 576
a39859 577         return false;
0d94fd 578     }
AM 579
580     /**
581      * Adds LIMIT,OFFSET clauses to the query
582      *
3d231c 583      * @param string $query  SQL query
AM 584      * @param int    $limit  Number of rows
585      * @param int    $offset Offset
8c2375 586      *
AM 587      * @return string SQL query
0d94fd 588      */
AM 589     protected function set_limit($query, $limit = 0, $offset = 0)
590     {
591         if ($limit) {
592             $query .= ' LIMIT ' . intval($limit);
593         }
594
595         if ($offset) {
596             $query .= ' OFFSET ' . intval($offset);
597         }
598
599         return $query;
600     }
601
602     /**
603      * Returns list of tables in a database
604      *
605      * @return array List of all tables of the current database
606      */
607     public function list_tables()
608     {
609         // get tables if not cached
610         if ($this->tables === null) {
611             $q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_NAME');
612
a39859 613             if ($q) {
AM 614                 $this->tables = $q->fetchAll(PDO::FETCH_COLUMN, 0);
0d94fd 615             }
AM 616             else {
617                 $this->tables = array();
618             }
619         }
620
621         return $this->tables;
622     }
623
624     /**
625      * Returns list of columns in database table
626      *
3d231c 627      * @param string $table Table name
0d94fd 628      *
AM 629      * @return array List of table cols
630      */
631     public function list_cols($table)
632     {
633         $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?',
634             array($table));
635
a39859 636         if ($q) {
AM 637             return $q->fetchAll(PDO::FETCH_COLUMN, 0);
0d94fd 638         }
AM 639
640         return array();
641     }
642
643     /**
644      * Formats input so it can be safely used in a query
645      *
3d231c 646      * @param mixed  $input Value to quote
ac3774 647      * @param string $type  Type of data (integer, bool, ident)
0d94fd 648      *
3d231c 649      * @return string Quoted/converted string for use in query
0d94fd 650      */
AM 651     public function quote($input, $type = null)
652     {
653         // handle int directly for better performance
654         if ($type == 'integer' || $type == 'int') {
655             return intval($input);
656         }
657
0db8d0 658         if (is_null($input)) {
TB 659             return 'NULL';
ac3774 660         }
AM 661
662         if ($type == 'ident') {
663             return $this->quote_identifier($input);
0db8d0 664         }
TB 665
0d94fd 666         // create DB handle if not available
AM 667         if (!$this->dbh) {
668             $this->db_connect('r');
669         }
670
671         if ($this->dbh) {
672             $map = array(
673                 'bool'    => PDO::PARAM_BOOL,
674                 'integer' => PDO::PARAM_INT,
675             );
676             $type = isset($map[$type]) ? $map[$type] : PDO::PARAM_STR;
13969c 677             return strtr($this->dbh->quote($input, $type), array('?' => '??'));  // escape ?
0d94fd 678         }
AM 679
680         return 'NULL';
681     }
682
683     /**
282dff 684      * Escapes a string so it can be safely used in a query
AM 685      *
686      * @param string $str A string to escape
687      *
688      * @return string Escaped string for use in a query
689      */
690     public function escape($str)
691     {
692         if (is_null($str)) {
693             return 'NULL';
694         }
695
696         return substr($this->quote($str), 1, -1);
697     }
698
699     /**
0d94fd 700      * Quotes a string so it can be safely used as a table or column name
AM 701      *
3d231c 702      * @param string $str Value to quote
0d94fd 703      *
3d231c 704      * @return string Quoted string for use in query
AM 705      * @deprecated    Replaced by rcube_db::quote_identifier
706      * @see           rcube_db::quote_identifier
0d94fd 707      */
AM 708     public function quoteIdentifier($str)
709     {
710         return $this->quote_identifier($str);
711     }
712
713     /**
282dff 714      * Escapes a string so it can be safely used in a query
0d94fd 715      *
282dff 716      * @param string $str A string to escape
0d94fd 717      *
282dff 718      * @return string Escaped string for use in a query
AM 719      * @deprecated    Replaced by rcube_db::escape
720      * @see           rcube_db::escape
39a034 721      */
c465ee 722     public function escapeSimple($str)
39a034 723     {
282dff 724         return $this->escape($str);
39a034 725     }
AM 726
727     /**
728      * Quotes a string so it can be safely used as a table or column name
729      *
730      * @param string $str Value to quote
731      *
732      * @return string Quoted string for use in query
0d94fd 733      */
AM 734     public function quote_identifier($str)
735     {
736         $start = $this->options['identifier_start'];
737         $end   = $this->options['identifier_end'];
738         $name  = array();
739
740         foreach (explode('.', $str) as $elem) {
741             $elem = str_replace(array($start, $end), '', $elem);
742             $name[] = $start . $elem . $end;
743         }
744
ac3774 745         return implode($name, '.');
0d94fd 746     }
AM 747
748     /**
749      * Return SQL function for current time and date
750      *
aa44ce 751      * @param int $interval Optional interval (in seconds) to add/subtract
AM 752      *
0d94fd 753      * @return string SQL function to use in query
AM 754      */
aa44ce 755     public function now($interval = 0)
0d94fd 756     {
aa44ce 757         if ($interval) {
AM 758             $add = ' ' . ($interval > 0 ? '+' : '-') . ' INTERVAL ';
759             $add .= $interval > 0 ? intval($interval) : intval($interval) * -1;
60b6d7 760             $add .= ' SECOND';
aa44ce 761         }
AM 762
763         return "now()" . $add;
0d94fd 764     }
AM 765
766     /**
767      * Return list of elements for use with SQL's IN clause
768      *
3d231c 769      * @param array  $arr  Input array
ac3774 770      * @param string $type Type of data (integer, bool, ident)
0d94fd 771      *
AM 772      * @return string Comma-separated list of quoted values for use in query
773      */
774     public function array2list($arr, $type = null)
775     {
776         if (!is_array($arr)) {
777             return $this->quote($arr, $type);
778         }
779
780         foreach ($arr as $idx => $item) {
781             $arr[$idx] = $this->quote($item, $type);
782         }
783
784         return implode(',', $arr);
785     }
786
787     /**
788      * Return SQL statement to convert a field value into a unix timestamp
789      *
790      * This method is deprecated and should not be used anymore due to limitations
791      * of timestamp functions in Mysql (year 2038 problem)
792      *
3d231c 793      * @param string $field Field name
0d94fd 794      *
AM 795      * @return string  SQL statement to use in query
796      * @deprecated
797      */
798     public function unixtimestamp($field)
799     {
800         return "UNIX_TIMESTAMP($field)";
801     }
802
803     /**
804      * Return SQL statement to convert from a unix timestamp
805      *
3d231c 806      * @param int $timestamp Unix timestamp
0d94fd 807      *
3d231c 808      * @return string Date string in db-specific format
0d94fd 809      */
AM 810     public function fromunixtime($timestamp)
811     {
812         return date("'Y-m-d H:i:s'", $timestamp);
813     }
814
815     /**
816      * Return SQL statement for case insensitive LIKE
817      *
3d231c 818      * @param string $column Field name
AM 819      * @param string $value  Search value
0d94fd 820      *
3d231c 821      * @return string SQL statement to use in query
0d94fd 822      */
AM 823     public function ilike($column, $value)
824     {
825         return $this->quote_identifier($column).' LIKE '.$this->quote($value);
826     }
827
828     /**
829      * Abstract SQL statement for value concatenation
830      *
831      * @return string SQL statement to be used in query
832      */
833     public function concat(/* col1, col2, ... */)
834     {
835         $args = func_get_args();
836         if (is_array($args[0])) {
837             $args = $args[0];
838         }
839
840         return '(' . join(' || ', $args) . ')';
841     }
842
843     /**
844      * Encodes non-UTF-8 characters in string/array/object (recursive)
845      *
a6b0ca 846      * @param mixed $input      Data to fix
AM 847      * @param bool  $serialized Enable serialization
0d94fd 848      *
3d231c 849      * @return mixed Properly UTF-8 encoded data
0d94fd 850      */
a6b0ca 851     public static function encode($input, $serialized = false)
0d94fd 852     {
a6b0ca 853         // use Base64 encoding to workaround issues with invalid
AM 854         // or null characters in serialized string (#1489142)
855         if ($serialized) {
856             return base64_encode(serialize($input));
857         }
858
0d94fd 859         if (is_object($input)) {
AM 860             foreach (get_object_vars($input) as $idx => $value) {
861                 $input->$idx = self::encode($value);
862             }
863             return $input;
864         }
865         else if (is_array($input)) {
866             foreach ($input as $idx => $value) {
867                 $input[$idx] = self::encode($value);
868             }
a6b0ca 869
0d94fd 870             return $input;
AM 871         }
872
873         return utf8_encode($input);
874     }
875
876     /**
877      * Decodes encoded UTF-8 string/object/array (recursive)
878      *
a6b0ca 879      * @param mixed $input      Input data
AM 880      * @param bool  $serialized Enable serialization
0d94fd 881      *
3d231c 882      * @return mixed Decoded data
0d94fd 883      */
a6b0ca 884     public static function decode($input, $serialized = false)
0d94fd 885     {
5df4fe 886         // use Base64 encoding to workaround issues with invalid
AM 887         // or null characters in serialized string (#1489142)
a6b0ca 888         if ($serialized) {
5df4fe 889             // Keep backward compatybility where base64 wasn't used
AM 890             if (strpos(substr($input, 0, 16), ':') !== false) {
891                 return self::decode(@unserialize($input));
892             }
893
a6b0ca 894             return @unserialize(base64_decode($input));
AM 895         }
896
0d94fd 897         if (is_object($input)) {
AM 898             foreach (get_object_vars($input) as $idx => $value) {
899                 $input->$idx = self::decode($value);
900             }
901             return $input;
902         }
903         else if (is_array($input)) {
904             foreach ($input as $idx => $value) {
905                 $input[$idx] = self::decode($value);
906             }
907             return $input;
908         }
909
910         return utf8_decode($input);
911     }
912
913     /**
914      * Return correct name for a specific database table
915      *
916      * @param string $table Table name
917      *
918      * @return string Translated table name
919      */
920     public function table_name($table)
921     {
a69f99 922         static $rcube;
TB 923
924         if (!$rcube) {
925             $rcube = rcube::get_instance();
926         }
0d94fd 927
399db1 928         // add prefix to the table name if configured
a69f99 929         if (($prefix = $rcube->config->get('db_prefix')) && strpos($table, $prefix) !== 0) {
399db1 930             return $prefix . $table;
0d94fd 931         }
AM 932
933         return $table;
934     }
935
936     /**
be4b5c 937      * Set class option value
AM 938      *
939      * @param string $name  Option name
940      * @param mixed  $value Option value
941      */
942     public function set_option($name, $value)
943     {
944         $this->options[$name] = $value;
945     }
946
947     /**
a69f99 948      * Set DSN connection to be used for the given table
TB 949      *
950      * @param string Table name
951      * @param string DSN connection ('r' or 'w') to be used
952      */
953     public function set_table_dsn($table, $mode)
954     {
955         $this->db_table_dsn_map[$this->table_name($table)] = $mode;
956     }
957
958     /**
0d94fd 959      * MDB2 DSN string parser
3e386e 960      *
AM 961      * @param string $sequence Secuence name
962      *
963      * @return array DSN parameters
0d94fd 964      */
AM 965     public static function parse_dsn($dsn)
966     {
967         if (empty($dsn)) {
968             return null;
969         }
970
971         // Find phptype and dbsyntax
972         if (($pos = strpos($dsn, '://')) !== false) {
973             $str = substr($dsn, 0, $pos);
974             $dsn = substr($dsn, $pos + 3);
3d231c 975         }
AM 976         else {
0d94fd 977             $str = $dsn;
AM 978             $dsn = null;
979         }
980
981         // Get phptype and dbsyntax
982         // $str => phptype(dbsyntax)
983         if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
984             $parsed['phptype']  = $arr[1];
985             $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
3d231c 986         }
AM 987         else {
0d94fd 988             $parsed['phptype']  = $str;
AM 989             $parsed['dbsyntax'] = $str;
990         }
991
992         if (empty($dsn)) {
993             return $parsed;
994         }
995
996         // Get (if found): username and password
997         // $dsn => username:password@protocol+hostspec/database
998         if (($at = strrpos($dsn,'@')) !== false) {
999             $str = substr($dsn, 0, $at);
1000             $dsn = substr($dsn, $at + 1);
1001             if (($pos = strpos($str, ':')) !== false) {
1002                 $parsed['username'] = rawurldecode(substr($str, 0, $pos));
1003                 $parsed['password'] = rawurldecode(substr($str, $pos + 1));
3d231c 1004             }
AM 1005             else {
0d94fd 1006                 $parsed['username'] = rawurldecode($str);
AM 1007             }
1008         }
1009
1010         // Find protocol and hostspec
1011
1012         // $dsn => proto(proto_opts)/database
1013         if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
1014             $proto       = $match[1];
1015             $proto_opts  = $match[2] ? $match[2] : false;
1016             $dsn         = $match[3];
1017         }
1018         // $dsn => protocol+hostspec/database (old format)
1019         else {
1020             if (strpos($dsn, '+') !== false) {
1021                 list($proto, $dsn) = explode('+', $dsn, 2);
1022             }
1023             if (   strpos($dsn, '//') === 0
1024                 && strpos($dsn, '/', 2) !== false
1025                 && $parsed['phptype'] == 'oci8'
1026             ) {
1027                 //oracle's "Easy Connect" syntax:
1028                 //"username/password@[//]host[:port][/service_name]"
1029                 //e.g. "scott/tiger@//mymachine:1521/oracle"
1030                 $proto_opts = $dsn;
1031                 $pos = strrpos($proto_opts, '/');
1032                 $dsn = substr($proto_opts, $pos + 1);
1033                 $proto_opts = substr($proto_opts, 0, $pos);
3d231c 1034             }
AM 1035             else if (strpos($dsn, '/') !== false) {
0d94fd 1036                 list($proto_opts, $dsn) = explode('/', $dsn, 2);
3d231c 1037             }
AM 1038             else {
0d94fd 1039                 $proto_opts = $dsn;
AM 1040                 $dsn = null;
1041             }
1042         }
1043
1044         // process the different protocol options
1045         $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp';
1046         $proto_opts = rawurldecode($proto_opts);
1047         if (strpos($proto_opts, ':') !== false) {
1048             list($proto_opts, $parsed['port']) = explode(':', $proto_opts);
1049         }
1050         if ($parsed['protocol'] == 'tcp') {
1051             $parsed['hostspec'] = $proto_opts;
1052         }
1053         else if ($parsed['protocol'] == 'unix') {
1054             $parsed['socket'] = $proto_opts;
1055         }
1056
1057         // Get dabase if any
1058         // $dsn => database
1059         if ($dsn) {
1060             // /database
1061             if (($pos = strpos($dsn, '?')) === false) {
1062                 $parsed['database'] = rawurldecode($dsn);
1063             // /database?param1=value1&param2=value2
1064             }
1065             else {
1066                 $parsed['database'] = rawurldecode(substr($dsn, 0, $pos));
1067                 $dsn = substr($dsn, $pos + 1);
1068                 if (strpos($dsn, '&') !== false) {
1069                     $opts = explode('&', $dsn);
3d231c 1070                 }
AM 1071                 else { // database?param1=value1
0d94fd 1072                     $opts = array($dsn);
AM 1073                 }
1074                 foreach ($opts as $opt) {
1075                     list($key, $value) = explode('=', $opt);
1076                     if (!array_key_exists($key, $parsed) || false === $parsed[$key]) {
1077                         // don't allow params overwrite
1078                         $parsed[$key] = rawurldecode($value);
1079                     }
1080                 }
1081             }
1082         }
1083
1084         return $parsed;
1085     }
1086
1087     /**
3e386e 1088      * Returns PDO DSN string from DSN array
AM 1089      *
3d231c 1090      * @param array $dsn DSN parameters
3e386e 1091      *
AM 1092      * @return string DSN string
0d94fd 1093      */
AM 1094     protected function dsn_string($dsn)
1095     {
1096         $params = array();
1097         $result = $dsn['phptype'] . ':';
1098
1099         if ($dsn['hostspec']) {
1100             $params[] = 'host=' . $dsn['hostspec'];
1101         }
1102
1103         if ($dsn['port']) {
1104             $params[] = 'port=' . $dsn['port'];
1105         }
1106
1107         if ($dsn['database']) {
1108             $params[] = 'dbname=' . $dsn['database'];
1109         }
1110
1111         if (!empty($params)) {
1112             $result .= implode(';', $params);
1113         }
1114
1115         return $result;
1116     }
1117
1118     /**
3e386e 1119      * Returns driver-specific connection options
AM 1120      *
3d231c 1121      * @param array $dsn DSN parameters
3e386e 1122      *
AM 1123      * @return array Connection options
0d94fd 1124      */
AM 1125     protected function dsn_options($dsn)
1126     {
1127         $result = array();
1128
1129         return $result;
1130     }
1131 }