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