From d7c91c14f8ebacc6fde60b9a6cb16cab523102f1 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Wed, 17 Sep 2014 19:58:54 +0200 Subject: Add Oracle driver which uses OCI8 extension - the one that supports CLOB columns --- program/lib/Roundcube/rcube_db.php | 36 ++- program/lib/Roundcube/rcube_db_oci8.php | 462 ++++++++++++++++++++++++++++++++ 2 files changed, 489 insertions(+), 9 deletions(-) create mode 100644 program/lib/Roundcube/rcube_db_oci8.php diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php index b12c99d0e..72cad0119 100644 --- a/program/lib/Roundcube/rcube_db.php +++ b/program/lib/Roundcube/rcube_db.php @@ -130,6 +130,20 @@ class rcube_db return $this->dbh; } + // connect to database + if ($dbh = $this->conn_create($dsn)) { + $this->dbh = $dbh; + $this->dbhs[$mode] = $dbh; + $this->db_mode = $mode; + $this->db_connected = true; + } + } + + /** + * Create PDO connection + */ + protected function conn_create($dsn) + { // Get database specific connection options $dsn_string = $this->dsn_string($dsn); $dsn_options = $this->dsn_options($dsn); @@ -151,6 +165,8 @@ class rcube_db // don't throw exceptions or warnings $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + + $this->conn_configure($dsn, $dbh); } catch (Exception $e) { $this->db_error = true; @@ -163,11 +179,7 @@ class rcube_db return null; } - $this->dbh = $dbh; - $this->dbhs[$mode] = $dbh; - $this->db_mode = $mode; - $this->db_connected = true; - $this->conn_configure($dsn, $dbh); + return $dbh; } /** @@ -442,8 +454,16 @@ class rcube_db // log query $this->debug($query); + return $this->query_execute($query); + } + + /** + * Query execution + */ + protected function query_execute($query) + { // destroy reference to previous result, required for SQLite driver (#1488874) - $this->last_result = null; + $this->last_result = null; $this->db_error_msg = null; // send query @@ -453,9 +473,7 @@ class rcube_db $result = $this->handle_error($query); } - $this->last_result = $result; - - return $result; + return $this->last_result = $result; } /** diff --git a/program/lib/Roundcube/rcube_db_oci8.php b/program/lib/Roundcube/rcube_db_oci8.php new file mode 100644 index 000000000..b9f63f38a --- /dev/null +++ b/program/lib/Roundcube/rcube_db_oci8.php @@ -0,0 +1,462 @@ + | + +-----------------------------------------------------------------------+ +*/ + +/** + * Database independent query interface + * + * @package Framework + * @subpackage Database + */ +class rcube_db_oci8 extends rcube_db +{ + public $db_provider = 'oracle'; + + + /** + * Create connection instance + */ + protected function conn_create($dsn) + { + // Get database specific connection options + $dsn_options = $this->dsn_options($dsn); + + $function = $this->db_pconn ? 'oci_pconnect' : 'oci_connect'; + + if (!function_exists($function)) { + $this->db_error = true; + $this->db_error_msg = 'OCI8 extension not loaded. See http://php.net/manual/en/book.oci8.php'; + + rcube::raise_error(array('code' => 500, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $this->db_error_msg), true, false); + + return; + } + + // connect + $dbh = @$function($dsn['username'], $dsn['password'], $dsn_options['database'], $dsn_options['charset']); + + if (!$dbh) { + $error = oci_error(); + $this->db_error = true; + $this->db_error_msg = $error['message']; + + rcube::raise_error(array('code' => 500, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $this->db_error_msg), true, false); + + return; + } + + // configure session + $this->conn_configure($dsn, $dbh); + + return $dbh; + } + + /** + * Driver-specific configuration of database connection + * + * @param array $dsn DSN for DB connections + * @param PDO $dbh Connection handler + */ + protected function conn_configure($dsn, $dbh) + { + $init_queries = array( + "ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'", + "ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'", + ); + + foreach ($init_queries as $query) { + $stmt = oci_parse($dbh, $query); + oci_execute($stmt); + } + } + + /** + * Connection state checker + * + * @return boolean True if in connected state + */ + public function is_connected() + { + return empty($this->dbh) ? false : $this->db_connected; + } + + /** + * Execute the query + */ + protected function query_execute($query) + { + // destroy reference to previous result + $this->last_result = null; + $this->db_error_msg = null; + + // prepare query + $result = oci_parse($this->dbh, $query); + + if (!@oci_execute($result, OCI_COMMIT_ON_SUCCESS)) { // OCI_NO_AUTO_COMMIT + $result = $this->handle_error($query, $result); + } + + return $this->last_result = $result; + } + + /** + * Helper method to handle DB errors. + * This by default logs the error but could be overriden by a driver implementation + * + * @param string Query that triggered the error + * @return mixed Result to be stored and returned + */ + protected function handle_error($query, $result = null) + { + $error = oci_error(is_resource($result) ? $result : $this->dbh); + + // @TODO: Find error codes for key errors + if (empty($this->options['ignore_key_errors']) || !in_array($error['code'], array('23000', '23505'))) { + $this->db_error = true; + $this->db_error_msg = sprintf('[%s] %s', $error['code'], $error['message']); + + rcube::raise_error(array('code' => 500, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $this->db_error_msg . " (SQL Query: $query)" + ), true, false); + } + + return false; + } + + /** + * Get last inserted record ID + * + * @param string $table Table name (to find the incremented sequence) + * + * @return mixed ID or false on failure + */ + public function insert_id($table = null) + { + if (!$this->db_connected || $this->db_mode == 'r' || empty($table)) { + return false; + } + + $sequence = $this->quote_identifier($this->sequence_name($table)); + $result = $this->query("SELECT $sequence.currval FROM dual"); + $result = $this->fetch_array($result); + + return $result[0] ?: false; + } + + /** + * Get number of affected rows for the last query + * + * @param mixed $result Optional query handle + * + * @return int Number of (matching) rows + */ + public function affected_rows($result = null) + { + if ($result || ($result === null && ($result = $this->last_result))) { + return oci_num_rows($result); + } + + return 0; + } + + /** + * Get number of rows for a SQL query + * If no query handle is specified, the last query will be taken as reference + * + * @param mixed $result Optional query handle + * @return mixed Number of rows or false on failure + * @deprecated This method shows very poor performance and should be avoided. + */ + public function num_rows($result = null) + { + // not implemented + return false; + } + + /** + * Get an associative array for one row + * If no query handle is specified, the last query will be taken as reference + * + * @param mixed $result Optional query handle + * + * @return mixed Array with col values or false on failure + */ + public function fetch_assoc($result = null) + { + return $this->_fetch_row($result, OCI_ASSOC); + } + + /** + * Get an index array for one row + * If no query handle is specified, the last query will be taken as reference + * + * @param mixed $result Optional query handle + * + * @return mixed Array with col values or false on failure + */ + public function fetch_array($result = null) + { + return $this->_fetch_row($result, OCI_NUM); + } + + /** + * Get col values for a result row + * + * @param mixed $result Optional query handle + * @param int $mode Fetch mode identifier + * + * @return mixed Array with col values or false on failure + */ + protected function _fetch_row($result, $mode) + { + if ($result || ($result === null && ($result = $this->last_result))) { + return oci_fetch_array($result, $mode + OCI_RETURN_NULLS + OCI_RETURN_LOBS); + } + + return false; + } + + /** + * Formats input so it can be safely used in a query + * PDO_OCI does not implement quote() method + * + * @param mixed $input Value to quote + * @param string $type Type of data (integer, bool, ident) + * + * @return string Quoted/converted string for use in query + */ + public function quote($input, $type = null) + { + // handle int directly for better performance + if ($type == 'integer' || $type == 'int') { + return intval($input); + } + + if (is_null($input)) { + return 'NULL'; + } + + if ($type == 'ident') { + return $this->quote_identifier($input); + } + + switch ($type) { + case 'bool': + case 'integer': + return intval($input); + default: + return "'" . strtr($input, array( + '?' => '??', + "'" => "''", + rcube_db::DEFAULT_QUOTE => rcube_db::DEFAULT_QUOTE . rcube_db::DEFAULT_QUOTE + )) . "'"; + } + } + + /** + * Return correct name for a specific database sequence + * + * @param string $table Table name + * + * @return string Translated sequence name + */ + protected function sequence_name($table) + { + // Note: we support only one sequence per table + // Note: The sequence name must be _seq + $sequence = $table . '_seq'; + + // modify sequence name if prefix is configured + if ($prefix = $this->options['table_prefix']) { + return $prefix . $sequence; + } + + return $sequence; + } + + /** + * Return SQL statement for case insensitive LIKE + * + * @param string $column Field name + * @param string $value Search value + * + * @return string SQL statement to use in query + */ + public function ilike($column, $value) + { + return 'UPPER(' . $this->quote_identifier($column) . ') LIKE UPPER(' . $this->quote($value) . ')'; + } + + /** + * Return SQL function for current time and date + * + * @param int $interval Optional interval (in seconds) to add/subtract + * + * @return string SQL function to use in query + */ + public function now($interval = 0) + { + if ($interval) { + $interval = intval($interval); + return "current_timestamp + INTERVAL '$interval' SECOND"; + } + + return "current_timestamp"; + } + + /** + * Return SQL statement to convert a field value into a unix timestamp + * + * @param string $field Field name + * + * @return string SQL statement to use in query + * @deprecated + */ + public function unixtimestamp($field) + { + return "(($field - to_date('1970-01-01','YYYY-MM-DD')) * 60 * 60 * 24)"; + } + + /** + * Adds TOP (LIMIT,OFFSET) clause to the query + * + * @param string $query SQL query + * @param int $limit Number of rows + * @param int $offset Offset + * + * @return string SQL query + */ + protected function set_limit($query, $limit = 0, $offset = 0) + { + $limit = intval($limit); + $offset = intval($offset); + $end = $offset + $limit; + + // @TODO: Oracle 12g has better OFFSET support + + if (!$offset) { + $query = "SELECT * FROM ($query) a WHERE rownum <= $end"; + } + else { + $query = "SELECT * FROM (SELECT a.*, rownum as rn FROM ($query) a WHERE rownum <= $end) b WHERE rn > $offset"; + } + + return $query; + } + + /** + * Parse SQL file and fix table names according to table prefix + */ + protected function fix_table_names($sql) + { + if (!$this->options['table_prefix']) { + return $sql; + } + + $sql = parent::fix_table_names($sql); + + // replace sequence names, and other Oracle-specific commands + $sql = preg_replace_callback('/((SEQUENCE ["]?)([^" \r\n]+)/', + array($this, 'fix_table_names_callback'), + $sql + ); + + $sql = preg_replace_callback( + '/([ \r\n]+["]?)([^"\' \r\n\.]+)(["]?\.nextval)/', + array($this, 'fix_table_names_seq_callback'), + $sql + ); + + return $sql; + } + + /** + * Preg_replace callback for fix_table_names() + */ + protected function fix_table_names_seq_callback($matches) + { + return $matches[1] . $this->options['table_prefix'] . $matches[2] . $matches[3]; + } + + /** + * Returns connection options from DSN array + */ + protected function dsn_options($dsn) + { + $params = array(); + + if ($dsn['hostspec']) { + $host = $dsn['hostspec']; + if ($dsn['port']) { + $host .= ':' . $dsn['port']; + } + + $params['database'] = $host . '/' . $dsn['database']; + } + + $params['charset'] = 'UTF8'; + + return $params; + } + + /** + * Execute the given SQL script + * + * @param string SQL queries to execute + * + * @return boolen True on success, False on error + */ + public function exec_script($sql) + { + $sql = $this->fix_table_names($sql); + $buff = ''; + $body = false; + + foreach (explode("\n", $sql) as $line) { + $tok = strtolower(trim($line)); + if (preg_match('/^--/', $line) || $tok == '') { + continue; + } + + $buff .= $line . "\n"; + + // detect PL/SQL function bodies, don't break on semicolon + if ($body && $tok == 'end;') { + $body = false; + } + else if (!$body && $tok == 'begin') { + $body = true; + } + + if (!$body && substr($tok, -1) == ';') { + $this->query($buff); + $buff = ''; + if ($this->db_error) { + break; + } + } + } + + return !$this->db_error; + } +} -- cgit v1.2.3 From 84515776597960a8f33172ae2a38b418c944fdbb Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Wed, 17 Sep 2014 20:39:16 +0200 Subject: Added transactions support --- program/lib/Roundcube/rcube_db.php | 67 +++++++++++++++++++++++++++-- program/lib/Roundcube/rcube_db_oci8.php | 74 ++++++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 4 deletions(-) diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php index 72cad0119..5a2ad038c 100644 --- a/program/lib/Roundcube/rcube_db.php +++ b/program/lib/Roundcube/rcube_db.php @@ -555,7 +555,9 @@ class rcube_db public function affected_rows($result = null) { if ($result || ($result === null && ($result = $this->last_result))) { - return $result->rowCount(); + if ($result !== true) { + return $result->rowCount(); + } } return 0; @@ -571,7 +573,7 @@ class rcube_db */ public function num_rows($result = null) { - if ($result || ($result === null && ($result = $this->last_result))) { + if (($result || ($result === null && ($result = $this->last_result))) && $result !== true) { // repeat query with SELECT COUNT(*) ... if (preg_match('/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/ims', $result->queryString, $m)) { $query = $this->dbh->query('SELECT COUNT(*) FROM ' . $m[1], PDO::FETCH_NUM); @@ -647,7 +649,9 @@ class rcube_db protected function _fetch_row($result, $mode) { if ($result || ($result === null && ($result = $this->last_result))) { - return $result->fetch($mode); + if ($result !== true) { + return $result->fetch($mode); + } } return false; @@ -716,6 +720,63 @@ class rcube_db return array(); } + /** + * Start transaction + * + * @return bool True on success, False on failure + */ + public function startTransaction() + { + $this->db_connect('w', true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('BEGIN TRANSACTION'); + + return $this->last_result = $this->dbh->beginTransaction(); + } + + /** + * Commit transaction + * + * @return bool True on success, False on failure + */ + public function endTransaction() + { + $this->db_connect('w', true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('COMMIT TRANSACTION'); + + return $this->last_result = $this->dbh->commit(); + } + + /** + * Rollback transaction + * + * @return bool True on success, False on failure + */ + public function rollbackTransaction() + { + $this->db_connect('w', true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('ROLLBACK TRANSACTION'); + + return $this->last_result = $this->dbh->rollBack(); + } + /** * Formats input so it can be safely used in a query * diff --git a/program/lib/Roundcube/rcube_db_oci8.php b/program/lib/Roundcube/rcube_db_oci8.php index b9f63f38a..a55ba0459 100644 --- a/program/lib/Roundcube/rcube_db_oci8.php +++ b/program/lib/Roundcube/rcube_db_oci8.php @@ -110,8 +110,9 @@ class rcube_db_oci8 extends rcube_db // prepare query $result = oci_parse($this->dbh, $query); + $mode = $this->in_transaction ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS; - if (!@oci_execute($result, OCI_COMMIT_ON_SUCCESS)) { // OCI_NO_AUTO_COMMIT + if (!@oci_execute($result, $mode)) { $result = $this->handle_error($query, $result); } @@ -459,4 +460,75 @@ class rcube_db_oci8 extends rcube_db return !$this->db_error; } + + /** + * Start transaction + * + * @return bool True on success, False on failure + */ + public function startTransaction() + { + $this->db_connect('w', true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('BEGIN TRANSACTION'); + + return $this->last_result = $this->in_transaction = true; + } + + /** + * Commit transaction + * + * @return bool True on success, False on failure + */ + public function endTransaction() + { + $this->db_connect('w', true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('COMMIT TRANSACTION'); + + if ($result = @oci_commit($this->dbh)) { + $this->in_transaction = true; + } + else { + $this->handle_error('COMMIT'); + } + + return $this->last_result = $result; + } + + /** + * Rollback transaction + * + * @return bool True on success, False on failure + */ + public function rollbackTransaction() + { + $this->db_connect('w', true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('ROLLBACK TRANSACTION'); + + if ($result = @oci_rollback($this->dbh)) { + $this->in_transaction = false; + } + else { + $this->handle_error('ROLLBACK'); + } + + return $this->last_result = $this->dbh->rollBack(); + } } -- cgit v1.2.3 From e7af012bc22722dbe63e9e79729f546d4efa934c Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Thu, 18 Sep 2014 15:32:15 +0200 Subject: Fix binary operator use for Oracle --- program/lib/Roundcube/rcube_imap_cache.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php index 519132126..6ba6b8b4c 100644 --- a/program/lib/Roundcube/rcube_imap_cache.php +++ b/program/lib/Roundcube/rcube_imap_cache.php @@ -562,6 +562,8 @@ class rcube_imap_cache } } + $binary_check = $this->db->db_provider == 'oracle' ? "BITAND(`flags`, %d)" : "(`flags` & %d)"; + $this->db->query( "UPDATE {$this->messages_table}" ." SET `expires` = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') @@ -570,6 +572,7 @@ class rcube_imap_cache ." AND `mailbox` = ?" .(!empty($uids) ? " AND `uid` IN (".$this->db->array2list($uids, 'integer').")" : "") ." AND (`flags` & $idx) ".($enabled ? "= 0" : "= $idx"), + ." AND " . sprintf($binary_check, $idx) . ($enabled ? " = 0" : " = $idx"), $this->userid, $mailbox); } -- cgit v1.2.3 From 7f8492479901117ec71dd31df0397e27aa90feab Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Thu, 18 Sep 2014 16:46:19 +0200 Subject: Fix long data handling --- program/lib/Roundcube/rcube_db_oci8.php | 73 +++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/program/lib/Roundcube/rcube_db_oci8.php b/program/lib/Roundcube/rcube_db_oci8.php index a55ba0459..2f94c1825 100644 --- a/program/lib/Roundcube/rcube_db_oci8.php +++ b/program/lib/Roundcube/rcube_db_oci8.php @@ -100,19 +100,84 @@ class rcube_db_oci8 extends rcube_db } /** - * Execute the query + * Execute a SQL query with limits + * + * @param string $query SQL query to execute + * @param int $offset Offset for LIMIT statement + * @param int $numrows Number of rows for LIMIT statement + * @param array $params Values to be inserted in query + * + * @return PDOStatement|bool Query handle or False on error */ - protected function query_execute($query) + protected function _query($query, $offset, $numrows, $params) { + $query = ltrim($query); + + $this->db_connect($this->dsn_select($query), true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + if ($numrows || $offset) { + $query = $this->set_limit($query, $numrows, $offset); + } + + // replace self::DEFAULT_QUOTE with driver-specific quoting + $query = $this->query_parse($query); + + // Because in Roundcube we mostly use queries that are + // executed only once, we will not use prepared queries + $pos = 0; + $idx = 0; + $args = array(); + + if (count($params)) { + while ($pos = strpos($query, '?', $pos)) { + if ($query[$pos+1] == '?') { // skip escaped '?' + $pos += 2; + } + else { + $val = $this->quote($params[$idx++]); + + // long strings are not allowed inline, need to be parametrized + if (strlen($val) > 4000) { + $key = ':param' . (count($args) + 1); + $args[$key] = $params[$idx-1]; + $val = $key; + } + + unset($params[$idx-1]); + $query = substr_replace($query, $val, $pos, 1); + $pos += strlen($val); + } + } + } + + // replace escaped '?' back to normal, see self::quote() + $query = str_replace('??', '?', $query); + $query = rtrim($query, " \t\n\r\0\x0B;"); + + // log query + $this->debug($query); + // destroy reference to previous result $this->last_result = null; $this->db_error_msg = null; // prepare query - $result = oci_parse($this->dbh, $query); + $result = @oci_parse($this->dbh, $query); $mode = $this->in_transaction ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS; - if (!@oci_execute($result, $mode)) { + if ($result) { + foreach ($args as $param => $arg) { + oci_bind_by_name($result, $param, $args[$param], -1, SQLT_LNG); + } + } + + // execute query + if (!$result || !@oci_execute($result, $mode)) { $result = $this->handle_error($query, $result); } -- cgit v1.2.3 From fb8adc8a19d0b4b9c3e9ea7996b6290225dc0856 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Thu, 18 Sep 2014 16:48:53 +0200 Subject: Remove Oracle driver based on PDO_OCI extension --- program/lib/Roundcube/rcube_db.php | 1 + program/lib/Roundcube/rcube_db_oci8.php | 599 ------------------------------ program/lib/Roundcube/rcube_db_oracle.php | 396 ++++++++++++++++++-- 3 files changed, 367 insertions(+), 629 deletions(-) delete mode 100644 program/lib/Roundcube/rcube_db_oci8.php diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php index 5a2ad038c..1e6a206da 100644 --- a/program/lib/Roundcube/rcube_db.php +++ b/program/lib/Roundcube/rcube_db.php @@ -70,6 +70,7 @@ class rcube_db 'dblib' => 'mssql', 'mysqli' => 'mysql', 'oci' => 'oracle', + 'oci8' => 'oracle', ); $driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver; diff --git a/program/lib/Roundcube/rcube_db_oci8.php b/program/lib/Roundcube/rcube_db_oci8.php deleted file mode 100644 index 2f94c1825..000000000 --- a/program/lib/Roundcube/rcube_db_oci8.php +++ /dev/null @@ -1,599 +0,0 @@ - | - +-----------------------------------------------------------------------+ -*/ - -/** - * Database independent query interface - * - * @package Framework - * @subpackage Database - */ -class rcube_db_oci8 extends rcube_db -{ - public $db_provider = 'oracle'; - - - /** - * Create connection instance - */ - protected function conn_create($dsn) - { - // Get database specific connection options - $dsn_options = $this->dsn_options($dsn); - - $function = $this->db_pconn ? 'oci_pconnect' : 'oci_connect'; - - if (!function_exists($function)) { - $this->db_error = true; - $this->db_error_msg = 'OCI8 extension not loaded. See http://php.net/manual/en/book.oci8.php'; - - rcube::raise_error(array('code' => 500, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg), true, false); - - return; - } - - // connect - $dbh = @$function($dsn['username'], $dsn['password'], $dsn_options['database'], $dsn_options['charset']); - - if (!$dbh) { - $error = oci_error(); - $this->db_error = true; - $this->db_error_msg = $error['message']; - - rcube::raise_error(array('code' => 500, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg), true, false); - - return; - } - - // configure session - $this->conn_configure($dsn, $dbh); - - return $dbh; - } - - /** - * Driver-specific configuration of database connection - * - * @param array $dsn DSN for DB connections - * @param PDO $dbh Connection handler - */ - protected function conn_configure($dsn, $dbh) - { - $init_queries = array( - "ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'", - "ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'", - ); - - foreach ($init_queries as $query) { - $stmt = oci_parse($dbh, $query); - oci_execute($stmt); - } - } - - /** - * Connection state checker - * - * @return boolean True if in connected state - */ - public function is_connected() - { - return empty($this->dbh) ? false : $this->db_connected; - } - - /** - * Execute a SQL query with limits - * - * @param string $query SQL query to execute - * @param int $offset Offset for LIMIT statement - * @param int $numrows Number of rows for LIMIT statement - * @param array $params Values to be inserted in query - * - * @return PDOStatement|bool Query handle or False on error - */ - protected function _query($query, $offset, $numrows, $params) - { - $query = ltrim($query); - - $this->db_connect($this->dsn_select($query), true); - - // check connection before proceeding - if (!$this->is_connected()) { - return $this->last_result = false; - } - - if ($numrows || $offset) { - $query = $this->set_limit($query, $numrows, $offset); - } - - // replace self::DEFAULT_QUOTE with driver-specific quoting - $query = $this->query_parse($query); - - // Because in Roundcube we mostly use queries that are - // executed only once, we will not use prepared queries - $pos = 0; - $idx = 0; - $args = array(); - - if (count($params)) { - while ($pos = strpos($query, '?', $pos)) { - if ($query[$pos+1] == '?') { // skip escaped '?' - $pos += 2; - } - else { - $val = $this->quote($params[$idx++]); - - // long strings are not allowed inline, need to be parametrized - if (strlen($val) > 4000) { - $key = ':param' . (count($args) + 1); - $args[$key] = $params[$idx-1]; - $val = $key; - } - - unset($params[$idx-1]); - $query = substr_replace($query, $val, $pos, 1); - $pos += strlen($val); - } - } - } - - // replace escaped '?' back to normal, see self::quote() - $query = str_replace('??', '?', $query); - $query = rtrim($query, " \t\n\r\0\x0B;"); - - // log query - $this->debug($query); - - // destroy reference to previous result - $this->last_result = null; - $this->db_error_msg = null; - - // prepare query - $result = @oci_parse($this->dbh, $query); - $mode = $this->in_transaction ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS; - - if ($result) { - foreach ($args as $param => $arg) { - oci_bind_by_name($result, $param, $args[$param], -1, SQLT_LNG); - } - } - - // execute query - if (!$result || !@oci_execute($result, $mode)) { - $result = $this->handle_error($query, $result); - } - - return $this->last_result = $result; - } - - /** - * Helper method to handle DB errors. - * This by default logs the error but could be overriden by a driver implementation - * - * @param string Query that triggered the error - * @return mixed Result to be stored and returned - */ - protected function handle_error($query, $result = null) - { - $error = oci_error(is_resource($result) ? $result : $this->dbh); - - // @TODO: Find error codes for key errors - if (empty($this->options['ignore_key_errors']) || !in_array($error['code'], array('23000', '23505'))) { - $this->db_error = true; - $this->db_error_msg = sprintf('[%s] %s', $error['code'], $error['message']); - - rcube::raise_error(array('code' => 500, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg . " (SQL Query: $query)" - ), true, false); - } - - return false; - } - - /** - * Get last inserted record ID - * - * @param string $table Table name (to find the incremented sequence) - * - * @return mixed ID or false on failure - */ - public function insert_id($table = null) - { - if (!$this->db_connected || $this->db_mode == 'r' || empty($table)) { - return false; - } - - $sequence = $this->quote_identifier($this->sequence_name($table)); - $result = $this->query("SELECT $sequence.currval FROM dual"); - $result = $this->fetch_array($result); - - return $result[0] ?: false; - } - - /** - * Get number of affected rows for the last query - * - * @param mixed $result Optional query handle - * - * @return int Number of (matching) rows - */ - public function affected_rows($result = null) - { - if ($result || ($result === null && ($result = $this->last_result))) { - return oci_num_rows($result); - } - - return 0; - } - - /** - * Get number of rows for a SQL query - * If no query handle is specified, the last query will be taken as reference - * - * @param mixed $result Optional query handle - * @return mixed Number of rows or false on failure - * @deprecated This method shows very poor performance and should be avoided. - */ - public function num_rows($result = null) - { - // not implemented - return false; - } - - /** - * Get an associative array for one row - * If no query handle is specified, the last query will be taken as reference - * - * @param mixed $result Optional query handle - * - * @return mixed Array with col values or false on failure - */ - public function fetch_assoc($result = null) - { - return $this->_fetch_row($result, OCI_ASSOC); - } - - /** - * Get an index array for one row - * If no query handle is specified, the last query will be taken as reference - * - * @param mixed $result Optional query handle - * - * @return mixed Array with col values or false on failure - */ - public function fetch_array($result = null) - { - return $this->_fetch_row($result, OCI_NUM); - } - - /** - * Get col values for a result row - * - * @param mixed $result Optional query handle - * @param int $mode Fetch mode identifier - * - * @return mixed Array with col values or false on failure - */ - protected function _fetch_row($result, $mode) - { - if ($result || ($result === null && ($result = $this->last_result))) { - return oci_fetch_array($result, $mode + OCI_RETURN_NULLS + OCI_RETURN_LOBS); - } - - return false; - } - - /** - * Formats input so it can be safely used in a query - * PDO_OCI does not implement quote() method - * - * @param mixed $input Value to quote - * @param string $type Type of data (integer, bool, ident) - * - * @return string Quoted/converted string for use in query - */ - public function quote($input, $type = null) - { - // handle int directly for better performance - if ($type == 'integer' || $type == 'int') { - return intval($input); - } - - if (is_null($input)) { - return 'NULL'; - } - - if ($type == 'ident') { - return $this->quote_identifier($input); - } - - switch ($type) { - case 'bool': - case 'integer': - return intval($input); - default: - return "'" . strtr($input, array( - '?' => '??', - "'" => "''", - rcube_db::DEFAULT_QUOTE => rcube_db::DEFAULT_QUOTE . rcube_db::DEFAULT_QUOTE - )) . "'"; - } - } - - /** - * Return correct name for a specific database sequence - * - * @param string $table Table name - * - * @return string Translated sequence name - */ - protected function sequence_name($table) - { - // Note: we support only one sequence per table - // Note: The sequence name must be _seq - $sequence = $table . '_seq'; - - // modify sequence name if prefix is configured - if ($prefix = $this->options['table_prefix']) { - return $prefix . $sequence; - } - - return $sequence; - } - - /** - * Return SQL statement for case insensitive LIKE - * - * @param string $column Field name - * @param string $value Search value - * - * @return string SQL statement to use in query - */ - public function ilike($column, $value) - { - return 'UPPER(' . $this->quote_identifier($column) . ') LIKE UPPER(' . $this->quote($value) . ')'; - } - - /** - * Return SQL function for current time and date - * - * @param int $interval Optional interval (in seconds) to add/subtract - * - * @return string SQL function to use in query - */ - public function now($interval = 0) - { - if ($interval) { - $interval = intval($interval); - return "current_timestamp + INTERVAL '$interval' SECOND"; - } - - return "current_timestamp"; - } - - /** - * Return SQL statement to convert a field value into a unix timestamp - * - * @param string $field Field name - * - * @return string SQL statement to use in query - * @deprecated - */ - public function unixtimestamp($field) - { - return "(($field - to_date('1970-01-01','YYYY-MM-DD')) * 60 * 60 * 24)"; - } - - /** - * Adds TOP (LIMIT,OFFSET) clause to the query - * - * @param string $query SQL query - * @param int $limit Number of rows - * @param int $offset Offset - * - * @return string SQL query - */ - protected function set_limit($query, $limit = 0, $offset = 0) - { - $limit = intval($limit); - $offset = intval($offset); - $end = $offset + $limit; - - // @TODO: Oracle 12g has better OFFSET support - - if (!$offset) { - $query = "SELECT * FROM ($query) a WHERE rownum <= $end"; - } - else { - $query = "SELECT * FROM (SELECT a.*, rownum as rn FROM ($query) a WHERE rownum <= $end) b WHERE rn > $offset"; - } - - return $query; - } - - /** - * Parse SQL file and fix table names according to table prefix - */ - protected function fix_table_names($sql) - { - if (!$this->options['table_prefix']) { - return $sql; - } - - $sql = parent::fix_table_names($sql); - - // replace sequence names, and other Oracle-specific commands - $sql = preg_replace_callback('/((SEQUENCE ["]?)([^" \r\n]+)/', - array($this, 'fix_table_names_callback'), - $sql - ); - - $sql = preg_replace_callback( - '/([ \r\n]+["]?)([^"\' \r\n\.]+)(["]?\.nextval)/', - array($this, 'fix_table_names_seq_callback'), - $sql - ); - - return $sql; - } - - /** - * Preg_replace callback for fix_table_names() - */ - protected function fix_table_names_seq_callback($matches) - { - return $matches[1] . $this->options['table_prefix'] . $matches[2] . $matches[3]; - } - - /** - * Returns connection options from DSN array - */ - protected function dsn_options($dsn) - { - $params = array(); - - if ($dsn['hostspec']) { - $host = $dsn['hostspec']; - if ($dsn['port']) { - $host .= ':' . $dsn['port']; - } - - $params['database'] = $host . '/' . $dsn['database']; - } - - $params['charset'] = 'UTF8'; - - return $params; - } - - /** - * Execute the given SQL script - * - * @param string SQL queries to execute - * - * @return boolen True on success, False on error - */ - public function exec_script($sql) - { - $sql = $this->fix_table_names($sql); - $buff = ''; - $body = false; - - foreach (explode("\n", $sql) as $line) { - $tok = strtolower(trim($line)); - if (preg_match('/^--/', $line) || $tok == '') { - continue; - } - - $buff .= $line . "\n"; - - // detect PL/SQL function bodies, don't break on semicolon - if ($body && $tok == 'end;') { - $body = false; - } - else if (!$body && $tok == 'begin') { - $body = true; - } - - if (!$body && substr($tok, -1) == ';') { - $this->query($buff); - $buff = ''; - if ($this->db_error) { - break; - } - } - } - - return !$this->db_error; - } - - /** - * Start transaction - * - * @return bool True on success, False on failure - */ - public function startTransaction() - { - $this->db_connect('w', true); - - // check connection before proceeding - if (!$this->is_connected()) { - return $this->last_result = false; - } - - $this->debug('BEGIN TRANSACTION'); - - return $this->last_result = $this->in_transaction = true; - } - - /** - * Commit transaction - * - * @return bool True on success, False on failure - */ - public function endTransaction() - { - $this->db_connect('w', true); - - // check connection before proceeding - if (!$this->is_connected()) { - return $this->last_result = false; - } - - $this->debug('COMMIT TRANSACTION'); - - if ($result = @oci_commit($this->dbh)) { - $this->in_transaction = true; - } - else { - $this->handle_error('COMMIT'); - } - - return $this->last_result = $result; - } - - /** - * Rollback transaction - * - * @return bool True on success, False on failure - */ - public function rollbackTransaction() - { - $this->db_connect('w', true); - - // check connection before proceeding - if (!$this->is_connected()) { - return $this->last_result = false; - } - - $this->debug('ROLLBACK TRANSACTION'); - - if ($result = @oci_rollback($this->dbh)) { - $this->in_transaction = false; - } - else { - $this->handle_error('ROLLBACK'); - } - - return $this->last_result = $this->dbh->rollBack(); - } -} diff --git a/program/lib/Roundcube/rcube_db_oracle.php b/program/lib/Roundcube/rcube_db_oracle.php index ddd351ec3..338eb2e2a 100644 --- a/program/lib/Roundcube/rcube_db_oracle.php +++ b/program/lib/Roundcube/rcube_db_oracle.php @@ -10,8 +10,8 @@ | See the README file for a full license statement. | | | | PURPOSE: | - | Database wrapper class that implements PHP PDO functions | - | for Oracle database | + | Database wrapper class that implements database functions | + | for Oracle database using OCI8 extension | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak | +-----------------------------------------------------------------------+ @@ -19,7 +19,6 @@ /** * Database independent query interface - * This is a wrapper for the PHP PDO * * @package Framework * @subpackage Database @@ -28,6 +27,49 @@ class rcube_db_oracle extends rcube_db { public $db_provider = 'oracle'; + + /** + * Create connection instance + */ + protected function conn_create($dsn) + { + // Get database specific connection options + $dsn_options = $this->dsn_options($dsn); + + $function = $this->db_pconn ? 'oci_pconnect' : 'oci_connect'; + + if (!function_exists($function)) { + $this->db_error = true; + $this->db_error_msg = 'OCI8 extension not loaded. See http://php.net/manual/en/book.oci8.php'; + + rcube::raise_error(array('code' => 500, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $this->db_error_msg), true, false); + + return; + } + + // connect + $dbh = @$function($dsn['username'], $dsn['password'], $dsn_options['database'], $dsn_options['charset']); + + if (!$dbh) { + $error = oci_error(); + $this->db_error = true; + $this->db_error_msg = $error['message']; + + rcube::raise_error(array('code' => 500, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $this->db_error_msg), true, false); + + return; + } + + // configure session + $this->conn_configure($dsn, $dbh); + + return $dbh; + } + /** * Driver-specific configuration of database connection * @@ -36,8 +78,135 @@ class rcube_db_oracle extends rcube_db */ protected function conn_configure($dsn, $dbh) { - $dbh->query("ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'"); - $dbh->query("ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'"); + $init_queries = array( + "ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'", + "ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'", + ); + + foreach ($init_queries as $query) { + $stmt = oci_parse($dbh, $query); + oci_execute($stmt); + } + } + + /** + * Connection state checker + * + * @return boolean True if in connected state + */ + public function is_connected() + { + return empty($this->dbh) ? false : $this->db_connected; + } + + /** + * Execute a SQL query with limits + * + * @param string $query SQL query to execute + * @param int $offset Offset for LIMIT statement + * @param int $numrows Number of rows for LIMIT statement + * @param array $params Values to be inserted in query + * + * @return PDOStatement|bool Query handle or False on error + */ + protected function _query($query, $offset, $numrows, $params) + { + $query = ltrim($query); + + $this->db_connect($this->dsn_select($query), true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + if ($numrows || $offset) { + $query = $this->set_limit($query, $numrows, $offset); + } + + // replace self::DEFAULT_QUOTE with driver-specific quoting + $query = $this->query_parse($query); + + // Because in Roundcube we mostly use queries that are + // executed only once, we will not use prepared queries + $pos = 0; + $idx = 0; + $args = array(); + + if (count($params)) { + while ($pos = strpos($query, '?', $pos)) { + if ($query[$pos+1] == '?') { // skip escaped '?' + $pos += 2; + } + else { + $val = $this->quote($params[$idx++]); + + // long strings are not allowed inline, need to be parametrized + if (strlen($val) > 4000) { + $key = ':param' . (count($args) + 1); + $args[$key] = $params[$idx-1]; + $val = $key; + } + + unset($params[$idx-1]); + $query = substr_replace($query, $val, $pos, 1); + $pos += strlen($val); + } + } + } + + // replace escaped '?' back to normal, see self::quote() + $query = str_replace('??', '?', $query); + $query = rtrim($query, " \t\n\r\0\x0B;"); + + // log query + $this->debug($query); + + // destroy reference to previous result + $this->last_result = null; + $this->db_error_msg = null; + + // prepare query + $result = @oci_parse($this->dbh, $query); + $mode = $this->in_transaction ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS; + + if ($result) { + foreach ($args as $param => $arg) { + oci_bind_by_name($result, $param, $args[$param], -1, SQLT_LNG); + } + } + + // execute query + if (!$result || !@oci_execute($result, $mode)) { + $result = $this->handle_error($query, $result); + } + + return $this->last_result = $result; + } + + /** + * Helper method to handle DB errors. + * This by default logs the error but could be overriden by a driver implementation + * + * @param string Query that triggered the error + * @return mixed Result to be stored and returned + */ + protected function handle_error($query, $result = null) + { + $error = oci_error(is_resource($result) ? $result : $this->dbh); + + // @TODO: Find error codes for key errors + if (empty($this->options['ignore_key_errors']) || !in_array($error['code'], array('23000', '23505'))) { + $this->db_error = true; + $this->db_error_msg = sprintf('[%s] %s', $error['code'], $error['message']); + + rcube::raise_error(array('code' => 500, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $this->db_error_msg . " (SQL Query: $query)" + ), true, false); + } + + return false; } /** @@ -54,9 +223,83 @@ class rcube_db_oracle extends rcube_db } $sequence = $this->quote_identifier($this->sequence_name($table)); - $result = $dbh->query("SELECT $sequence.currval FROM dual"); + $result = $this->query("SELECT $sequence.currval FROM dual"); + $result = $this->fetch_array($result); + + return $result[0] ?: false; + } + + /** + * Get number of affected rows for the last query + * + * @param mixed $result Optional query handle + * + * @return int Number of (matching) rows + */ + public function affected_rows($result = null) + { + if ($result || ($result === null && ($result = $this->last_result))) { + return oci_num_rows($result); + } + + return 0; + } + + /** + * Get number of rows for a SQL query + * If no query handle is specified, the last query will be taken as reference + * + * @param mixed $result Optional query handle + * @return mixed Number of rows or false on failure + * @deprecated This method shows very poor performance and should be avoided. + */ + public function num_rows($result = null) + { + // not implemented + return false; + } + + /** + * Get an associative array for one row + * If no query handle is specified, the last query will be taken as reference + * + * @param mixed $result Optional query handle + * + * @return mixed Array with col values or false on failure + */ + public function fetch_assoc($result = null) + { + return $this->_fetch_row($result, OCI_ASSOC); + } - return $result ? $result->fetchColumn() : false; + /** + * Get an index array for one row + * If no query handle is specified, the last query will be taken as reference + * + * @param mixed $result Optional query handle + * + * @return mixed Array with col values or false on failure + */ + public function fetch_array($result = null) + { + return $this->_fetch_row($result, OCI_NUM); + } + + /** + * Get col values for a result row + * + * @param mixed $result Optional query handle + * @param int $mode Fetch mode identifier + * + * @return mixed Array with col values or false on failure + */ + protected function _fetch_row($result, $mode) + { + if ($result || ($result === null && ($result = $this->last_result))) { + return oci_fetch_array($result, $mode + OCI_RETURN_NULLS + OCI_RETURN_LOBS); + } + + return false; } /** @@ -177,23 +420,13 @@ class rcube_db_oracle extends rcube_db // @TODO: Oracle 12g has better OFFSET support - $orderby = stristr($query, 'ORDER BY'); - $select = substr($query, 0, stripos($query, 'FROM')); - $offset += 1; - - if ($orderby !== false) { - $query = trim(substr($query, 0, -1 * strlen($orderby))); + if (!$offset) { + $query = "SELECT * FROM ($query) a WHERE rownum <= $end"; } else { - // it shouldn't happen, paging without sorting has not much sense - // @FIXME: I don't know how to build paging query without ORDER BY - $orderby = "ORDER BY 1"; + $query = "SELECT * FROM (SELECT a.*, rownum as rn FROM ($query) a WHERE rownum <= $end) b WHERE rn > $offset"; } - $query = preg_replace('/^SELECT\s/i', '', $query); - $query = "$select FROM (SELECT ROW_NUMBER() OVER ($orderby) AS row_number, $query)" - . " WHERE row_number BETWEEN $offset AND $end"; - return $query; } @@ -232,12 +465,11 @@ class rcube_db_oracle extends rcube_db } /** - * Returns PDO DSN string from DSN array + * Returns connection options from DSN array */ - protected function dsn_string($dsn) + protected function dsn_options($dsn) { $params = array(); - $result = 'oci:'; if ($dsn['hostspec']) { $host = $dsn['hostspec']; @@ -245,19 +477,123 @@ class rcube_db_oracle extends rcube_db $host .= ':' . $dsn['port']; } - $dsn['database'] = $host . '/' . $dsn['database']; + $params['database'] = $host . '/' . $dsn['database']; } - if ($dsn['database']) { - $params[] = 'dbname=' . $dsn['database']; + $params['charset'] = 'UTF8'; + + return $params; + } + + /** + * Execute the given SQL script + * + * @param string SQL queries to execute + * + * @return boolen True on success, False on error + */ + public function exec_script($sql) + { + $sql = $this->fix_table_names($sql); + $buff = ''; + $body = false; + + foreach (explode("\n", $sql) as $line) { + $tok = strtolower(trim($line)); + if (preg_match('/^--/', $line) || $tok == '') { + continue; + } + + $buff .= $line . "\n"; + + // detect PL/SQL function bodies, don't break on semicolon + if ($body && $tok == 'end;') { + $body = false; + } + else if (!$body && $tok == 'begin') { + $body = true; + } + + if (!$body && substr($tok, -1) == ';') { + $this->query($buff); + $buff = ''; + if ($this->db_error) { + break; + } + } } - $params['charset'] = 'UTF8'; + return !$this->db_error; + } + + /** + * Start transaction + * + * @return bool True on success, False on failure + */ + public function startTransaction() + { + $this->db_connect('w', true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('BEGIN TRANSACTION'); + + return $this->last_result = $this->in_transaction = true; + } + + /** + * Commit transaction + * + * @return bool True on success, False on failure + */ + public function endTransaction() + { + $this->db_connect('w', true); - if (!empty($params)) { - $result .= implode(';', $params); + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('COMMIT TRANSACTION'); + + if ($result = @oci_commit($this->dbh)) { + $this->in_transaction = true; + } + else { + $this->handle_error('COMMIT'); + } + + return $this->last_result = $result; + } + + /** + * Rollback transaction + * + * @return bool True on success, False on failure + */ + public function rollbackTransaction() + { + $this->db_connect('w', true); + + // check connection before proceeding + if (!$this->is_connected()) { + return $this->last_result = false; + } + + $this->debug('ROLLBACK TRANSACTION'); + + if ($result = @oci_rollback($this->dbh)) { + $this->in_transaction = false; + } + else { + $this->handle_error('ROLLBACK'); } - return $result; + return $this->last_result = $this->dbh->rollBack(); } } -- cgit v1.2.3 From c2345747acec85bd158784a2819b5273e6af3cdd Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Thu, 18 Sep 2014 16:57:41 +0200 Subject: Add Oracle driver check in Installer --- installer/check.php | 1 + program/include/rcmail_install.php | 1 + 2 files changed, 2 insertions(+) diff --git a/installer/check.php b/installer/check.php index 6974d3720..80c22bb69 100644 --- a/installer/check.php +++ b/installer/check.php @@ -67,6 +67,7 @@ $source_urls = array( 'DOM' => 'http://www.php.net/manual/en/book.dom.php', 'Intl' => 'http://www.php.net/manual/en/book.intl.php', 'Exif' => 'http://www.php.net/manual/en/book.exif.php', + 'oci8' => 'http://www.php.net/manual/en/book.oci8.php', 'PDO' => 'http://www.php.net/manual/en/book.pdo.php', 'pdo_mysql' => 'http://www.php.net/manual/en/ref.pdo-mysql.php', 'pdo_pgsql' => 'http://www.php.net/manual/en/ref.pdo-pgsql.php', diff --git a/program/include/rcmail_install.php b/program/include/rcmail_install.php index 9945f1d81..7877b8e33 100644 --- a/program/include/rcmail_install.php +++ b/program/include/rcmail_install.php @@ -55,6 +55,7 @@ class rcmail_install 'SQLite (v2)' => 'pdo_sqlite2', 'SQL Server (SQLSRV)' => 'pdo_sqlsrv', 'SQL Server (DBLIB)' => 'pdo_dblib', + 'Oracle' => 'oci8', ); -- cgit v1.2.3