PageRenderTime 62ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/program/lib/Roundcube/rcube_db.php

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