Passed
Pull Request — 4 (#8209)
by Ingo
09:07
created

createConnection()   D

Complexity

Conditions 16
Paths 44

Size

Total Lines 80

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 44
nop 2
dl 0
loc 80
rs 4.8896
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Dev\Install;
4
5
use mysqli;
6
use PDO;
7
use Exception;
8
use mysqli_result;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\ORM\Connect\MySQLiConnector;
11
use SilverStripe\ORM\Connect\PDOConnector;
12
13
/**
14
 * This is a helper class for the SS installer.
15
 *
16
 * It does all the specific checking for MySQLDatabase
17
 * to ensure that the configuration is setup correctly.
18
 */
19
class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper
20
{
21
22
    /**
23
     * Create a connection of the appropriate type
24
     *
25
     * @skipUpgrade
26
     * @param array $databaseConfig
27
     * @param string $error Error message passed by value
28
     * @return mixed|null Either the connection object, or null if error
29
     */
30
    protected function createConnection($databaseConfig, &$error)
31
    {
32
        $error = null;
33
        try {
34
            switch ($databaseConfig['type']) {
35
                case 'MySQLDatabase':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
36
                    $conn = mysqli_init();
37
38
                    // Set SSL parameters if they exist. All parameters are required.
39
                    if (array_key_exists('ssl_key', $databaseConfig) &&
40
                        array_key_exists('ssl_cert', $databaseConfig) &&
41
                        array_key_exists('ssl_ca', $databaseConfig)
42
                    ) {
43
                        $conn->ssl_set(
44
                            $databaseConfig['ssl_key'],
45
                            $databaseConfig['ssl_cert'],
46
                            $databaseConfig['ssl_ca'],
47
                            dirname($databaseConfig['ssl_ca']),
48
                            array_key_exists('ssl_cipher', $databaseConfig)
49
                                ? $databaseConfig['ssl_cipher']
50
                                : Config::inst()->get(MySQLiConnector::class, 'ssl_cipher_default')
51
                        );
52
                    }
53
                    @$conn->real_connect(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for real_connect(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

53
                    /** @scrutinizer ignore-unhandled */ @$conn->real_connect(

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
54
                        $databaseConfig['server'],
55
                        $databaseConfig['username'],
56
                        $databaseConfig['password']
57
                    );
58
59
                    if ($conn && empty($conn->connect_errno)) {
60
                        $conn->query("SET sql_mode = 'ANSI'");
61
                        return $conn;
62
                    } else {
63
                        $error = ($conn->connect_errno)
64
                            ? $conn->connect_error
65
                            : 'Unknown connection error';
66
                        return null;
67
                    }
68
                case 'MySQLPDODatabase':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
69
                    // May throw a PDOException if fails
70
71
                    // Set SSL parameters
72
                    $ssl = null;
73
74
                    if (array_key_exists('ssl_key', $databaseConfig) &&
75
                        array_key_exists('ssl_cert', $databaseConfig)
76
                    ) {
77
                        $ssl = array(
78
                            PDO::MYSQL_ATTR_SSL_KEY => $databaseConfig['ssl_key'],
79
                            PDO::MYSQL_ATTR_SSL_CERT => $databaseConfig['ssl_cert'],
80
                        );
81
                        if (array_key_exists('ssl_ca', $databaseConfig)) {
82
                            $ssl[PDO::MYSQL_ATTR_SSL_CA] = $databaseConfig['ssl_ca'];
83
                        }
84
                        // use default cipher if not provided
85
                        $ssl[PDO::MYSQL_ATTR_SSL_CA] = array_key_exists('ssl_ca', $databaseConfig)
86
                            ? $databaseConfig['ssl_ca']
87
                            : Config::inst()->get(PDOConnector::class, 'ssl_cipher_default');
88
                    }
89
90
                    $conn = @new PDO(
91
                        'mysql:host=' . $databaseConfig['server'],
92
                        $databaseConfig['username'],
93
                        $databaseConfig['password'],
94
                        $ssl
95
                    );
96
                    if ($conn) {
0 ignored issues
show
introduced by
$conn is of type PDO, thus it always evaluated to true.
Loading history...
97
                        $conn->query("SET sql_mode = 'ANSI'");
98
                        return $conn;
99
                    } else {
100
                        $error = 'Unknown connection error';
101
                        return null;
102
                    }
103
                default:
104
                    $error = 'Invalid connection type: ' . $databaseConfig['type'];
105
                    return null;
106
            }
107
        } catch (Exception $ex) {
108
            $error = $ex->getMessage();
109
            return null;
110
        }
111
    }
112
113
    /**
114
     * Helper function to quickly extract a column from a mysqi_result
115
     *
116
     * @param mixed $results mysqli_result or enumerable list of rows
117
     * @return array Resulting data
118
     */
119
    protected function column($results)
120
    {
121
        $array = array();
122
        if ($results instanceof mysqli_result) {
123
            while ($row = $results->fetch_array()) {
124
                $array[] = $row[0];
125
            }
126
        } else {
127
            foreach ($results as $row) {
128
                $array[] = $row[0];
129
            }
130
        }
131
        return $array;
132
    }
133
134
    public function requireDatabaseFunctions($databaseConfig)
135
    {
136
        $data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']);
137
        return !empty($data['supported']);
138
    }
139
140
    public function requireDatabaseServer($databaseConfig)
141
    {
142
        $connection = $this->createConnection($databaseConfig, $error);
143
        $success = !empty($connection);
144
145
        return array(
146
            'success' => $success,
147
            'error' => $error
148
        );
149
    }
150
151
    public function getDatabaseVersion($databaseConfig)
152
    {
153
        $conn = $this->createConnection($databaseConfig, $error);
154
        if (!$conn) {
155
            return false;
156
        } elseif ($conn instanceof mysqli) {
157
            return $conn->server_info;
158
        } elseif ($conn instanceof PDO) {
0 ignored issues
show
introduced by
$conn is always a sub-type of PDO.
Loading history...
159
            return $conn->getAttribute(PDO::ATTR_SERVER_VERSION);
160
        }
161
        return false;
162
    }
163
164
    /**
165
     * Ensure that the MySQL server version is at least 5.0.
166
     *
167
     * @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
168
     * @return array Result - e.g. array('success' => true, 'error' => 'details of error')
169
     */
170
    public function requireDatabaseVersion($databaseConfig)
171
    {
172
        $version = $this->getDatabaseVersion($databaseConfig);
173
        $success = false;
174
        $error = '';
175
        if ($version) {
176
            $success = version_compare($version, '5.0', '>=');
177
            if (!$success) {
178
                $error = "Your MySQL server version is $version. It's recommended you use at least MySQL 5.0.";
179
            }
180
        } else {
181
            $error = "Could not determine your MySQL version.";
182
        }
183
        return array(
184
            'success' => $success,
185
            'error' => $error
186
        );
187
    }
188
189
    public function requireDatabaseConnection($databaseConfig)
190
    {
191
        $conn = $this->createConnection($databaseConfig, $error);
192
        $success = !empty($conn);
193
194
        // Check database name only uses valid characters
195
        if ($success && !$this->checkValidDatabaseName($databaseConfig['database'])) {
196
            $success = false;
197
            $error = 'Invalid characters in database name.';
198
        }
199
200
        return array(
201
            'success' => $success,
202
            'error' => $error
203
        );
204
    }
205
206
    /**
207
     * Determines if a given database name is a valid Silverstripe name.
208
     *
209
     * @param string $database Candidate database name
210
     * @return boolean
211
     */
212
    public function checkValidDatabaseName($database)
213
    {
214
215
        // Reject filename unsafe characters (cross platform)
216
        if (preg_match('/[\\\\\/\?%\*\:\|"\<\>\.]+/', $database)) {
217
            return false;
218
        }
219
220
        // Restricted to characters in the ASCII and Extended ASCII range
221
        // @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
222
        return preg_match('/^[\x{0001}-\x{FFFF}]+$/u', $database);
223
    }
224
225
    /**
226
     * Checks if a specified grant proves that the current user has the specified
227
     * permission on the specified database
228
     *
229
     * @param string $database Database name
230
     * @param string $permission Permission to check for
231
     * @param string $grant MySQL syntax grant to check within
232
     * @return boolean
233
     */
234
    public function checkDatabasePermissionGrant($database, $permission, $grant)
235
    {
236
        // Filter out invalid database names
237
        if (!$this->checkValidDatabaseName($database)) {
238
            return false;
239
        }
240
241
        // Escape all valid database patterns (permission must exist on all tables)
242
        $sqlDatabase = addcslashes($database, '_%'); // See http://dev.mysql.com/doc/refman/5.7/en/string-literals.html
243
        $dbPattern = sprintf(
244
            '((%s)|(%s)|(%s)|(%s))',
245
            preg_quote("\"$sqlDatabase\".*"), // Regexp escape sql-escaped db identifier
246
            preg_quote("\"$database\".*"),
247
            preg_quote('"%".*'),
248
            preg_quote('*.*')
249
        );
250
        $expression = '/GRANT[ ,\w]+((ALL PRIVILEGES)|(' . $permission . '(?! ((VIEW)|(ROUTINE)))))[ ,\w]+ON ' . $dbPattern . '/i';
251
        return preg_match($expression, $grant);
252
    }
253
254
    /**
255
     * Checks if the current user has the specified permission on the specified database
256
     *
257
     * @param mixed $conn Connection object
258
     * @param string $database Database name
259
     * @param string $permission Permission to check
260
     * @return boolean
261
     */
262
    public function checkDatabasePermission($conn, $database, $permission)
263
    {
264
        $grants = $this->column($conn->query("SHOW GRANTS FOR CURRENT_USER"));
265
        foreach ($grants as $grant) {
266
            if ($this->checkDatabasePermissionGrant($database, $permission, $grant)) {
267
                return true;
268
            }
269
        }
270
        return false;
271
    }
272
273
    public function requireDatabaseOrCreatePermissions($databaseConfig)
274
    {
275
        $success = false;
276
        $alreadyExists = false;
277
        $conn = $this->createConnection($databaseConfig, $error);
278
        if ($conn) {
279
            $list = $this->column($conn->query("SHOW DATABASES"));
280
            if (in_array($databaseConfig['database'], $list)) {
281
                $success = true;
282
                $alreadyExists = true;
283
            } else {
284
                // If no database exists then check DDL permissions
285
                $alreadyExists = false;
286
                $success = $this->checkDatabasePermission($conn, $databaseConfig['database'], 'CREATE');
287
            }
288
        }
289
290
        return array(
291
            'success' => $success,
292
            'alreadyExists' => $alreadyExists
293
        );
294
    }
295
296
    public function requireDatabaseAlterPermissions($databaseConfig)
297
    {
298
        $conn = $this->createConnection($databaseConfig, $error);
299
        $success = $this->checkDatabasePermission($conn, $databaseConfig['database'], 'ALTER');
300
        return array(
301
            'success' => $success,
302
            'applies' => true
303
        );
304
    }
305
}
306