lastParameters = $parameters; // Note: Postgres always behaves as though $selectDB = true, ignoring // any value actually passed in. The controller passes in true for other // connectors such as PDOConnector. // Escape parameters $arguments = array( $this->escapeParameter($parameters, 'server', 'host', 'localhost'), $this->escapeParameter($parameters, 'port', 'port', 5432), $this->escapeParameter($parameters, 'database', 'dbname', 'postgres'), $this->escapeParameter($parameters, 'username', 'user'), $this->escapeParameter($parameters, 'password', 'password') ); // Close the old connection if ($this->dbConn) { pg_close($this->dbConn); } // Connect $this->dbConn = @pg_connect(implode(' ', $arguments)); if ($this->dbConn === false) { // Extract error details from PHP error handling $error = error_get_last(); if ($error && preg_match('/function\\.pg-connect\\<\\/a\\>\\]\\: (?.*)/', $error['message'], $matches)) { $this->databaseError(html_entity_decode($matches['message'])); } else { $this->databaseError("Couldn't connect to PostgreSQL database."); } } elseif (pg_connection_status($this->dbConn) != PGSQL_CONNECTION_OK) { throw new ErrorException($this->getLastError()); } //By virtue of getting here, the connection is active: $this->databaseName = empty($parameters['database']) ? PostgreSQLDatabase::MASTER_DATABASE : $parameters['database']; } public function affectedRows() { return $this->lastRows; } public function getGeneratedID($table) { $result = $this->query("SELECT last_value FROM \"{$table}_ID_seq\";")->first(); return $result['last_value']; } public function getLastError() { return pg_last_error($this->dbConn); } public function getSelectedDatabase() { return $this->databaseName; } public function getVersion() { $version = pg_version($this->dbConn); if (isset($version['server'])) { return $version['server']; } else { return false; } } public function isActive() { return $this->databaseName && $this->dbConn; } /** * Determines if the SQL fragment either breaks into or out of a string literal * by counting single quotes * * Handles double-quote escaped quotes as well as slash escaped quotes * * @todo Test this! * * @see http://www.postgresql.org/docs/8.3/interactive/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS * * @param string $input The SQL fragment * @return boolean True if the string breaks into or out of a string literal */ public function checkStringTogglesLiteral($input) { // Remove escaped backslashes, count them! $input = preg_replace('/\\\\\\\\/', '', $input); // Count quotes $totalQuotes = substr_count($input, "'"); // Includes double quote escaped quotes $escapedQuotes = substr_count($input, "\\'"); return (($totalQuotes - $escapedQuotes) % 2) !== 0; } /** * Iteratively replaces all question marks with numerical placeholders * E.g. "Title = ? AND Name = ?" becomes "Title = $1 AND Name = $2" * * @todo Better consider question marks in string literals * * @param string $sql Paramaterised query using question mark placeholders * @return string Paramaterised query using numeric placeholders */ public function replacePlaceholders($sql) { $segments = preg_split('/\?/', $sql); $joined = ''; $inString = false; $num = 0; for ($i = 0; $i < count($segments); $i++) { // Append next segment $joined .= $segments[$i]; // Don't add placeholder after last segment if ($i === count($segments) - 1) { break; } // check string escape on previous fragment if ($this->checkStringTogglesLiteral($segments[$i])) { $inString = !$inString; } // Append placeholder replacement if ($inString) { $joined .= "?"; } else { $joined .= '$' . ++$num; } } return $joined; } public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) { // Reset state $this->lastQuery = null; $this->lastRows = 0; // Replace question mark placeholders with numeric placeholders if (!empty($parameters)) { $sql = $this->replacePlaceholders($sql); $parameters = $this->parameterValues($parameters); } // Execute query // Unfortunately error-suppression is required in order to handle sql errors elegantly. // Please use PDO if you can help it if (!empty($parameters)) { $result = @pg_query_params($this->dbConn, $sql, $parameters); } else { $result = @pg_query($this->dbConn, $sql); } // Handle error if (!$result) { $this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters); return null; } // Save and return results $this->lastQuery = $result; $this->lastRows = pg_affected_rows($result); return new PostgreSQLQuery($result); } public function query($sql, $errorLevel = E_USER_ERROR) { return $this->preparedQuery($sql, array(), $errorLevel); } public function quoteString($value) { if (function_exists('pg_escape_literal')) { return pg_escape_literal($this->dbConn, $value); } else { return "'" . $this->escapeString($value) . "'"; } } public function escapeString($value) { return pg_escape_string($this->dbConn, $value); } public function selectDatabase($name) { if ($name !== $this->databaseName) { user_error("PostgreSQLConnector can't change databases. Please create a new database connection", E_USER_ERROR); } return true; } public function unloadDatabase() { $this->databaseName = null; } }