1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverStripe\PostgreSQL; |
4
|
|
|
|
5
|
|
|
use SilverStripe\ORM\Connect\DBConnector; |
6
|
|
|
use ErrorException; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* PostgreSQL connector class using the PostgreSQL specific api |
10
|
|
|
* |
11
|
|
|
* The connector doesn't know anything about schema selection, so code related to |
12
|
|
|
* masking multiple databases as schemas should be handled in the database controller |
13
|
|
|
* and schema manager. |
14
|
|
|
* |
15
|
|
|
* @package sapphire |
16
|
|
|
* @subpackage model |
17
|
|
|
*/ |
18
|
|
|
class PostgreSQLConnector extends DBConnector |
19
|
|
|
{ |
20
|
|
|
/** |
21
|
|
|
* Connection to the PG Database database |
22
|
|
|
* |
23
|
|
|
* @var resource |
24
|
|
|
*/ |
25
|
|
|
protected $dbConn = null; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Name of the currently selected database |
29
|
|
|
* |
30
|
|
|
* @var string |
31
|
|
|
*/ |
32
|
|
|
protected $databaseName = null; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Reference to the last query result (for pg_affected_rows) |
36
|
|
|
* |
37
|
|
|
* @var resource |
38
|
|
|
*/ |
39
|
|
|
protected $lastQuery = null; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Last parameters used to connect |
43
|
|
|
* |
44
|
|
|
* @var array |
45
|
|
|
*/ |
46
|
|
|
protected $lastParameters = null; |
47
|
|
|
|
48
|
|
|
protected $lastRows = 0; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Escape a parameter to be used in the connection string |
52
|
|
|
* |
53
|
|
|
* @param array $parameters All parameters |
54
|
|
|
* @param string $key The key in $parameters to pull from |
55
|
|
|
* @param string $name The connection string parameter name |
56
|
|
|
* @param mixed $default The default value, or null if optional |
57
|
|
|
* @return string The completed fragment in the form name=value |
58
|
|
|
*/ |
59
|
|
|
protected function escapeParameter($parameters, $key, $name, $default = null) |
60
|
|
|
{ |
61
|
|
|
if (empty($parameters[$key])) { |
62
|
|
|
if ($default === null) { |
63
|
|
|
return ''; |
64
|
|
|
} |
65
|
|
|
$value = $default; |
66
|
|
|
} else { |
67
|
|
|
$value = $parameters[$key]; |
68
|
|
|
} |
69
|
|
|
return "$name='" . addslashes($value) . "'"; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
public function connect($parameters, $selectDB = false) |
73
|
|
|
{ |
74
|
|
|
$this->lastParameters = $parameters; |
75
|
|
|
|
76
|
|
|
// Note: Postgres always behaves as though $selectDB = true, ignoring |
77
|
|
|
// any value actually passed in. The controller passes in true for other |
78
|
|
|
// connectors such as PDOConnector. |
79
|
|
|
|
80
|
|
|
// Escape parameters |
81
|
|
|
$arguments = array( |
82
|
|
|
$this->escapeParameter($parameters, 'server', 'host', 'localhost'), |
83
|
|
|
$this->escapeParameter($parameters, 'port', 'port', 5432), |
84
|
|
|
$this->escapeParameter($parameters, 'database', 'dbname', 'postgres'), |
85
|
|
|
$this->escapeParameter($parameters, 'username', 'user'), |
86
|
|
|
$this->escapeParameter($parameters, 'password', 'password') |
87
|
|
|
); |
88
|
|
|
|
89
|
|
|
// Close the old connection |
90
|
|
|
if ($this->dbConn) { |
91
|
|
|
pg_close($this->dbConn); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
// Connect |
95
|
|
|
$this->dbConn = @pg_connect(implode(' ', $arguments)); |
|
|
|
|
96
|
|
|
if ($this->dbConn === false) { |
97
|
|
|
// Extract error details from PHP error handling |
98
|
|
|
$error = error_get_last(); |
99
|
|
|
if ($error && preg_match('/function\\.pg-connect\\<\\/a\\>\\]\\: (?<message>.*)/', $error['message'], $matches)) { |
100
|
|
|
$this->databaseError(html_entity_decode($matches['message'])); |
101
|
|
|
} else { |
102
|
|
|
$this->databaseError("Couldn't connect to PostgreSQL database."); |
103
|
|
|
} |
104
|
|
|
} elseif (pg_connection_status($this->dbConn) != PGSQL_CONNECTION_OK) { |
105
|
|
|
throw new ErrorException($this->getLastError()); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
//By virtue of getting here, the connection is active: |
109
|
|
|
$this->databaseName = empty($parameters['database']) ? PostgreSQLDatabase::MASTER_DATABASE : $parameters['database']; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
public function affectedRows() |
113
|
|
|
{ |
114
|
|
|
return $this->lastRows; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
public function getGeneratedID($table) |
118
|
|
|
{ |
119
|
|
|
$result = $this->query("SELECT currval('\"{$table}_ID_seq\"')")->first(); |
|
|
|
|
120
|
|
|
return $result['currval']; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
public function getLastError() |
124
|
|
|
{ |
125
|
|
|
return pg_last_error($this->dbConn); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
public function getSelectedDatabase() |
129
|
|
|
{ |
130
|
|
|
return $this->databaseName; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
public function getVersion() |
134
|
|
|
{ |
135
|
|
|
$version = pg_version($this->dbConn); |
136
|
|
|
if (isset($version['server'])) { |
137
|
|
|
return $version['server']; |
138
|
|
|
} else { |
139
|
|
|
return false; |
|
|
|
|
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
public function isActive() |
144
|
|
|
{ |
145
|
|
|
return $this->databaseName && $this->dbConn; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Determines if the SQL fragment either breaks into or out of a string literal |
150
|
|
|
* by counting single quotes |
151
|
|
|
* |
152
|
|
|
* Handles double-quote escaped quotes as well as slash escaped quotes |
153
|
|
|
* |
154
|
|
|
* @todo Test this! |
155
|
|
|
* |
156
|
|
|
* @see http://www.postgresql.org/docs/8.3/interactive/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS |
157
|
|
|
* |
158
|
|
|
* @param string $input The SQL fragment |
159
|
|
|
* @return boolean True if the string breaks into or out of a string literal |
160
|
|
|
*/ |
161
|
|
|
public function checkStringTogglesLiteral($input) |
162
|
|
|
{ |
163
|
|
|
// Remove escaped backslashes, count them! |
164
|
|
|
$input = preg_replace('/\\\\\\\\/', '', $input); |
165
|
|
|
|
166
|
|
|
// Count quotes |
167
|
|
|
$totalQuotes = substr_count($input, "'"); // Includes double quote escaped quotes |
168
|
|
|
$escapedQuotes = substr_count($input, "\\'"); |
169
|
|
|
return (($totalQuotes - $escapedQuotes) % 2) !== 0; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Iteratively replaces all question marks with numerical placeholders |
174
|
|
|
* E.g. "Title = ? AND Name = ?" becomes "Title = $1 AND Name = $2" |
175
|
|
|
* |
176
|
|
|
* @todo Better consider question marks in string literals |
177
|
|
|
* |
178
|
|
|
* @param string $sql Paramaterised query using question mark placeholders |
179
|
|
|
* @return string Paramaterised query using numeric placeholders |
180
|
|
|
*/ |
181
|
|
|
public function replacePlaceholders($sql) |
182
|
|
|
{ |
183
|
|
|
$segments = preg_split('/\?/', $sql); |
184
|
|
|
$joined = ''; |
185
|
|
|
$inString = false; |
186
|
|
|
$num = 0; |
187
|
|
|
for ($i = 0; $i < count($segments); $i++) { |
|
|
|
|
188
|
|
|
// Append next segment |
189
|
|
|
$joined .= $segments[$i]; |
190
|
|
|
|
191
|
|
|
// Don't add placeholder after last segment |
192
|
|
|
if ($i === count($segments) - 1) { |
193
|
|
|
break; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
// check string escape on previous fragment |
197
|
|
|
if ($this->checkStringTogglesLiteral($segments[$i])) { |
198
|
|
|
$inString = !$inString; |
|
|
|
|
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
// Append placeholder replacement |
202
|
|
|
if ($inString) { |
203
|
|
|
$joined .= "?"; |
204
|
|
|
} else { |
205
|
|
|
$joined .= '$' . ++$num; |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
return $joined; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) |
212
|
|
|
{ |
213
|
|
|
// Reset state |
214
|
|
|
$this->lastQuery = null; |
215
|
|
|
$this->lastRows = 0; |
216
|
|
|
|
217
|
|
|
// Replace question mark placeholders with numeric placeholders |
218
|
|
|
if (!empty($parameters)) { |
219
|
|
|
$sql = $this->replacePlaceholders($sql); |
220
|
|
|
$parameters = $this->parameterValues($parameters); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
// Execute query |
224
|
|
|
// Unfortunately error-suppression is required in order to handle sql errors elegantly. |
225
|
|
|
// Please use PDO if you can help it |
226
|
|
|
if (!empty($parameters)) { |
227
|
|
|
$result = @pg_query_params($this->dbConn, $sql, $parameters); |
228
|
|
|
} else { |
229
|
|
|
$result = @pg_query($this->dbConn, $sql); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
// Handle error |
233
|
|
|
if (!$result) { |
|
|
|
|
234
|
|
|
$this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters); |
235
|
|
|
return null; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
// Save and return results |
239
|
|
|
$this->lastQuery = $result; |
240
|
|
|
$this->lastRows = pg_affected_rows($result); |
241
|
|
|
return new PostgreSQLQuery($result); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
public function query($sql, $errorLevel = E_USER_ERROR) |
245
|
|
|
{ |
246
|
|
|
return $this->preparedQuery($sql, array(), $errorLevel); |
|
|
|
|
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
public function quoteString($value) |
250
|
|
|
{ |
251
|
|
|
if (function_exists('pg_escape_literal')) { |
252
|
|
|
return pg_escape_literal($this->dbConn, $value); |
253
|
|
|
} else { |
254
|
|
|
return "'" . $this->escapeString($value) . "'"; |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
public function escapeString($value) |
259
|
|
|
{ |
260
|
|
|
return pg_escape_string($this->dbConn, $value); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
public function selectDatabase($name) |
264
|
|
|
{ |
265
|
|
|
if ($name !== $this->databaseName) { |
266
|
|
|
user_error("PostgreSQLConnector can't change databases. Please create a new database connection", E_USER_ERROR); |
267
|
|
|
} |
268
|
|
|
return true; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
public function unloadDatabase() |
272
|
|
|
{ |
273
|
|
|
$this->databaseName = null; |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.