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