PageRenderTime 43ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/program/include/rcube_db.php

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