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