Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 229 |
|
0.00% |
0 / 21 |
CRAP | |
0.00% |
0 / 1 |
MysqlInstaller | |
0.00% |
0 / 229 |
|
0.00% |
0 / 21 |
4160 | |
0.00% |
0 / 1 |
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isCompiled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConnectForm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSettingsForm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
meetsMinimumRequirement | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
openConnection | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
preUpgrade | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
132 | |||
escapeLikeInternal | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getEngines | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getCharsets | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canCreateAccounts | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
156 | |||
likeToRegex | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
preInstall | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setupDatabase | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
databaseExists | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setupUser | |
0.00% |
0 / 64 |
|
0.00% |
0 / 1 |
156 | |||
buildFullUserName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
userDefinitelyExists | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getTableOptions | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getSchemaVars | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getLocalSettings | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * MySQL-specific installer. |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | * http://www.gnu.org/copyleft/gpl.html |
20 | * |
21 | * @file |
22 | * @ingroup Installer |
23 | */ |
24 | |
25 | namespace MediaWiki\Installer; |
26 | |
27 | use MediaWiki\Status\Status; |
28 | use Wikimedia\Rdbms\DatabaseFactory; |
29 | use Wikimedia\Rdbms\DatabaseMySQL; |
30 | use Wikimedia\Rdbms\DBConnectionError; |
31 | use Wikimedia\Rdbms\DBQueryError; |
32 | use Wikimedia\Rdbms\IDatabase; |
33 | |
34 | /** |
35 | * Class for setting up the MediaWiki database using MySQL. |
36 | * |
37 | * @ingroup Installer |
38 | * @since 1.17 |
39 | */ |
40 | class MysqlInstaller extends DatabaseInstaller { |
41 | |
42 | protected $globalNames = [ |
43 | 'wgDBserver', |
44 | 'wgDBname', |
45 | 'wgDBuser', |
46 | 'wgDBpassword', |
47 | 'wgDBssl', |
48 | 'wgDBprefix', |
49 | 'wgDBTableOptions', |
50 | ]; |
51 | |
52 | protected $internalDefaults = [ |
53 | '_MysqlEngine' => 'InnoDB', |
54 | '_MysqlCharset' => 'binary', |
55 | '_InstallUser' => 'root', |
56 | ]; |
57 | |
58 | public $supportedEngines = [ 'InnoDB' ]; |
59 | |
60 | private const MIN_VERSIONS = [ |
61 | 'MySQL' => '5.7.0', |
62 | 'MariaDB' => '10.3', |
63 | ]; |
64 | public static $minimumVersion; |
65 | protected static $notMinimumVersionMessage; |
66 | |
67 | public $webUserPrivs = [ |
68 | 'DELETE', |
69 | 'INSERT', |
70 | 'SELECT', |
71 | 'UPDATE', |
72 | 'CREATE TEMPORARY TABLES', |
73 | ]; |
74 | |
75 | /** |
76 | * @return string |
77 | */ |
78 | public function getName() { |
79 | return 'mysql'; |
80 | } |
81 | |
82 | /** |
83 | * @return bool |
84 | */ |
85 | public function isCompiled() { |
86 | return self::checkExtension( 'mysqli' ); |
87 | } |
88 | |
89 | public function getConnectForm( WebInstaller $webInstaller ): DatabaseConnectForm { |
90 | return new MysqlConnectForm( $webInstaller, $this ); |
91 | } |
92 | |
93 | public function getSettingsForm( WebInstaller $webInstaller ): DatabaseSettingsForm { |
94 | return new MysqlSettingsForm( $webInstaller, $this ); |
95 | } |
96 | |
97 | public static function meetsMinimumRequirement( IDatabase $conn ) { |
98 | $type = str_contains( $conn->getSoftwareLink(), 'MariaDB' ) ? 'MariaDB' : 'MySQL'; |
99 | self::$minimumVersion = self::MIN_VERSIONS[$type]; |
100 | // Used messages: config-mysql-old, config-mariadb-old |
101 | self::$notMinimumVersionMessage = 'config-' . strtolower( $type ) . '-old'; |
102 | return parent::meetsMinimumRequirement( $conn ); |
103 | } |
104 | |
105 | /** |
106 | * @return ConnectionStatus |
107 | */ |
108 | public function openConnection() { |
109 | $status = new ConnectionStatus; |
110 | try { |
111 | /** @var DatabaseMySQL $db */ |
112 | $db = ( new DatabaseFactory() )->create( 'mysql', [ |
113 | 'host' => $this->getVar( 'wgDBserver' ), |
114 | 'user' => $this->getVar( '_InstallUser' ), |
115 | 'password' => $this->getVar( '_InstallPassword' ), |
116 | 'ssl' => $this->getVar( 'wgDBssl' ), |
117 | 'dbname' => false, |
118 | 'flags' => 0, |
119 | 'tablePrefix' => $this->getVar( 'wgDBprefix' ) ] ); |
120 | $status->setDB( $db ); |
121 | } catch ( DBConnectionError $e ) { |
122 | $status->fatal( 'config-connection-error', $e->getMessage() ); |
123 | } |
124 | |
125 | return $status; |
126 | } |
127 | |
128 | public function preUpgrade() { |
129 | global $wgDBuser, $wgDBpassword; |
130 | |
131 | $status = $this->getConnection(); |
132 | if ( !$status->isOK() ) { |
133 | $this->parent->showStatusMessage( $status ); |
134 | |
135 | return; |
136 | } |
137 | $conn = $status->getDB(); |
138 | $this->selectDatabase( $conn, $this->getVar( 'wgDBname' ) ); |
139 | # Determine existing default character set |
140 | if ( $conn->tableExists( "revision", __METHOD__ ) ) { |
141 | $revision = $this->escapeLikeInternal( $this->getVar( 'wgDBprefix' ) . 'revision', '\\' ); |
142 | $res = $conn->query( "SHOW TABLE STATUS LIKE '$revision'", __METHOD__ ); |
143 | $row = $res->fetchObject(); |
144 | if ( !$row ) { |
145 | $this->parent->showMessage( 'config-show-table-status' ); |
146 | $existingSchema = false; |
147 | $existingEngine = false; |
148 | } else { |
149 | if ( preg_match( '/^latin1/', $row->Collation ) ) { |
150 | $existingSchema = 'latin1'; |
151 | } elseif ( preg_match( '/^utf8/', $row->Collation ) ) { |
152 | $existingSchema = 'utf8'; |
153 | } elseif ( preg_match( '/^binary/', $row->Collation ) ) { |
154 | $existingSchema = 'binary'; |
155 | } else { |
156 | $existingSchema = false; |
157 | $this->parent->showMessage( 'config-unknown-collation' ); |
158 | } |
159 | $existingEngine = $row->Engine ?? $row->Type; |
160 | } |
161 | } else { |
162 | $existingSchema = false; |
163 | $existingEngine = false; |
164 | } |
165 | |
166 | if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) { |
167 | $this->setVar( '_MysqlCharset', $existingSchema ); |
168 | } |
169 | if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) { |
170 | $this->setVar( '_MysqlEngine', $existingEngine ); |
171 | } |
172 | |
173 | # Normal user and password are selected after this step, so for now |
174 | # just copy these two |
175 | $wgDBuser = $this->getVar( '_InstallUser' ); |
176 | $wgDBpassword = $this->getVar( '_InstallPassword' ); |
177 | } |
178 | |
179 | /** |
180 | * @param string $s |
181 | * @param string $escapeChar |
182 | * @return string |
183 | */ |
184 | protected function escapeLikeInternal( $s, $escapeChar = '`' ) { |
185 | return str_replace( [ $escapeChar, '%', '_' ], |
186 | [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ], |
187 | $s ); |
188 | } |
189 | |
190 | /** |
191 | * Get a list of storage engines that are available and supported |
192 | * |
193 | * @return array |
194 | */ |
195 | public function getEngines() { |
196 | $status = $this->getConnection(); |
197 | $conn = $status->getDB(); |
198 | |
199 | $engines = []; |
200 | $res = $conn->query( 'SHOW ENGINES', __METHOD__ ); |
201 | foreach ( $res as $row ) { |
202 | if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) { |
203 | $engines[] = $row->Engine; |
204 | } |
205 | } |
206 | $engines = array_intersect( $this->supportedEngines, $engines ); |
207 | |
208 | return $engines; |
209 | } |
210 | |
211 | /** |
212 | * Get a list of character sets that are available and supported |
213 | * |
214 | * @return array |
215 | */ |
216 | public function getCharsets() { |
217 | return [ 'binary', 'utf8' ]; |
218 | } |
219 | |
220 | /** |
221 | * Return true if the install user can create accounts |
222 | * |
223 | * @return bool |
224 | */ |
225 | public function canCreateAccounts() { |
226 | $status = $this->getConnection(); |
227 | if ( !$status->isOK() ) { |
228 | return false; |
229 | } |
230 | $conn = $status->getDB(); |
231 | |
232 | // Get current account name |
233 | $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ ); |
234 | $parts = explode( '@', $currentName ); |
235 | if ( count( $parts ) != 2 ) { |
236 | return false; |
237 | } |
238 | $quotedUser = $conn->addQuotes( $parts[0] ) . |
239 | '@' . $conn->addQuotes( $parts[1] ); |
240 | |
241 | // The user needs to have INSERT on mysql.* to be able to CREATE USER |
242 | // The grantee will be double-quoted in this query, as required |
243 | $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*', |
244 | [ 'GRANTEE' => $quotedUser ], __METHOD__ ); |
245 | $insertMysql = false; |
246 | $grantOptions = array_fill_keys( $this->webUserPrivs, true ); |
247 | foreach ( $res as $row ) { |
248 | if ( $row->PRIVILEGE_TYPE == 'INSERT' ) { |
249 | $insertMysql = true; |
250 | } |
251 | if ( $row->IS_GRANTABLE ) { |
252 | unset( $grantOptions[$row->PRIVILEGE_TYPE] ); |
253 | } |
254 | } |
255 | |
256 | // Check for DB-specific privs for mysql.* |
257 | if ( !$insertMysql ) { |
258 | $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*', |
259 | [ |
260 | 'GRANTEE' => $quotedUser, |
261 | 'TABLE_SCHEMA' => 'mysql', |
262 | 'PRIVILEGE_TYPE' => 'INSERT', |
263 | ], __METHOD__ ); |
264 | if ( $row ) { |
265 | $insertMysql = true; |
266 | } |
267 | } |
268 | |
269 | if ( !$insertMysql ) { |
270 | return false; |
271 | } |
272 | |
273 | // Check for DB-level grant options |
274 | $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*', |
275 | [ |
276 | 'GRANTEE' => $quotedUser, |
277 | 'IS_GRANTABLE' => 1, |
278 | ], __METHOD__ ); |
279 | foreach ( $res as $row ) { |
280 | $regex = $this->likeToRegex( $row->TABLE_SCHEMA ); |
281 | if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) { |
282 | unset( $grantOptions[$row->PRIVILEGE_TYPE] ); |
283 | } |
284 | } |
285 | if ( count( $grantOptions ) ) { |
286 | // Can't grant everything |
287 | return false; |
288 | } |
289 | |
290 | return true; |
291 | } |
292 | |
293 | /** |
294 | * Convert a wildcard (as used in LIKE) to a regex |
295 | * Slashes are escaped, slash terminators included |
296 | * @param string $wildcard |
297 | * @return string |
298 | */ |
299 | protected function likeToRegex( $wildcard ) { |
300 | $r = preg_quote( $wildcard, '/' ); |
301 | $r = strtr( $r, [ |
302 | '%' => '.*', |
303 | '_' => '.' |
304 | ] ); |
305 | return "/$r/s"; |
306 | } |
307 | |
308 | public function preInstall() { |
309 | # Add our user callback to installSteps, right before the tables are created. |
310 | $callback = [ |
311 | 'name' => 'user', |
312 | 'callback' => [ $this, 'setupUser' ], |
313 | ]; |
314 | $this->parent->addInstallStep( $callback, 'tables' ); |
315 | } |
316 | |
317 | /** |
318 | * @return Status |
319 | */ |
320 | public function setupDatabase() { |
321 | $status = $this->getConnection(); |
322 | if ( !$status->isOK() ) { |
323 | return $status; |
324 | } |
325 | $conn = $status->getDB(); |
326 | $dbName = $this->getVar( 'wgDBname' ); |
327 | if ( !$this->databaseExists( $dbName ) ) { |
328 | $conn->query( |
329 | "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ) . "CHARACTER SET utf8", |
330 | __METHOD__ |
331 | ); |
332 | } |
333 | $this->selectDatabase( $conn, $dbName ); |
334 | $this->setupSchemaVars(); |
335 | |
336 | return $status; |
337 | } |
338 | |
339 | /** |
340 | * Try to see if a given database exists |
341 | * @param string $dbName Database name to check |
342 | * @return bool |
343 | */ |
344 | private function databaseExists( $dbName ) { |
345 | $encDatabase = $this->db->addQuotes( $dbName ); |
346 | |
347 | return $this->db->query( |
348 | "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = $encDatabase", |
349 | __METHOD__ |
350 | )->numRows() > 0; |
351 | } |
352 | |
353 | /** |
354 | * @return Status |
355 | */ |
356 | public function setupUser() { |
357 | $dbUser = $this->getVar( 'wgDBuser' ); |
358 | if ( $dbUser == $this->getVar( '_InstallUser' ) ) { |
359 | return Status::newGood(); |
360 | } |
361 | $status = $this->getConnection(); |
362 | if ( !$status->isOK() ) { |
363 | return $status; |
364 | } |
365 | |
366 | $this->setupSchemaVars(); |
367 | $dbName = $this->getVar( 'wgDBname' ); |
368 | $this->selectDatabase( $this->db, $dbName ); |
369 | $server = $this->getVar( 'wgDBserver' ); |
370 | $password = $this->getVar( 'wgDBpassword' ); |
371 | $grantableNames = []; |
372 | |
373 | if ( $this->getVar( '_CreateDBAccount' ) ) { |
374 | // Before we blindly try to create a user that already has access, |
375 | try { // first attempt to connect to the database |
376 | ( new DatabaseFactory() )->create( 'mysql', [ |
377 | 'host' => $server, |
378 | 'user' => $dbUser, |
379 | 'password' => $password, |
380 | 'ssl' => $this->getVar( 'wgDBssl' ), |
381 | 'dbname' => false, |
382 | 'flags' => 0, |
383 | 'tablePrefix' => $this->getVar( 'wgDBprefix' ) |
384 | ] ); |
385 | $grantableNames[] = $this->buildFullUserName( $dbUser, $server ); |
386 | $tryToCreate = false; |
387 | } catch ( DBConnectionError $e ) { |
388 | $tryToCreate = true; |
389 | } |
390 | } else { |
391 | $grantableNames[] = $this->buildFullUserName( $dbUser, $server ); |
392 | $tryToCreate = false; |
393 | } |
394 | |
395 | if ( $tryToCreate ) { |
396 | $createHostList = [ |
397 | $server, |
398 | 'localhost', |
399 | 'localhost.localdomain', |
400 | '%' |
401 | ]; |
402 | |
403 | $createHostList = array_unique( $createHostList ); |
404 | $escPass = $this->db->addQuotes( $password ); |
405 | |
406 | foreach ( $createHostList as $host ) { |
407 | $fullName = $this->buildFullUserName( $dbUser, $host ); |
408 | if ( !$this->userDefinitelyExists( $host, $dbUser ) ) { |
409 | try { |
410 | $this->db->begin( __METHOD__ ); |
411 | $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ ); |
412 | $this->db->commit( __METHOD__ ); |
413 | $grantableNames[] = $fullName; |
414 | } catch ( DBQueryError $dqe ) { |
415 | if ( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) { |
416 | // User (probably) already exists |
417 | $this->db->rollback( __METHOD__ ); |
418 | $status->warning( 'config-install-user-alreadyexists', $dbUser ); |
419 | $grantableNames[] = $fullName; |
420 | break; |
421 | } else { |
422 | // If we couldn't create for some bizarre reason and the |
423 | // user probably doesn't exist, skip the grant |
424 | $this->db->rollback( __METHOD__ ); |
425 | $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getMessage() ); |
426 | } |
427 | } |
428 | } else { |
429 | $status->warning( 'config-install-user-alreadyexists', $dbUser ); |
430 | $grantableNames[] = $fullName; |
431 | break; |
432 | } |
433 | } |
434 | } |
435 | |
436 | // Try to grant to all the users we know exist or we were able to create |
437 | $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*'; |
438 | foreach ( $grantableNames as $name ) { |
439 | try { |
440 | $this->db->begin( __METHOD__ ); |
441 | $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ ); |
442 | $this->db->commit( __METHOD__ ); |
443 | } catch ( DBQueryError $dqe ) { |
444 | $this->db->rollback( __METHOD__ ); |
445 | $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getMessage() ); |
446 | } |
447 | } |
448 | |
449 | return $status; |
450 | } |
451 | |
452 | /** |
453 | * Return a formal 'User'@'Host' username for use in queries |
454 | * @param string $name Username, quotes will be added |
455 | * @param string $host Hostname, quotes will be added |
456 | * @return string |
457 | */ |
458 | private function buildFullUserName( $name, $host ) { |
459 | return $this->db->addQuotes( $name ) . '@' . $this->db->addQuotes( $host ); |
460 | } |
461 | |
462 | /** |
463 | * Try to see if the user account exists. Our "superuser" may not have |
464 | * access to mysql.user, so false means "no" or "maybe" |
465 | * @param string $host Hostname to check |
466 | * @param string $user Username to check |
467 | * @return bool |
468 | */ |
469 | private function userDefinitelyExists( $host, $user ) { |
470 | try { |
471 | $res = $this->db->newSelectQueryBuilder() |
472 | ->select( [ 'Host', 'User' ] ) |
473 | ->from( 'mysql.user' ) |
474 | ->where( [ 'Host' => $host, 'User' => $user ] ) |
475 | ->caller( __METHOD__ )->fetchRow(); |
476 | |
477 | return (bool)$res; |
478 | } catch ( DBQueryError $dqe ) { |
479 | return false; |
480 | } |
481 | } |
482 | |
483 | /** |
484 | * Return any table options to be applied to all tables that don't |
485 | * override them. |
486 | * |
487 | * @return string |
488 | */ |
489 | protected function getTableOptions() { |
490 | $options = []; |
491 | if ( $this->getVar( '_MysqlEngine' ) !== null ) { |
492 | $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' ); |
493 | } |
494 | if ( $this->getVar( '_MysqlCharset' ) !== null ) { |
495 | $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' ); |
496 | } |
497 | |
498 | return implode( ', ', $options ); |
499 | } |
500 | |
501 | /** |
502 | * Get variables to substitute into tables.sql and the SQL patch files. |
503 | * |
504 | * @return array |
505 | */ |
506 | public function getSchemaVars() { |
507 | return [ |
508 | 'wgDBTableOptions' => $this->getTableOptions(), |
509 | 'wgDBname' => $this->getVar( 'wgDBname' ), |
510 | 'wgDBuser' => $this->getVar( 'wgDBuser' ), |
511 | 'wgDBpassword' => $this->getVar( 'wgDBpassword' ), |
512 | ]; |
513 | } |
514 | |
515 | public function getLocalSettings() { |
516 | $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) ); |
517 | $useSsl = $this->getVar( 'wgDBssl' ) ? 'true' : 'false'; |
518 | $tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() ); |
519 | |
520 | return "# MySQL specific settings |
521 | \$wgDBprefix = \"{$prefix}\"; |
522 | \$wgDBssl = {$useSsl}; |
523 | |
524 | # MySQL table options to use during installation or update |
525 | \$wgDBTableOptions = \"{$tblOpts}\";"; |
526 | } |
527 | } |