MediaWiki REL1_39
MysqlInstaller.php
Go to the documentation of this file.
1<?php
29
37
38 protected $globalNames = [
39 'wgDBserver',
40 'wgDBname',
41 'wgDBuser',
42 'wgDBpassword',
43 'wgDBprefix',
44 'wgDBTableOptions',
45 ];
46
47 protected $internalDefaults = [
48 '_MysqlEngine' => 'InnoDB',
49 '_MysqlCharset' => 'binary',
50 '_InstallUser' => 'root',
51 ];
52
53 public $supportedEngines = [ 'InnoDB' ];
54
55 private const MIN_VERSIONS = [
56 'MySQL' => '5.7.0',
57 'MariaDB' => '10.3',
58 ];
59 public static $minimumVersion;
60 protected static $notMinimumVersionMessage;
61
62 public $webUserPrivs = [
63 'DELETE',
64 'INSERT',
65 'SELECT',
66 'UPDATE',
67 'CREATE TEMPORARY TABLES',
68 ];
69
73 public function getName() {
74 return 'mysql';
75 }
76
80 public function isCompiled() {
81 return self::checkExtension( 'mysqli' );
82 }
83
87 public function getConnectForm() {
88 return $this->getTextBox(
89 'wgDBserver',
90 'config-db-host',
91 [],
92 $this->parent->getHelpBox( 'config-db-host-help' )
93 ) .
94 Html::openElement( 'fieldset' ) .
95 Html::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
96 $this->getTextBox( 'wgDBname', 'config-db-name', [ 'dir' => 'ltr' ],
97 $this->parent->getHelpBox( 'config-db-name-help' ) ) .
98 $this->getTextBox( 'wgDBprefix', 'config-db-prefix', [ 'dir' => 'ltr' ],
99 $this->parent->getHelpBox( 'config-db-prefix-help' ) ) .
100 Html::closeElement( 'fieldset' ) .
101 $this->getInstallUserBox();
102 }
103
104 public function submitConnectForm() {
105 // Get variables from the request.
106 $newValues = $this->setVarsFromRequest( [ 'wgDBserver', 'wgDBname', 'wgDBprefix' ] );
107
108 // Validate them.
109 $status = Status::newGood();
110 if ( !strlen( $newValues['wgDBserver'] ) ) {
111 $status->fatal( 'config-missing-db-host' );
112 }
113 if ( !strlen( $newValues['wgDBname'] ) ) {
114 $status->fatal( 'config-missing-db-name' );
115 } elseif ( !preg_match( '/^[a-z0-9+_-]+$/i', $newValues['wgDBname'] ) ) {
116 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
117 }
118 if ( !preg_match( '/^[a-z0-9_-]*$/i', $newValues['wgDBprefix'] ) ) {
119 $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] );
120 }
121 if ( !$status->isOK() ) {
122 return $status;
123 }
124
125 // Submit user box
126 $status = $this->submitInstallUserBox();
127 if ( !$status->isOK() ) {
128 return $status;
129 }
130
131 // Try to connect
132 $status = $this->getConnection();
133 if ( !$status->isOK() ) {
134 return $status;
135 }
139 $conn = $status->value;
140 '@phan-var Database $conn';
141
142 // Check version
143 return static::meetsMinimumRequirement( $conn );
144 }
145
146 public static function meetsMinimumRequirement( IDatabase $conn ) {
147 $type = str_contains( $conn->getSoftwareLink(), 'MariaDB' ) ? 'MariaDB' : 'MySQL';
148 self::$minimumVersion = self::MIN_VERSIONS[$type];
149 // Used messages: config-mysql-old, config-mariadb-old
150 self::$notMinimumVersionMessage = 'config-' . strtolower( $type ) . '-old';
151 return parent::meetsMinimumRequirement( $conn );
152 }
153
157 public function openConnection() {
158 $status = Status::newGood();
159 try {
161 $db = Database::factory( 'mysql', [
162 'host' => $this->getVar( 'wgDBserver' ),
163 'user' => $this->getVar( '_InstallUser' ),
164 'password' => $this->getVar( '_InstallPassword' ),
165 'dbname' => false,
166 'flags' => 0,
167 'tablePrefix' => $this->getVar( 'wgDBprefix' ) ] );
168 $status->value = $db;
169 } catch ( DBConnectionError $e ) {
170 $status->fatal( 'config-connection-error', $e->getMessage() );
171 }
172
173 return $status;
174 }
175
176 public function preUpgrade() {
177 global $wgDBuser, $wgDBpassword;
178
179 $status = $this->getConnection();
180 if ( !$status->isOK() ) {
181 $this->parent->showStatusMessage( $status );
182
183 return;
184 }
188 $conn = $status->value;
189 $conn->selectDB( $this->getVar( 'wgDBname' ) );
190
191 # Determine existing default character set
192 if ( $conn->tableExists( "revision", __METHOD__ ) ) {
193 $revision = $this->escapeLikeInternal( $this->getVar( 'wgDBprefix' ) . 'revision', '\\' );
194 $res = $conn->query( "SHOW TABLE STATUS LIKE '$revision'", __METHOD__ );
195 $row = $res->fetchObject();
196 if ( !$row ) {
197 $this->parent->showMessage( 'config-show-table-status' );
198 $existingSchema = false;
199 $existingEngine = false;
200 } else {
201 if ( preg_match( '/^latin1/', $row->Collation ) ) {
202 $existingSchema = 'latin1';
203 } elseif ( preg_match( '/^utf8/', $row->Collation ) ) {
204 $existingSchema = 'utf8';
205 } elseif ( preg_match( '/^binary/', $row->Collation ) ) {
206 $existingSchema = 'binary';
207 } else {
208 $existingSchema = false;
209 $this->parent->showMessage( 'config-unknown-collation' );
210 }
211 $existingEngine = $row->Engine ?? $row->Type;
212 }
213 } else {
214 $existingSchema = false;
215 $existingEngine = false;
216 }
217
218 if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) {
219 $this->setVar( '_MysqlCharset', $existingSchema );
220 }
221 if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) {
222 $this->setVar( '_MysqlEngine', $existingEngine );
223 }
224
225 # Normal user and password are selected after this step, so for now
226 # just copy these two
227 $wgDBuser = $this->getVar( '_InstallUser' );
228 $wgDBpassword = $this->getVar( '_InstallPassword' );
229 }
230
236 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
237 return str_replace( [ $escapeChar, '%', '_' ],
238 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
239 $s );
240 }
241
247 public function getEngines() {
248 $status = $this->getConnection();
249
253 $conn = $status->value;
254
255 $engines = [];
256 $res = $conn->query( 'SHOW ENGINES', __METHOD__ );
257 foreach ( $res as $row ) {
258 if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) {
259 $engines[] = $row->Engine;
260 }
261 }
262 $engines = array_intersect( $this->supportedEngines, $engines );
263
264 return $engines;
265 }
266
272 public function getCharsets() {
273 return [ 'binary', 'utf8' ];
274 }
275
281 public function canCreateAccounts() {
282 $status = $this->getConnection();
283 if ( !$status->isOK() ) {
284 return false;
285 }
287 $conn = $status->value;
288
289 // Get current account name
290 $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ );
291 $parts = explode( '@', $currentName );
292 if ( count( $parts ) != 2 ) {
293 return false;
294 }
295 $quotedUser = $conn->addQuotes( $parts[0] ) .
296 '@' . $conn->addQuotes( $parts[1] );
297
298 // The user needs to have INSERT on mysql.* to be able to CREATE USER
299 // The grantee will be double-quoted in this query, as required
300 $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*',
301 [ 'GRANTEE' => $quotedUser ], __METHOD__ );
302 $insertMysql = false;
303 $grantOptions = array_fill_keys( $this->webUserPrivs, true );
304 foreach ( $res as $row ) {
305 if ( $row->PRIVILEGE_TYPE == 'INSERT' ) {
306 $insertMysql = true;
307 }
308 if ( $row->IS_GRANTABLE ) {
309 unset( $grantOptions[$row->PRIVILEGE_TYPE] );
310 }
311 }
312
313 // Check for DB-specific privs for mysql.*
314 if ( !$insertMysql ) {
315 $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
316 [
317 'GRANTEE' => $quotedUser,
318 'TABLE_SCHEMA' => 'mysql',
319 'PRIVILEGE_TYPE' => 'INSERT',
320 ], __METHOD__ );
321 if ( $row ) {
322 $insertMysql = true;
323 }
324 }
325
326 if ( !$insertMysql ) {
327 return false;
328 }
329
330 // Check for DB-level grant options
331 $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
332 [
333 'GRANTEE' => $quotedUser,
334 'IS_GRANTABLE' => 1,
335 ], __METHOD__ );
336 foreach ( $res as $row ) {
337 $regex = $this->likeToRegex( $row->TABLE_SCHEMA );
338 if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) {
339 unset( $grantOptions[$row->PRIVILEGE_TYPE] );
340 }
341 }
342 if ( count( $grantOptions ) ) {
343 // Can't grant everything
344 return false;
345 }
346
347 return true;
348 }
349
356 protected function likeToRegex( $wildcard ) {
357 $r = preg_quote( $wildcard, '/' );
358 $r = strtr( $r, [
359 '%' => '.*',
360 '_' => '.'
361 ] );
362 return "/$r/s";
363 }
364
368 public function getSettingsForm() {
369 if ( $this->canCreateAccounts() ) {
370 $noCreateMsg = false;
371 } else {
372 $noCreateMsg = 'config-db-web-no-create-privs';
373 }
374 $s = $this->getWebUserBox( $noCreateMsg );
375
376 // Do engine selector
377 $engines = $this->getEngines();
378 // If the current default engine is not supported, use an engine that is
379 if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
380 $this->setVar( '_MysqlEngine', reset( $engines ) );
381 }
382
383 // If the current default charset is not supported, use a charset that is
384 $charsets = $this->getCharsets();
385 if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
386 $this->setVar( '_MysqlCharset', reset( $charsets ) );
387 }
388
389 return $s;
390 }
391
395 public function submitSettingsForm() {
396 $this->setVarsFromRequest( [ '_MysqlEngine', '_MysqlCharset' ] );
397 $status = $this->submitWebUserBox();
398 if ( !$status->isOK() ) {
399 return $status;
400 }
401
402 // Validate the create checkbox
403 $canCreate = $this->canCreateAccounts();
404 if ( !$canCreate ) {
405 $this->setVar( '_CreateDBAccount', false );
406 $create = false;
407 } else {
408 $create = $this->getVar( '_CreateDBAccount' );
409 }
410
411 if ( !$create ) {
412 // Test the web account
413 try {
414 MediaWikiServices::getInstance()->getDatabaseFactory()->create( 'mysql', [
415 'host' => $this->getVar( 'wgDBserver' ),
416 'user' => $this->getVar( 'wgDBuser' ),
417 'password' => $this->getVar( 'wgDBpassword' ),
418 'dbname' => false,
419 'flags' => 0,
420 'tablePrefix' => $this->getVar( 'wgDBprefix' )
421 ] );
422 } catch ( DBConnectionError $e ) {
423 return Status::newFatal( 'config-connection-error', $e->getMessage() );
424 }
425 }
426
427 // Validate engines and charsets
428 // This is done pre-submit already so it's just for security
429 $engines = $this->getEngines();
430 if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
431 $this->setVar( '_MysqlEngine', reset( $engines ) );
432 }
433 $charsets = $this->getCharsets();
434 if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
435 $this->setVar( '_MysqlCharset', reset( $charsets ) );
436 }
437
438 return Status::newGood();
439 }
440
441 public function preInstall() {
442 # Add our user callback to installSteps, right before the tables are created.
443 $callback = [
444 'name' => 'user',
445 'callback' => [ $this, 'setupUser' ],
446 ];
447 $this->parent->addInstallStep( $callback, 'tables' );
448 }
449
453 public function setupDatabase() {
454 $status = $this->getConnection();
455 if ( !$status->isOK() ) {
456 return $status;
457 }
459 $conn = $status->value;
460 $dbName = $this->getVar( 'wgDBname' );
461 if ( !$this->databaseExists( $dbName ) ) {
462 $conn->query(
463 "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ) . "CHARACTER SET utf8",
464 __METHOD__
465 );
466 }
467 $conn->selectDB( $dbName );
468 $this->setupSchemaVars();
469
470 return $status;
471 }
472
478 private function databaseExists( $dbName ) {
479 $encDatabase = $this->db->addQuotes( $dbName );
480
481 return $this->db->query(
482 "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = $encDatabase",
483 __METHOD__
484 )->numRows() > 0;
485 }
486
490 public function setupUser() {
491 $dbUser = $this->getVar( 'wgDBuser' );
492 if ( $dbUser == $this->getVar( '_InstallUser' ) ) {
493 return Status::newGood();
494 }
495 $status = $this->getConnection();
496 if ( !$status->isOK() ) {
497 return $status;
498 }
499
500 $this->setupSchemaVars();
501 $dbName = $this->getVar( 'wgDBname' );
502 $this->db->selectDB( $dbName );
503 $server = $this->getVar( 'wgDBserver' );
504 $password = $this->getVar( 'wgDBpassword' );
505 $grantableNames = [];
506
507 if ( $this->getVar( '_CreateDBAccount' ) ) {
508 // Before we blindly try to create a user that already has access,
509 try { // first attempt to connect to the database
510 Database::factory( 'mysql', [
511 'host' => $server,
512 'user' => $dbUser,
513 'password' => $password,
514 'dbname' => false,
515 'flags' => 0,
516 'tablePrefix' => $this->getVar( 'wgDBprefix' )
517 ] );
518 $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
519 $tryToCreate = false;
520 } catch ( DBConnectionError $e ) {
521 $tryToCreate = true;
522 }
523 } else {
524 $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
525 $tryToCreate = false;
526 }
527
528 if ( $tryToCreate ) {
529 $createHostList = [
530 $server,
531 'localhost',
532 'localhost.localdomain',
533 '%'
534 ];
535
536 $createHostList = array_unique( $createHostList );
537 $escPass = $this->db->addQuotes( $password );
538
539 foreach ( $createHostList as $host ) {
540 $fullName = $this->buildFullUserName( $dbUser, $host );
541 if ( !$this->userDefinitelyExists( $host, $dbUser ) ) {
542 try {
543 $this->db->begin( __METHOD__ );
544 $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
545 $this->db->commit( __METHOD__ );
546 $grantableNames[] = $fullName;
547 } catch ( DBQueryError $dqe ) {
548 if ( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
549 // User (probably) already exists
550 $this->db->rollback( __METHOD__ );
551 $status->warning( 'config-install-user-alreadyexists', $dbUser );
552 $grantableNames[] = $fullName;
553 break;
554 } else {
555 // If we couldn't create for some bizarre reason and the
556 // user probably doesn't exist, skip the grant
557 $this->db->rollback( __METHOD__ );
558 $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getMessage() );
559 }
560 }
561 } else {
562 $status->warning( 'config-install-user-alreadyexists', $dbUser );
563 $grantableNames[] = $fullName;
564 break;
565 }
566 }
567 }
568
569 // Try to grant to all the users we know exist or we were able to create
570 $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*';
571 foreach ( $grantableNames as $name ) {
572 try {
573 $this->db->begin( __METHOD__ );
574 $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
575 $this->db->commit( __METHOD__ );
576 } catch ( DBQueryError $dqe ) {
577 $this->db->rollback( __METHOD__ );
578 $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getMessage() );
579 }
580 }
581
582 return $status;
583 }
584
591 private function buildFullUserName( $name, $host ) {
592 return $this->db->addQuotes( $name ) . '@' . $this->db->addQuotes( $host );
593 }
594
602 private function userDefinitelyExists( $host, $user ) {
603 try {
604 $res = $this->db->selectRow( 'mysql.user', [ 'Host', 'User' ],
605 [ 'Host' => $host, 'User' => $user ], __METHOD__ );
606
607 return (bool)$res;
608 } catch ( DBQueryError $dqe ) {
609 return false;
610 }
611 }
612
619 protected function getTableOptions() {
620 $options = [];
621 if ( $this->getVar( '_MysqlEngine' ) !== null ) {
622 $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' );
623 }
624 if ( $this->getVar( '_MysqlCharset' ) !== null ) {
625 $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' );
626 }
627
628 return implode( ', ', $options );
629 }
630
636 public function getSchemaVars() {
637 return [
638 'wgDBTableOptions' => $this->getTableOptions(),
639 'wgDBname' => $this->getVar( 'wgDBname' ),
640 'wgDBuser' => $this->getVar( 'wgDBuser' ),
641 'wgDBpassword' => $this->getVar( 'wgDBpassword' ),
642 ];
643 }
644
645 public function getLocalSettings() {
646 $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
647 $tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() );
648
649 return "# MySQL specific settings
650\$wgDBprefix = \"{$prefix}\";
651
652# MySQL table options to use during installation or update
653\$wgDBTableOptions = \"{$tblOpts}\";";
654 }
655}
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Base class for DBMS-specific installation helper classes.
getWebUserBox( $noCreateMsg=false)
Get a standard web-user fieldset.
submitWebUserBox()
Submit the form from getWebUserBox().
static checkExtension( $name)
Convenience function.
Database $db
The database connection.
setVarsFromRequest( $varNames)
Convenience function to set variables based on form data.
getConnection()
Connect to the database using the administrative user/password currently defined in the session.
getVar( $var, $default=null)
Get a variable, taking local defaults into account.
getTextBox( $var, $label, $attribs=[], $helpData="")
Get a labelled text box to configure a local variable.
setVar( $name, $value)
Convenience alias for $this->parent->setVar()
submitInstallUserBox()
Submit a standard install user fieldset.
getInstallUserBox()
Get a standard install-user fieldset.
setupSchemaVars()
Set appropriate schema variables in the current database connection.
Service locator for MediaWiki core services.
Class for setting up the MediaWiki database using MySQL.
likeToRegex( $wildcard)
Convert a wildcard (as used in LIKE) to a regex Slashes are escaped, slash terminators included.
escapeLikeInternal( $s, $escapeChar='`')
static meetsMinimumRequirement(IDatabase $conn)
Whether the provided version meets the necessary requirements for this type.
getSchemaVars()
Get variables to substitute into tables.sql and the SQL patch files.
preInstall()
Allow DB installers a chance to make last-minute changes before installation occurs.
getEngines()
Get a list of storage engines that are available and supported.
canCreateAccounts()
Return true if the install user can create accounts.
preUpgrade()
Allow DB installers a chance to make checks before upgrade.
getTableOptions()
Return any table options to be applied to all tables that don't override them.
getCharsets()
Get a list of character sets that are available and supported.
static $notMinimumVersionMessage
submitConnectForm()
Set variables based on the request array, assuming it was submitted via the form returned by getConne...
getLocalSettings()
Get the DBMS-specific options for LocalSettings.php generation.
$wgDBuser
Config variable stub for the DBuser setting, for use by phpdoc and IDEs.
$wgDBpassword
Config variable stub for the DBpassword setting, for use by phpdoc and IDEs.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
getSoftwareLink()
Returns a wikitext style link to the DB's website (e.g.
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s