Aleksander Machniak
2012-12-28 be72fb3597c21ca3aaa058adf41bb72d53d197c7
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
34     protected $dbh;                   // Connection handle
35
a39859 36     protected $db_error     = false;
AM 37     protected $db_error_msg = '';
38     protected $conn_failure = false;
39     protected $db_index     = 0;
40     protected $last_result;
0d94fd 41     protected $tables;
c389a8 42     protected $variables;
0d94fd 43
AM 44     protected $options = array(
45         // column/table quotes
46         'identifier_start' => '"',
47         'identifier_end'   => '"',
48     );
49
50
51     /**
52      * Factory, returns driver-specific instance of the class
53      *
3d231c 54      * @param string $db_dsnw DSN for read/write operations
AM 55      * @param string $db_dsnr Optional DSN for read only operations
56      * @param bool   $pconn   Enables persistent connections
0d94fd 57      *
3d231c 58      * @return rcube_db Object instance
0d94fd 59      */
AM 60     public static function factory($db_dsnw, $db_dsnr = '', $pconn = false)
61     {
62         $driver     = strtolower(substr($db_dsnw, 0, strpos($db_dsnw, ':')));
63         $driver_map = array(
64             'sqlite2' => 'sqlite',
65             'sybase'  => 'mssql',
66             'dblib'   => 'mssql',
1a2b50 67             'mysqli'  => 'mysql',
0d94fd 68         );
AM 69
70         $driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver;
71         $class  = "rcube_db_$driver";
72
73         if (!class_exists($class)) {
74             rcube::raise_error(array('code' => 600, 'type' => 'db',
75                 'line' => __LINE__, 'file' => __FILE__,
76                 'message' => "Configuration error. Unsupported database driver: $driver"),
77                 true, true);
78         }
79
80         return new $class($db_dsnw, $db_dsnr, $pconn);
81     }
82
83     /**
84      * Object constructor
85      *
3d231c 86      * @param string $db_dsnw DSN for read/write operations
AM 87      * @param string $db_dsnr Optional DSN for read only operations
88      * @param bool   $pconn   Enables persistent connections
0d94fd 89      */
AM 90     public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
91     {
92         if (empty($db_dsnr)) {
93             $db_dsnr = $db_dsnw;
94         }
95
96         $this->db_dsnw  = $db_dsnw;
97         $this->db_dsnr  = $db_dsnr;
98         $this->db_pconn = $pconn;
99
100         $this->db_dsnw_array = self::parse_dsn($db_dsnw);
101         $this->db_dsnr_array = self::parse_dsn($db_dsnr);
102
103         // Initialize driver class
104         $this->init();
105     }
106
3e386e 107     /**
AM 108      * Initialization of the object with driver specific code
109      */
0d94fd 110     protected function init()
AM 111     {
112         // To be used by driver classes
113     }
114
115     /**
116      * Connect to specific database
117      *
3d231c 118      * @param array $dsn DSN for DB connections
0d94fd 119      *
AM 120      * @return PDO database handle
121      */
122     protected function dsn_connect($dsn)
123     {
124         $this->db_error     = false;
125         $this->db_error_msg = null;
126
127         // Get database specific connection options
128         $dsn_string  = $this->dsn_string($dsn);
129         $dsn_options = $this->dsn_options($dsn);
130
131         if ($db_pconn) {
132             $dsn_options[PDO::ATTR_PERSISTENT] = true;
133         }
134
135         // Connect
136         try {
e6e5cb 137             // with this check we skip fatal error on PDO object creation
AM 138             if (!class_exists('PDO', false)) {
139                 throw new Exception('PDO extension not loaded. See http://php.net/manual/en/intro.pdo.php');
140             }
141
0d94fd 142             $this->conn_prepare($dsn);
AM 143
144             $dbh = new PDO($dsn_string, $dsn['username'], $dsn['password'], $dsn_options);
145
146             // don't throw exceptions or warnings
147             $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
148         }
e6e5cb 149         catch (Exception $e) {
0d94fd 150             $this->db_error     = true;
AM 151             $this->db_error_msg = $e->getMessage();
152
153             rcube::raise_error(array('code' => 500, 'type' => 'db',
154                 'line' => __LINE__, 'file' => __FILE__,
155                 'message' => $this->db_error_msg), true, false);
156
157             return null;
158         }
159
160         $this->conn_configure($dsn, $dbh);
161
162         return $dbh;
163     }
164
3e386e 165     /**
AM 166      * Driver-specific preparation of database connection
167      *
3d231c 168      * @param array $dsn DSN for DB connections
3e386e 169      */
0d94fd 170     protected function conn_prepare($dsn)
AM 171     {
172     }
173
3e386e 174     /**
AM 175      * Driver-specific configuration of database connection
176      *
3d231c 177      * @param array $dsn DSN for DB connections
AM 178      * @param PDO   $dbh Connection handler
3e386e 179      */
0d94fd 180     protected function conn_configure($dsn, $dbh)
AM 181     {
182     }
183
3e386e 184     /**
AM 185      * Driver-specific database character set setting
186      *
3d231c 187      * @param string $charset Character set name
3e386e 188      */
AM 189     protected function set_charset($charset)
190     {
191         $this->query("SET NAMES 'utf8'");
192     }
0d94fd 193
AM 194     /**
654ac1 195      * Connect to appropriate database depending on the operation
0d94fd 196      *
3d231c 197      * @param string $mode Connection mode (r|w)
0d94fd 198      */
AM 199     public function db_connect($mode)
200     {
201         // previous connection failed, don't attempt to connect again
202         if ($this->conn_failure) {
203             return;
204         }
205
206         // no replication
207         if ($this->db_dsnw == $this->db_dsnr) {
208             $mode = 'w';
209         }
210
211         // Already connected
212         if ($this->db_connected) {
213             // connected to db with the same or "higher" mode
214             if ($this->db_mode == 'w' || $this->db_mode == $mode) {
215                 return;
216             }
217         }
218
219         $dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
220
221         $this->dbh          = $this->dsn_connect($dsn);
222         $this->db_connected = is_object($this->dbh);
223
224         // use write-master when read-only fails
225         if (!$this->db_connected && $mode == 'r') {
226             $mode = 'w';
227             $this->dbh          = $this->dsn_connect($this->db_dsnw_array);
228             $this->db_connected = is_object($this->dbh);
229         }
230
231         if ($this->db_connected) {
232             $this->db_mode = $mode;
233             $this->set_charset('utf8');
234         }
235         else {
236             $this->conn_failure = true;
237         }
238     }
239
240     /**
241      * Activate/deactivate debug mode
242      *
243      * @param boolean $dbg True if SQL queries should be logged
244      */
245     public function set_debug($dbg = true)
246     {
247         $this->options['debug_mode'] = $dbg;
248     }
249
250     /**
329eae 251      * Writes debug information/query to 'sql' log file
AM 252      *
253      * @param string $query SQL query
254      */
255     protected function debug($query)
256     {
257         if ($this->options['debug_mode']) {
258             rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';');
259         }
260     }
261
262     /**
0d94fd 263      * Getter for error state
AM 264      *
a39859 265      * @param mixed $result Optional query result
ea08d4 266      *
154425 267      * @return string Error message
0d94fd 268      */
a39859 269     public function is_error($result = null)
0d94fd 270     {
a39859 271         if ($result !== null) {
AM 272             return $result === false ? $this->db_error_msg : null;
ea08d4 273         }
AM 274
154425 275         return $this->db_error ? $this->db_error_msg : null;
0d94fd 276     }
AM 277
278     /**
279      * Connection state checker
280      *
3d231c 281      * @return boolean True if in connected state
0d94fd 282      */
AM 283     public function is_connected()
284     {
285         return !is_object($this->dbh) ? false : $this->db_connected;
286     }
287
288     /**
289      * Is database replication configured?
3d231c 290      *
AM 291      * @return bool Returns true if dsnw != dsnr
0d94fd 292      */
AM 293     public function is_replicated()
294     {
295       return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr;
296     }
297
298     /**
c389a8 299      * Get database runtime variables
AM 300      *
3d231c 301      * @param string $varname Variable name
AM 302      * @param mixed  $default Default value if variable is not set
c389a8 303      *
AM 304      * @return mixed Variable value or default
305      */
306     public function get_variable($varname, $default = null)
307     {
308         // to be implemented by driver class
309         return $default;
310     }
311
312     /**
0d94fd 313      * Execute a SQL query
AM 314      *
3d231c 315      * @param string SQL query to execute
AM 316      * @param mixed  Values to be inserted in query
0d94fd 317      *
AM 318      * @return number  Query handle identifier
319      */
320     public function query()
321     {
322         $params = func_get_args();
323         $query = array_shift($params);
324
325         // Support one argument of type array, instead of n arguments
326         if (count($params) == 1 && is_array($params[0])) {
327             $params = $params[0];
328         }
329
330         return $this->_query($query, 0, 0, $params);
331     }
332
333     /**
334      * Execute a SQL query with limits
335      *
3d231c 336      * @param string SQL query to execute
AM 337      * @param int    Offset for LIMIT statement
338      * @param int    Number of rows for LIMIT statement
339      * @param mixed  Values to be inserted in query
0d94fd 340      *
a39859 341      * @return PDOStatement|bool Query handle or False on error
0d94fd 342      */
AM 343     public function limitquery()
344     {
345         $params  = func_get_args();
346         $query   = array_shift($params);
347         $offset  = array_shift($params);
348         $numrows = array_shift($params);
349
350         return $this->_query($query, $offset, $numrows, $params);
351     }
352
353     /**
354      * Execute a SQL query with limits
355      *
3d231c 356      * @param string $query   SQL query to execute
AM 357      * @param int    $offset  Offset for LIMIT statement
358      * @param int    $numrows Number of rows for LIMIT statement
359      * @param array  $params  Values to be inserted in query
360      *
a39859 361      * @return PDOStatement|bool Query handle or False on error
0d94fd 362      */
AM 363     protected function _query($query, $offset, $numrows, $params)
364     {
365         // Read or write ?
c389a8 366         $mode = preg_match('/^(select|show)/i', ltrim($query)) ? 'r' : 'w';
0d94fd 367
AM 368         $this->db_connect($mode);
369
370         // check connection before proceeding
371         if (!$this->is_connected()) {
a39859 372             return $this->last_result = false;
0d94fd 373         }
AM 374
375         if ($numrows || $offset) {
376             $query = $this->set_limit($query, $numrows, $offset);
377         }
378
379         $params = (array) $params;
380
381         // Because in Roundcube we mostly use queries that are
382         // executed only once, we will not use prepared queries
383         $pos = 0;
384         $idx = 0;
385
386         while ($pos = strpos($query, '?', $pos)) {
13969c 387             if ($query[$pos+1] == '?') {  // skip escaped ?
TB 388                 $pos += 2;
389             }
390             else {
391                 $val = $this->quote($params[$idx++]);
392                 unset($params[$idx-1]);
393                 $query = substr_replace($query, $val, $pos, 1);
394                 $pos += strlen($val);
395             }
0d94fd 396         }
AM 397
13969c 398         // replace escaped ? back to normal
TB 399         $query = rtrim(strtr($query, array('??' => '?')), ';');
0d94fd 400
329eae 401         $this->debug($query);
0d94fd 402
a61326 403         // destroy reference to previous result, required for SQLite driver (#1488874)
AM 404         $this->last_result = null;
405
406         // send query
0d94fd 407         $query = $this->dbh->query($query);
AM 408
409         if ($query === false) {
410             $error = $this->dbh->errorInfo();
411             $this->db_error = true;
412             $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
413
414             rcube::raise_error(array('code' => 500, 'type' => 'db',
415                 'line' => __LINE__, 'file' => __FILE__,
416                 'message' => $this->db_error_msg), true, false);
417         }
418
a39859 419         $this->last_result = $query;
AM 420
421         return $query;
0d94fd 422     }
AM 423
424     /**
425      * Get number of affected rows for the last query
426      *
a39859 427      * @param mixed $result Optional query handle
3d231c 428      *
679b37 429      * @return int Number of (matching) rows
0d94fd 430      */
a39859 431     public function affected_rows($result = null)
0d94fd 432     {
a39859 433         if ($result || ($result === null && ($result = $this->last_result))) {
0d94fd 434             return $result->rowCount();
AM 435         }
436
437         return 0;
438     }
439
440     /**
441      * Get last inserted record ID
442      *
3d231c 443      * @param string $table Table name (to find the incremented sequence)
0d94fd 444      *
3d231c 445      * @return mixed ID or false on failure
0d94fd 446      */
AM 447     public function insert_id($table = '')
448     {
449         if (!$this->db_connected || $this->db_mode == 'r') {
450             return false;
451         }
452
453         if ($table) {
454             // resolve table name
455             $table = $this->table_name($table);
456         }
457
458         $id = $this->dbh->lastInsertId($table);
459
460         return $id;
461     }
462
463     /**
464      * Get an associative array for one row
465      * If no query handle is specified, the last query will be taken as reference
466      *
a39859 467      * @param mixed $result Optional query handle
0d94fd 468      *
3d231c 469      * @return mixed Array with col values or false on failure
0d94fd 470      */
a39859 471     public function fetch_assoc($result = null)
0d94fd 472     {
AM 473         return $this->_fetch_row($result, PDO::FETCH_ASSOC);
474     }
475
476     /**
477      * Get an index array for one row
478      * If no query handle is specified, the last query will be taken as reference
479      *
a39859 480      * @param mixed $result Optional query handle
0d94fd 481      *
3d231c 482      * @return mixed Array with col values or false on failure
0d94fd 483      */
a39859 484     public function fetch_array($result = null)
0d94fd 485     {
AM 486         return $this->_fetch_row($result, PDO::FETCH_NUM);
487     }
488
489     /**
490      * Get col values for a result row
491      *
a39859 492      * @param mixed $result Optional query handle
AM 493      * @param int   $mode   Fetch mode identifier
0d94fd 494      *
3d231c 495      * @return mixed Array with col values or false on failure
0d94fd 496      */
AM 497     protected function _fetch_row($result, $mode)
498     {
a39859 499         if ($result || ($result === null && ($result = $this->last_result))) {
AM 500             return $result->fetch($mode);
0d94fd 501         }
AM 502
a39859 503         return false;
0d94fd 504     }
AM 505
506     /**
507      * Adds LIMIT,OFFSET clauses to the query
508      *
3d231c 509      * @param string $query  SQL query
AM 510      * @param int    $limit  Number of rows
511      * @param int    $offset Offset
8c2375 512      *
AM 513      * @return string SQL query
0d94fd 514      */
AM 515     protected function set_limit($query, $limit = 0, $offset = 0)
516     {
517         if ($limit) {
518             $query .= ' LIMIT ' . intval($limit);
519         }
520
521         if ($offset) {
522             $query .= ' OFFSET ' . intval($offset);
523         }
524
525         return $query;
526     }
527
528     /**
529      * Returns list of tables in a database
530      *
531      * @return array List of all tables of the current database
532      */
533     public function list_tables()
534     {
535         // get tables if not cached
536         if ($this->tables === null) {
537             $q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_NAME');
538
a39859 539             if ($q) {
AM 540                 $this->tables = $q->fetchAll(PDO::FETCH_COLUMN, 0);
0d94fd 541             }
AM 542             else {
543                 $this->tables = array();
544             }
545         }
546
547         return $this->tables;
548     }
549
550     /**
551      * Returns list of columns in database table
552      *
3d231c 553      * @param string $table Table name
0d94fd 554      *
AM 555      * @return array List of table cols
556      */
557     public function list_cols($table)
558     {
559         $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?',
560             array($table));
561
a39859 562         if ($q) {
AM 563             return $q->fetchAll(PDO::FETCH_COLUMN, 0);
0d94fd 564         }
AM 565
566         return array();
567     }
568
569     /**
570      * Formats input so it can be safely used in a query
571      *
3d231c 572      * @param mixed  $input Value to quote
AM 573      * @param string $type  Type of data
0d94fd 574      *
3d231c 575      * @return string Quoted/converted string for use in query
0d94fd 576      */
AM 577     public function quote($input, $type = null)
578     {
579         // handle int directly for better performance
580         if ($type == 'integer' || $type == 'int') {
581             return intval($input);
582         }
583
0db8d0 584         if (is_null($input)) {
TB 585             return 'NULL';
586         }
587
0d94fd 588         // create DB handle if not available
AM 589         if (!$this->dbh) {
590             $this->db_connect('r');
591         }
592
593         if ($this->dbh) {
594             $map = array(
595                 'bool'    => PDO::PARAM_BOOL,
596                 'integer' => PDO::PARAM_INT,
597             );
598             $type = isset($map[$type]) ? $map[$type] : PDO::PARAM_STR;
13969c 599             return strtr($this->dbh->quote($input, $type), array('?' => '??'));  // escape ?
0d94fd 600         }
AM 601
602         return 'NULL';
603     }
604
605     /**
606      * Quotes a string so it can be safely used as a table or column name
607      *
3d231c 608      * @param string $str Value to quote
0d94fd 609      *
3d231c 610      * @return string Quoted string for use in query
AM 611      * @deprecated    Replaced by rcube_db::quote_identifier
612      * @see           rcube_db::quote_identifier
0d94fd 613      */
AM 614     public function quoteIdentifier($str)
615     {
616         return $this->quote_identifier($str);
617     }
618
619     /**
620      * Quotes a string so it can be safely used as a table or column name
621      *
3d231c 622      * @param string $str Value to quote
0d94fd 623      *
3d231c 624      * @return string Quoted string for use in query
0d94fd 625      */
AM 626     public function quote_identifier($str)
627     {
628         $start = $this->options['identifier_start'];
629         $end   = $this->options['identifier_end'];
630         $name  = array();
631
632         foreach (explode('.', $str) as $elem) {
633             $elem = str_replace(array($start, $end), '', $elem);
634             $name[] = $start . $elem . $end;
635         }
636
637         return  implode($name, '.');
638     }
639
640     /**
641      * Return SQL function for current time and date
642      *
643      * @return string SQL function to use in query
644      */
645     public function now()
646     {
647         return "now()";
648     }
649
650     /**
651      * Return list of elements for use with SQL's IN clause
652      *
3d231c 653      * @param array  $arr  Input array
AM 654      * @param string $type Type of data
0d94fd 655      *
AM 656      * @return string Comma-separated list of quoted values for use in query
657      */
658     public function array2list($arr, $type = null)
659     {
660         if (!is_array($arr)) {
661             return $this->quote($arr, $type);
662         }
663
664         foreach ($arr as $idx => $item) {
665             $arr[$idx] = $this->quote($item, $type);
666         }
667
668         return implode(',', $arr);
669     }
670
671     /**
672      * Return SQL statement to convert a field value into a unix timestamp
673      *
674      * This method is deprecated and should not be used anymore due to limitations
675      * of timestamp functions in Mysql (year 2038 problem)
676      *
3d231c 677      * @param string $field Field name
0d94fd 678      *
AM 679      * @return string  SQL statement to use in query
680      * @deprecated
681      */
682     public function unixtimestamp($field)
683     {
684         return "UNIX_TIMESTAMP($field)";
685     }
686
687     /**
688      * Return SQL statement to convert from a unix timestamp
689      *
3d231c 690      * @param int $timestamp Unix timestamp
0d94fd 691      *
3d231c 692      * @return string Date string in db-specific format
0d94fd 693      */
AM 694     public function fromunixtime($timestamp)
695     {
696         return date("'Y-m-d H:i:s'", $timestamp);
697     }
698
699     /**
700      * Return SQL statement for case insensitive LIKE
701      *
3d231c 702      * @param string $column Field name
AM 703      * @param string $value  Search value
0d94fd 704      *
3d231c 705      * @return string SQL statement to use in query
0d94fd 706      */
AM 707     public function ilike($column, $value)
708     {
709         return $this->quote_identifier($column).' LIKE '.$this->quote($value);
710     }
711
712     /**
713      * Abstract SQL statement for value concatenation
714      *
715      * @return string SQL statement to be used in query
716      */
717     public function concat(/* col1, col2, ... */)
718     {
719         $args = func_get_args();
720         if (is_array($args[0])) {
721             $args = $args[0];
722         }
723
724         return '(' . join(' || ', $args) . ')';
725     }
726
727     /**
728      * Encodes non-UTF-8 characters in string/array/object (recursive)
729      *
3d231c 730      * @param mixed $input Data to fix
0d94fd 731      *
3d231c 732      * @return mixed Properly UTF-8 encoded data
0d94fd 733      */
AM 734     public static function encode($input)
735     {
736         if (is_object($input)) {
737             foreach (get_object_vars($input) as $idx => $value) {
738                 $input->$idx = self::encode($value);
739             }
740             return $input;
741         }
742         else if (is_array($input)) {
743             foreach ($input as $idx => $value) {
744                 $input[$idx] = self::encode($value);
745             }
746             return $input;
747         }
748
749         return utf8_encode($input);
750     }
751
752     /**
753      * Decodes encoded UTF-8 string/object/array (recursive)
754      *
3d231c 755      * @param mixed $input Input data
0d94fd 756      *
3d231c 757      * @return mixed Decoded data
0d94fd 758      */
AM 759     public static function decode($input)
760     {
761         if (is_object($input)) {
762             foreach (get_object_vars($input) as $idx => $value) {
763                 $input->$idx = self::decode($value);
764             }
765             return $input;
766         }
767         else if (is_array($input)) {
768             foreach ($input as $idx => $value) {
769                 $input[$idx] = self::decode($value);
770             }
771             return $input;
772         }
773
774         return utf8_decode($input);
775     }
776
777     /**
778      * Return correct name for a specific database table
779      *
780      * @param string $table Table name
781      *
782      * @return string Translated table name
783      */
784     public function table_name($table)
785     {
786         $rcube = rcube::get_instance();
787
788         // return table name if configured
789         $config_key = 'db_table_'.$table;
790
791         if ($name = $rcube->config->get($config_key)) {
792             return $name;
793         }
794
795         return $table;
796     }
797
798     /**
799      * MDB2 DSN string parser
3e386e 800      *
AM 801      * @param string $sequence Secuence name
802      *
803      * @return array DSN parameters
0d94fd 804      */
AM 805     public static function parse_dsn($dsn)
806     {
807         if (empty($dsn)) {
808             return null;
809         }
810
811         // Find phptype and dbsyntax
812         if (($pos = strpos($dsn, '://')) !== false) {
813             $str = substr($dsn, 0, $pos);
814             $dsn = substr($dsn, $pos + 3);
3d231c 815         }
AM 816         else {
0d94fd 817             $str = $dsn;
AM 818             $dsn = null;
819         }
820
821         // Get phptype and dbsyntax
822         // $str => phptype(dbsyntax)
823         if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
824             $parsed['phptype']  = $arr[1];
825             $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
3d231c 826         }
AM 827         else {
0d94fd 828             $parsed['phptype']  = $str;
AM 829             $parsed['dbsyntax'] = $str;
830         }
831
832         if (empty($dsn)) {
833             return $parsed;
834         }
835
836         // Get (if found): username and password
837         // $dsn => username:password@protocol+hostspec/database
838         if (($at = strrpos($dsn,'@')) !== false) {
839             $str = substr($dsn, 0, $at);
840             $dsn = substr($dsn, $at + 1);
841             if (($pos = strpos($str, ':')) !== false) {
842                 $parsed['username'] = rawurldecode(substr($str, 0, $pos));
843                 $parsed['password'] = rawurldecode(substr($str, $pos + 1));
3d231c 844             }
AM 845             else {
0d94fd 846                 $parsed['username'] = rawurldecode($str);
AM 847             }
848         }
849
850         // Find protocol and hostspec
851
852         // $dsn => proto(proto_opts)/database
853         if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
854             $proto       = $match[1];
855             $proto_opts  = $match[2] ? $match[2] : false;
856             $dsn         = $match[3];
857         }
858         // $dsn => protocol+hostspec/database (old format)
859         else {
860             if (strpos($dsn, '+') !== false) {
861                 list($proto, $dsn) = explode('+', $dsn, 2);
862             }
863             if (   strpos($dsn, '//') === 0
864                 && strpos($dsn, '/', 2) !== false
865                 && $parsed['phptype'] == 'oci8'
866             ) {
867                 //oracle's "Easy Connect" syntax:
868                 //"username/password@[//]host[:port][/service_name]"
869                 //e.g. "scott/tiger@//mymachine:1521/oracle"
870                 $proto_opts = $dsn;
871                 $pos = strrpos($proto_opts, '/');
872                 $dsn = substr($proto_opts, $pos + 1);
873                 $proto_opts = substr($proto_opts, 0, $pos);
3d231c 874             }
AM 875             else if (strpos($dsn, '/') !== false) {
0d94fd 876                 list($proto_opts, $dsn) = explode('/', $dsn, 2);
3d231c 877             }
AM 878             else {
0d94fd 879                 $proto_opts = $dsn;
AM 880                 $dsn = null;
881             }
882         }
883
884         // process the different protocol options
885         $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp';
886         $proto_opts = rawurldecode($proto_opts);
887         if (strpos($proto_opts, ':') !== false) {
888             list($proto_opts, $parsed['port']) = explode(':', $proto_opts);
889         }
890         if ($parsed['protocol'] == 'tcp') {
891             $parsed['hostspec'] = $proto_opts;
892         }
893         else if ($parsed['protocol'] == 'unix') {
894             $parsed['socket'] = $proto_opts;
895         }
896
897         // Get dabase if any
898         // $dsn => database
899         if ($dsn) {
900             // /database
901             if (($pos = strpos($dsn, '?')) === false) {
902                 $parsed['database'] = rawurldecode($dsn);
903             // /database?param1=value1&param2=value2
904             }
905             else {
906                 $parsed['database'] = rawurldecode(substr($dsn, 0, $pos));
907                 $dsn = substr($dsn, $pos + 1);
908                 if (strpos($dsn, '&') !== false) {
909                     $opts = explode('&', $dsn);
3d231c 910                 }
AM 911                 else { // database?param1=value1
0d94fd 912                     $opts = array($dsn);
AM 913                 }
914                 foreach ($opts as $opt) {
915                     list($key, $value) = explode('=', $opt);
916                     if (!array_key_exists($key, $parsed) || false === $parsed[$key]) {
917                         // don't allow params overwrite
918                         $parsed[$key] = rawurldecode($value);
919                     }
920                 }
921             }
922         }
923
924         return $parsed;
925     }
926
927     /**
3e386e 928      * Returns PDO DSN string from DSN array
AM 929      *
3d231c 930      * @param array $dsn DSN parameters
3e386e 931      *
AM 932      * @return string DSN string
0d94fd 933      */
AM 934     protected function dsn_string($dsn)
935     {
936         $params = array();
937         $result = $dsn['phptype'] . ':';
938
939         if ($dsn['hostspec']) {
940             $params[] = 'host=' . $dsn['hostspec'];
941         }
942
943         if ($dsn['port']) {
944             $params[] = 'port=' . $dsn['port'];
945         }
946
947         if ($dsn['database']) {
948             $params[] = 'dbname=' . $dsn['database'];
949         }
950
951         if (!empty($params)) {
952             $result .= implode(';', $params);
953         }
954
955         return $result;
956     }
957
958     /**
3e386e 959      * Returns driver-specific connection options
AM 960      *
3d231c 961      * @param array $dsn DSN parameters
3e386e 962      *
AM 963      * @return array Connection options
0d94fd 964      */
AM 965     protected function dsn_options($dsn)
966     {
967         $result = array();
968
969         return $result;
970     }
971 }