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)); |
||
0 ignored issues
–
show
|
|||
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(); |
||
0 ignored issues
–
show
Are you sure the usage of
$this->query('SELECT cur...'.$table.'_ID_seq\"')') targeting SilverStripe\PostgreSQL\...reSQLConnector::query() seems to always return null.
This check looks for function or method calls that always return null and whose return value is used. class A
{
function getObject()
{
return null;
}
}
$a = new A();
if ($a->getObject()) {
The method The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes. ![]() |
|||
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; |
||
0 ignored issues
–
show
The expression
return false returns the type false which is incompatible with the return type mandated by SilverStripe\ORM\Connect\DBConnector::getVersion() of string .
In the issue above, the returned value is violating the contract defined by the mentioned interface. Let's take a look at an example: interface HasName {
/** @return string */
public function getName();
}
class Name {
public $name;
}
class User implements HasName {
/** @return string|Name */
public function getName() {
return new Name('foo'); // This is a violation of the ``HasName`` interface
// which only allows a string value to be returned.
}
}
![]() |
|||
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++) { |
||
0 ignored issues
–
show
It seems like you are calling the size function
count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}
// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
![]() |
|||
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; |
||
0 ignored issues
–
show
|
|||
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) { |
||
0 ignored issues
–
show
|
|||
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); |
||
0 ignored issues
–
show
Are you sure the usage of
$this->preparedQuery($sql, array(), $errorLevel) targeting SilverStripe\PostgreSQL\...nector::preparedQuery() seems to always return null.
This check looks for function or method calls that always return null and whose return value is used. class A
{
function getObject()
{
return null;
}
}
$a = new A();
if ($a->getObject()) {
The method The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes. ![]() |
|||
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.