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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  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 (count($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 overriden 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 ($type == 'ident') {
  280. return $this->quote_identifier($input);
  281. }
  282. switch ($type) {
  283. case 'bool':
  284. case 'integer':
  285. return intval($input);
  286. default:
  287. return "'" . strtr($input, array(
  288. '?' => '??',
  289. "'" => "''",
  290. rcube_db::DEFAULT_QUOTE => rcube_db::DEFAULT_QUOTE . rcube_db::DEFAULT_QUOTE
  291. )) . "'";
  292. }
  293. }
  294. /**
  295. * Return correct name for a specific database sequence
  296. *
  297. * @param string $table Table name
  298. *
  299. * @return string Translated sequence name
  300. */
  301. protected function sequence_name($table)
  302. {
  303. // Note: we support only one sequence per table
  304. // Note: The sequence name must be <table_name>_seq
  305. $sequence = $table . '_seq';
  306. // modify sequence name if prefix is configured
  307. if ($prefix = $this->options['table_prefix']) {
  308. return $prefix . $sequence;
  309. }
  310. return $sequence;
  311. }
  312. /**
  313. * Return SQL statement for case insensitive LIKE
  314. *
  315. * @param string $column Field name
  316. * @param string $value Search value
  317. *
  318. * @return string SQL statement to use in query
  319. */
  320. public function ilike($column, $value)
  321. {
  322. return 'UPPER(' . $this->quote_identifier($column) . ') LIKE UPPER(' . $this->quote($value) . ')';
  323. }
  324. /**
  325. * Return SQL function for current time and date
  326. *
  327. * @param int $interval Optional interval (in seconds) to add/subtract
  328. *
  329. * @return string SQL function to use in query
  330. */
  331. public function now($interval = 0)
  332. {
  333. if ($interval) {
  334. $interval = intval($interval);
  335. return "current_timestamp + INTERVAL '$interval' SECOND";
  336. }
  337. return "current_timestamp";
  338. }
  339. /**
  340. * Return SQL statement to convert a field value into a unix timestamp
  341. *
  342. * @param string $field Field name
  343. *
  344. * @return string SQL statement to use in query
  345. * @deprecated
  346. */
  347. public function unixtimestamp($field)
  348. {
  349. return "(($field - to_date('1970-01-01','YYYY-MM-DD')) * 60 * 60 * 24)";
  350. }
  351. /**
  352. * Adds TOP (LIMIT,OFFSET) clause to the query
  353. *
  354. * @param string $query SQL query
  355. * @param int $limit Number of rows
  356. * @param int $offset Offset
  357. *
  358. * @return string SQL query
  359. */
  360. protected function set_limit($query, $limit = 0, $offset = 0)
  361. {
  362. $limit = intval($limit);
  363. $offset = intval($offset);
  364. $end = $offset + $limit;
  365. // @TODO: Oracle 12g has better OFFSET support
  366. if (!$offset) {
  367. $query = "SELECT * FROM ($query) a WHERE rownum <= $end";
  368. }
  369. else {
  370. $query = "SELECT * FROM (SELECT a.*, rownum as rn FROM ($query) a WHERE rownum <= $end) b WHERE rn > $offset";
  371. }
  372. return $query;
  373. }
  374. /**
  375. * Parse SQL file and fix table names according to table prefix
  376. */
  377. protected function fix_table_names($sql)
  378. {
  379. if (!$this->options['table_prefix']) {
  380. return $sql;
  381. }
  382. $sql = parent::fix_table_names($sql);
  383. // replace sequence names, and other Oracle-specific commands
  384. $sql = preg_replace_callback('/((SEQUENCE ["]?)([^" \r\n]+)/',
  385. array($this, 'fix_table_names_callback'),
  386. $sql
  387. );
  388. $sql = preg_replace_callback(
  389. '/([ \r\n]+["]?)([^"\' \r\n\.]+)(["]?\.nextval)/',
  390. array($this, 'fix_table_names_seq_callback'),
  391. $sql
  392. );
  393. return $sql;
  394. }
  395. /**
  396. * Preg_replace callback for fix_table_names()
  397. */
  398. protected function fix_table_names_seq_callback($matches)
  399. {
  400. return $matches[1] . $this->options['table_prefix'] . $matches[2] . $matches[3];
  401. }
  402. /**
  403. * Returns connection options from DSN array
  404. */
  405. protected function dsn_options($dsn)
  406. {
  407. $params = array();
  408. if ($dsn['hostspec']) {
  409. $host = $dsn['hostspec'];
  410. if ($dsn['port']) {
  411. $host .= ':' . $dsn['port'];
  412. }
  413. $params['database'] = $host . '/' . $dsn['database'];
  414. }
  415. $params['charset'] = 'UTF8';
  416. return $params;
  417. }
  418. /**
  419. * Execute the given SQL script
  420. *
  421. * @param string SQL queries to execute
  422. *
  423. * @return boolen True on success, False on error
  424. */
  425. public function exec_script($sql)
  426. {
  427. $sql = $this->fix_table_names($sql);
  428. $buff = '';
  429. $body = false;
  430. foreach (explode("\n", $sql) as $line) {
  431. $tok = strtolower(trim($line));
  432. if (preg_match('/^--/', $line) || $tok == '' || $tok == '/') {
  433. continue;
  434. }
  435. $buff .= $line . "\n";
  436. // detect PL/SQL function bodies, don't break on semicolon
  437. if ($body && $tok == 'end;') {
  438. $body = false;
  439. }
  440. else if (!$body && $tok == 'begin') {
  441. $body = true;
  442. }
  443. if (!$body && substr($tok, -1) == ';') {
  444. $this->query($buff);
  445. $buff = '';
  446. if ($this->db_error) {
  447. break;
  448. }
  449. }
  450. }
  451. return !$this->db_error;
  452. }
  453. /**
  454. * Start transaction
  455. *
  456. * @return bool True on success, False on failure
  457. */
  458. public function startTransaction()
  459. {
  460. $this->db_connect('w', true);
  461. // check connection before proceeding
  462. if (!$this->is_connected()) {
  463. return $this->last_result = false;
  464. }
  465. $this->debug('BEGIN TRANSACTION');
  466. return $this->last_result = $this->in_transaction = true;
  467. }
  468. /**
  469. * Commit transaction
  470. *
  471. * @return bool True on success, False on failure
  472. */
  473. public function endTransaction()
  474. {
  475. $this->db_connect('w', true);
  476. // check connection before proceeding
  477. if (!$this->is_connected()) {
  478. return $this->last_result = false;
  479. }
  480. $this->debug('COMMIT TRANSACTION');
  481. if ($result = @oci_commit($this->dbh)) {
  482. $this->in_transaction = true;
  483. }
  484. else {
  485. $this->handle_error('COMMIT');
  486. }
  487. return $this->last_result = $result;
  488. }
  489. /**
  490. * Rollback transaction
  491. *
  492. * @return bool True on success, False on failure
  493. */
  494. public function rollbackTransaction()
  495. {
  496. $this->db_connect('w', true);
  497. // check connection before proceeding
  498. if (!$this->is_connected()) {
  499. return $this->last_result = false;
  500. }
  501. $this->debug('ROLLBACK TRANSACTION');
  502. if (@oci_rollback($this->dbh)) {
  503. $this->in_transaction = false;
  504. }
  505. else {
  506. $this->handle_error('ROLLBACK');
  507. }
  508. return $this->last_result = $this->dbh->rollBack();
  509. }
  510. /**
  511. * Terminate database connection.
  512. */
  513. public function closeConnection()
  514. {
  515. // release statement and close connection(s)
  516. $this->last_result = null;
  517. foreach ($this->dbhs as $dbh) {
  518. oci_close($dbh);
  519. }
  520. parent::closeConnection();
  521. }
  522. }