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