You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

rcube_db_oracle.php 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2011-2014, Kolab Systems AG |
  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 database functions |
  13. | for Oracle database using OCI8 extension |
  14. +-----------------------------------------------------------------------+
  15. | Author: Aleksander Machniak <machniak@kolabsys.com> |
  16. +-----------------------------------------------------------------------+
  17. */
  18. /**
  19. * Database independent query interface
  20. *
  21. * @package Framework
  22. * @subpackage Database
  23. */
  24. class rcube_db_oracle extends rcube_db
  25. {
  26. public $db_provider = 'oracle';
  27. /**
  28. * Create connection instance
  29. */
  30. protected function conn_create($dsn)
  31. {
  32. // Get database specific connection options
  33. $dsn_options = $this->dsn_options($dsn);
  34. $function = $this->db_pconn ? 'oci_pconnect' : 'oci_connect';
  35. if (!function_exists($function)) {
  36. $this->db_error = true;
  37. $this->db_error_msg = 'OCI8 extension not loaded. See http://php.net/manual/en/book.oci8.php';
  38. rcube::raise_error(array('code' => 500, 'type' => 'db',
  39. 'line' => __LINE__, 'file' => __FILE__,
  40. 'message' => $this->db_error_msg), true, false);
  41. return;
  42. }
  43. // connect
  44. $dbh = @$function($dsn['username'], $dsn['password'], $dsn_options['database'], $dsn_options['charset']);
  45. if (!$dbh) {
  46. $error = oci_error();
  47. $this->db_error = true;
  48. $this->db_error_msg = $error['message'];
  49. rcube::raise_error(array('code' => 500, 'type' => 'db',
  50. 'line' => __LINE__, 'file' => __FILE__,
  51. 'message' => $this->db_error_msg), true, false);
  52. return;
  53. }
  54. // configure session
  55. $this->conn_configure($dsn, $dbh);
  56. return $dbh;
  57. }
  58. /**
  59. * Driver-specific configuration of database connection
  60. *
  61. * @param array $dsn DSN for DB connections
  62. * @param PDO $dbh Connection handler
  63. */
  64. protected function conn_configure($dsn, $dbh)
  65. {
  66. $init_queries = array(
  67. "ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'",
  68. "ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'",
  69. );
  70. foreach ($init_queries as $query) {
  71. $stmt = oci_parse($dbh, $query);
  72. oci_execute($stmt);
  73. }
  74. }
  75. /**
  76. * Connection state checker
  77. *
  78. * @return boolean True if in connected state
  79. */
  80. public function is_connected()
  81. {
  82. return empty($this->dbh) ? false : $this->db_connected;
  83. }
  84. /**
  85. * Execute a SQL query with limits
  86. *
  87. * @param string $query SQL query to execute
  88. * @param int $offset Offset for LIMIT statement
  89. * @param int $numrows Number of rows for LIMIT statement
  90. * @param array $params Values to be inserted in query
  91. *
  92. * @return PDOStatement|bool Query handle or False on error
  93. */
  94. protected function _query($query, $offset, $numrows, $params)
  95. {
  96. $query = ltrim($query);
  97. $this->db_connect($this->dsn_select($query), true);
  98. // check connection before proceeding
  99. if (!$this->is_connected()) {
  100. return $this->last_result = false;
  101. }
  102. if ($numrows || $offset) {
  103. $query = $this->set_limit($query, $numrows, $offset);
  104. }
  105. // replace self::DEFAULT_QUOTE with driver-specific quoting
  106. $query = $this->query_parse($query);
  107. // Because in Roundcube we mostly use queries that are
  108. // executed only once, we will not use prepared queries
  109. $pos = 0;
  110. $idx = 0;
  111. $args = array();
  112. if (!empty($params)) {
  113. while ($pos = strpos($query, '?', $pos)) {
  114. if ($query[$pos+1] == '?') { // skip escaped '?'
  115. $pos += 2;
  116. }
  117. else {
  118. $val = $this->quote($params[$idx++]);
  119. // long strings are not allowed inline, need to be parametrized
  120. if (strlen($val) > 4000) {
  121. $key = ':param' . (count($args) + 1);
  122. $args[$key] = $params[$idx-1];
  123. $val = $key;
  124. }
  125. unset($params[$idx-1]);
  126. $query = substr_replace($query, $val, $pos, 1);
  127. $pos += strlen($val);
  128. }
  129. }
  130. }
  131. $query = rtrim($query, " \t\n\r\0\x0B;");
  132. // replace escaped '?' and quotes back to normal, see self::quote()
  133. $query = str_replace(
  134. array('??', self::DEFAULT_QUOTE.self::DEFAULT_QUOTE),
  135. array('?', self::DEFAULT_QUOTE),
  136. $query
  137. );
  138. // log query
  139. $this->debug($query);
  140. // destroy reference to previous result
  141. $this->last_result = null;
  142. $this->db_error_msg = null;
  143. // prepare query
  144. $result = @oci_parse($this->dbh, $query);
  145. $mode = $this->in_transaction ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS;
  146. if ($result) {
  147. foreach (array_keys($args) as $param) {
  148. oci_bind_by_name($result, $param, $args[$param], -1, SQLT_LNG);
  149. }
  150. }
  151. // execute query
  152. if (!$result || !@oci_execute($result, $mode)) {
  153. $result = $this->handle_error($query, $result);
  154. }
  155. return $this->last_result = $result;
  156. }
  157. /**
  158. * Helper method to handle DB errors.
  159. * This by default logs the error but could be overridden by a driver implementation
  160. *
  161. * @param string Query that triggered the error
  162. * @return mixed Result to be stored and returned
  163. */
  164. protected function handle_error($query, $result = null)
  165. {
  166. $error = oci_error(is_resource($result) ? $result : $this->dbh);
  167. // @TODO: Find error codes for key errors
  168. if (empty($this->options['ignore_key_errors']) || !in_array($error['code'], array('23000', '23505'))) {
  169. $this->db_error = true;
  170. $this->db_error_msg = sprintf('[%s] %s', $error['code'], $error['message']);
  171. rcube::raise_error(array('code' => 500, 'type' => 'db',
  172. 'line' => __LINE__, 'file' => __FILE__,
  173. 'message' => $this->db_error_msg . " (SQL Query: $query)"
  174. ), true, false);
  175. }
  176. return false;
  177. }
  178. /**
  179. * Get last inserted record ID
  180. *
  181. * @param string $table Table name (to find the incremented sequence)
  182. *
  183. * @return mixed ID or false on failure
  184. */
  185. public function insert_id($table = null)
  186. {
  187. if (!$this->db_connected || $this->db_mode == 'r' || empty($table)) {
  188. return false;
  189. }
  190. $sequence = $this->quote_identifier($this->sequence_name($table));
  191. $result = $this->query("SELECT $sequence.currval FROM dual");
  192. $result = $this->fetch_array($result);
  193. return $result[0] ?: false;
  194. }
  195. /**
  196. * Get number of affected rows for the last query
  197. *
  198. * @param mixed $result Optional query handle
  199. *
  200. * @return int Number of (matching) rows
  201. */
  202. public function affected_rows($result = null)
  203. {
  204. if ($result || ($result === null && ($result = $this->last_result))) {
  205. return oci_num_rows($result);
  206. }
  207. return 0;
  208. }
  209. /**
  210. * Get number of rows for a SQL query
  211. * If no query handle is specified, the last query will be taken as reference
  212. *
  213. * @param mixed $result Optional query handle
  214. * @return mixed Number of rows or false on failure
  215. * @deprecated This method shows very poor performance and should be avoided.
  216. */
  217. public function num_rows($result = null)
  218. {
  219. // not implemented
  220. return false;
  221. }
  222. /**
  223. * Get an associative array for one row
  224. * If no query handle is specified, the last query will be taken as reference
  225. *
  226. * @param mixed $result Optional query handle
  227. *
  228. * @return mixed Array with col values or false on failure
  229. */
  230. public function fetch_assoc($result = null)
  231. {
  232. return $this->_fetch_row($result, OCI_ASSOC);
  233. }
  234. /**
  235. * Get an index array for one row
  236. * If no query handle is specified, the last query will be taken as reference
  237. *
  238. * @param mixed $result Optional query handle
  239. *
  240. * @return mixed Array with col values or false on failure
  241. */
  242. public function fetch_array($result = null)
  243. {
  244. return $this->_fetch_row($result, OCI_NUM);
  245. }
  246. /**
  247. * Get col values for a result row
  248. *
  249. * @param mixed $result Optional query handle
  250. * @param int $mode Fetch mode identifier
  251. *
  252. * @return mixed Array with col values or false on failure
  253. */
  254. protected function _fetch_row($result, $mode)
  255. {
  256. if ($result || ($result === null && ($result = $this->last_result))) {
  257. return oci_fetch_array($result, $mode + OCI_RETURN_NULLS + OCI_RETURN_LOBS);
  258. }
  259. return false;
  260. }
  261. /**
  262. * Formats input so it can be safely used in a query
  263. * PDO_OCI does not implement quote() method
  264. *
  265. * @param mixed $input Value to quote
  266. * @param string $type Type of data (integer, bool, ident)
  267. *
  268. * @return string Quoted/converted string for use in query
  269. */
  270. public function quote($input, $type = null)
  271. {
  272. // handle int directly for better performance
  273. if ($type == 'integer' || $type == 'int') {
  274. return intval($input);
  275. }
  276. if (is_null($input)) {
  277. return 'NULL';
  278. }
  279. if ($input instanceof DateTime) {
  280. return $this->quote($input->format($this->options['datetime_format']));
  281. }
  282. if ($type == 'ident') {
  283. return $this->quote_identifier($input);
  284. }
  285. switch ($type) {
  286. case 'bool':
  287. case 'integer':
  288. return intval($input);
  289. default:
  290. return "'" . strtr($input, array(
  291. '?' => '??',
  292. "'" => "''",
  293. rcube_db::DEFAULT_QUOTE => rcube_db::DEFAULT_QUOTE . rcube_db::DEFAULT_QUOTE
  294. )) . "'";
  295. }
  296. }
  297. /**
  298. * Return correct name for a specific database sequence
  299. *
  300. * @param string $table Table name
  301. *
  302. * @return string Translated sequence name
  303. */
  304. protected function sequence_name($table)
  305. {
  306. // Note: we support only one sequence per table
  307. // Note: The sequence name must be <table_name>_seq
  308. $sequence = $table . '_seq';
  309. // modify sequence name if prefix is configured
  310. if ($prefix = $this->options['table_prefix']) {
  311. return $prefix . $sequence;
  312. }
  313. return $sequence;
  314. }
  315. /**
  316. * Return SQL statement for case insensitive LIKE
  317. *
  318. * @param string $column Field name
  319. * @param string $value Search value
  320. *
  321. * @return string SQL statement to use in query
  322. */
  323. public function ilike($column, $value)
  324. {
  325. return 'UPPER(' . $this->quote_identifier($column) . ') LIKE UPPER(' . $this->quote($value) . ')';
  326. }
  327. /**
  328. * Return SQL function for current time and date
  329. *
  330. * @param int $interval Optional interval (in seconds) to add/subtract
  331. *
  332. * @return string SQL function to use in query
  333. */
  334. public function now($interval = 0)
  335. {
  336. if ($interval) {
  337. $interval = intval($interval);
  338. return "current_timestamp + INTERVAL '$interval' SECOND";
  339. }
  340. return "current_timestamp";
  341. }
  342. /**
  343. * Return SQL statement to convert a field value into a unix timestamp
  344. *
  345. * @param string $field Field name
  346. *
  347. * @return string SQL statement to use in query
  348. * @deprecated
  349. */
  350. public function unixtimestamp($field)
  351. {
  352. return "(($field - to_date('1970-01-01','YYYY-MM-DD')) * 60 * 60 * 24)";
  353. }
  354. /**
  355. * Adds TOP (LIMIT,OFFSET) clause to the query
  356. *
  357. * @param string $query SQL query
  358. * @param int $limit Number of rows
  359. * @param int $offset Offset
  360. *
  361. * @return string SQL query
  362. */
  363. protected function set_limit($query, $limit = 0, $offset = 0)
  364. {
  365. $limit = intval($limit);
  366. $offset = intval($offset);
  367. $end = $offset + $limit;
  368. // @TODO: Oracle 12g has better OFFSET support
  369. if (!$offset) {
  370. $query = "SELECT * FROM ($query) a WHERE rownum <= $end";
  371. }
  372. else {
  373. $query = "SELECT * FROM (SELECT a.*, rownum as rn FROM ($query) a WHERE rownum <= $end) b WHERE rn > $offset";
  374. }
  375. return $query;
  376. }
  377. /**
  378. * Parse SQL file and fix table names according to table prefix
  379. */
  380. protected function fix_table_names($sql)
  381. {
  382. if (!$this->options['table_prefix']) {
  383. return $sql;
  384. }
  385. $sql = parent::fix_table_names($sql);
  386. // replace sequence names, and other Oracle-specific commands
  387. $sql = preg_replace_callback('/((SEQUENCE ["]?)([^" \r\n]+)/',
  388. array($this, 'fix_table_names_callback'),
  389. $sql
  390. );
  391. $sql = preg_replace_callback(
  392. '/([ \r\n]+["]?)([^"\' \r\n\.]+)(["]?\.nextval)/',
  393. array($this, 'fix_table_names_seq_callback'),
  394. $sql
  395. );
  396. return $sql;
  397. }
  398. /**
  399. * Preg_replace callback for fix_table_names()
  400. */
  401. protected function fix_table_names_seq_callback($matches)
  402. {
  403. return $matches[1] . $this->options['table_prefix'] . $matches[2] . $matches[3];
  404. }
  405. /**
  406. * Returns connection options from DSN array
  407. */
  408. protected function dsn_options($dsn)
  409. {
  410. $params = array();
  411. if ($dsn['hostspec']) {
  412. $host = $dsn['hostspec'];
  413. if ($dsn['port']) {
  414. $host .= ':' . $dsn['port'];
  415. }
  416. $params['database'] = $host . '/' . $dsn['database'];
  417. }
  418. $params['charset'] = 'UTF8';
  419. return $params;
  420. }
  421. /**
  422. * Execute the given SQL script
  423. *
  424. * @param string $sql SQL queries to execute
  425. *
  426. * @return boolen True on success, False on error
  427. */
  428. public function exec_script($sql)
  429. {
  430. $sql = $this->fix_table_names($sql);
  431. $buff = '';
  432. $body = false;
  433. foreach (explode("\n", $sql) as $line) {
  434. $tok = strtolower(trim($line));
  435. if (preg_match('/^--/', $line) || $tok == '' || $tok == '/') {
  436. continue;
  437. }
  438. $buff .= $line . "\n";
  439. // detect PL/SQL function bodies, don't break on semicolon
  440. if ($body && $tok == 'end;') {
  441. $body = false;
  442. }
  443. else if (!$body && $tok == 'begin') {
  444. $body = true;
  445. }
  446. if (!$body && substr($tok, -1) == ';') {
  447. $this->query($buff);
  448. $buff = '';
  449. if ($this->db_error) {
  450. break;
  451. }
  452. }
  453. }
  454. return !$this->db_error;
  455. }
  456. /**
  457. * Start transaction
  458. *
  459. * @return bool True on success, False on failure
  460. */
  461. public function startTransaction()
  462. {
  463. $this->db_connect('w', true);
  464. // check connection before proceeding
  465. if (!$this->is_connected()) {
  466. return $this->last_result = false;
  467. }
  468. $this->debug('BEGIN TRANSACTION');
  469. return $this->last_result = $this->in_transaction = true;
  470. }
  471. /**
  472. * Commit transaction
  473. *
  474. * @return bool True on success, False on failure
  475. */
  476. public function endTransaction()
  477. {
  478. $this->db_connect('w', true);
  479. // check connection before proceeding
  480. if (!$this->is_connected()) {
  481. return $this->last_result = false;
  482. }
  483. $this->debug('COMMIT TRANSACTION');
  484. if ($result = @oci_commit($this->dbh)) {
  485. $this->in_transaction = true;
  486. }
  487. else {
  488. $this->handle_error('COMMIT');
  489. }
  490. return $this->last_result = $result;
  491. }
  492. /**
  493. * Rollback transaction
  494. *
  495. * @return bool True on success, False on failure
  496. */
  497. public function rollbackTransaction()
  498. {
  499. $this->db_connect('w', true);
  500. // check connection before proceeding
  501. if (!$this->is_connected()) {
  502. return $this->last_result = false;
  503. }
  504. $this->debug('ROLLBACK TRANSACTION');
  505. if (@oci_rollback($this->dbh)) {
  506. $this->in_transaction = false;
  507. }
  508. else {
  509. $this->handle_error('ROLLBACK');
  510. }
  511. return $this->last_result = $this->dbh->rollBack();
  512. }
  513. /**
  514. * Terminate database connection.
  515. */
  516. public function closeConnection()
  517. {
  518. // release statement and close connection(s)
  519. $this->last_result = null;
  520. foreach ($this->dbhs as $dbh) {
  521. oci_close($dbh);
  522. }
  523. parent::closeConnection();
  524. }
  525. }