Issues (41)

code/PostgreSQLConnector.php (7 issues)

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
Documentation Bug introduced by
It seems like @pg_connect(implode(' ', $arguments)) can also be of type false. However, the property $dbConn is declared as type resource. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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 getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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
Bug Best Practice introduced by
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.
    }
}
Loading history...
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
Performance Best Practice introduced by
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
}
Loading history...
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
The condition $inString is always false.
Loading history...
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
$result is of type false|resource, thus it always evaluated to false.
Loading history...
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 getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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