MediaWiki  master
MysqlInstaller.php
Go to the documentation of this file.
1 <?php
32 
40 
41  protected $globalNames = [
42  'wgDBserver',
43  'wgDBname',
44  'wgDBuser',
45  'wgDBpassword',
46  'wgDBssl',
47  'wgDBprefix',
48  'wgDBTableOptions',
49  ];
50 
51  protected $internalDefaults = [
52  '_MysqlEngine' => 'InnoDB',
53  '_MysqlCharset' => 'binary',
54  '_InstallUser' => 'root',
55  ];
56 
57  public $supportedEngines = [ 'InnoDB' ];
58 
59  private const MIN_VERSIONS = [
60  'MySQL' => '5.7.0',
61  'MariaDB' => '10.3',
62  ];
63  public static $minimumVersion;
64  protected static $notMinimumVersionMessage;
65 
66  public $webUserPrivs = [
67  'DELETE',
68  'INSERT',
69  'SELECT',
70  'UPDATE',
71  'CREATE TEMPORARY TABLES',
72  ];
73 
77  public function getName() {
78  return 'mysql';
79  }
80 
84  public function isCompiled() {
85  return self::checkExtension( 'mysqli' );
86  }
87 
91  public function getConnectForm() {
92  return $this->getTextBox(
93  'wgDBserver',
94  'config-db-host',
95  [],
96  $this->parent->getHelpBox( 'config-db-host-help' )
97  ) .
98  $this->getCheckBox( 'wgDBssl', 'config-db-ssl' ) .
99  Html::openElement( 'fieldset' ) .
100  Html::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
101  $this->getTextBox( 'wgDBname', 'config-db-name', [ 'dir' => 'ltr' ],
102  $this->parent->getHelpBox( 'config-db-name-help' ) ) .
103  $this->getTextBox( 'wgDBprefix', 'config-db-prefix', [ 'dir' => 'ltr' ],
104  $this->parent->getHelpBox( 'config-db-prefix-help' ) ) .
105  Html::closeElement( 'fieldset' ) .
106  $this->getInstallUserBox();
107  }
108 
109  public function submitConnectForm() {
110  // Get variables from the request.
111  $newValues = $this->setVarsFromRequest( [ 'wgDBserver', 'wgDBname', 'wgDBprefix', 'wgDBssl' ] );
112 
113  // Validate them.
114  $status = Status::newGood();
115  if ( !strlen( $newValues['wgDBserver'] ) ) {
116  $status->fatal( 'config-missing-db-host' );
117  }
118  if ( !strlen( $newValues['wgDBname'] ) ) {
119  $status->fatal( 'config-missing-db-name' );
120  } elseif ( !preg_match( '/^[a-z0-9+_-]+$/i', $newValues['wgDBname'] ) ) {
121  $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
122  }
123  if ( !preg_match( '/^[a-z0-9_-]*$/i', $newValues['wgDBprefix'] ) ) {
124  $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] );
125  }
126  if ( !$status->isOK() ) {
127  return $status;
128  }
129 
130  // Submit user box
131  $status = $this->submitInstallUserBox();
132  if ( !$status->isOK() ) {
133  return $status;
134  }
135 
136  // Try to connect
137  $status = $this->getConnection();
138  if ( !$status->isOK() ) {
139  return $status;
140  }
144  $conn = $status->value;
145  '@phan-var Database $conn';
146 
147  // Check version
148  return static::meetsMinimumRequirement( $conn );
149  }
150 
151  public static function meetsMinimumRequirement( IDatabase $conn ) {
152  $type = str_contains( $conn->getSoftwareLink(), 'MariaDB' ) ? 'MariaDB' : 'MySQL';
153  self::$minimumVersion = self::MIN_VERSIONS[$type];
154  // Used messages: config-mysql-old, config-mariadb-old
155  self::$notMinimumVersionMessage = 'config-' . strtolower( $type ) . '-old';
156  return parent::meetsMinimumRequirement( $conn );
157  }
158 
162  public function openConnection() {
163  $status = Status::newGood();
164  try {
166  $db = ( new DatabaseFactory() )->create( 'mysql', [
167  'host' => $this->getVar( 'wgDBserver' ),
168  'user' => $this->getVar( '_InstallUser' ),
169  'password' => $this->getVar( '_InstallPassword' ),
170  'ssl' => $this->getVar( 'wgDBssl' ),
171  'dbname' => false,
172  'flags' => 0,
173  'tablePrefix' => $this->getVar( 'wgDBprefix' ) ] );
174  $status->value = $db;
175  } catch ( DBConnectionError $e ) {
176  $status->fatal( 'config-connection-error', $e->getMessage() );
177  }
178 
179  return $status;
180  }
181 
182  public function preUpgrade() {
183  global $wgDBuser, $wgDBpassword;
184 
185  $status = $this->getConnection();
186  if ( !$status->isOK() ) {
187  $this->parent->showStatusMessage( $status );
188 
189  return;
190  }
194  $conn = $status->value;
195  $this->selectDatabase( $conn, $this->getVar( 'wgDBname' ) );
196  # Determine existing default character set
197  if ( $conn->tableExists( "revision", __METHOD__ ) ) {
198  $revision = $this->escapeLikeInternal( $this->getVar( 'wgDBprefix' ) . 'revision', '\\' );
199  $res = $conn->query( "SHOW TABLE STATUS LIKE '$revision'", __METHOD__ );
200  $row = $res->fetchObject();
201  if ( !$row ) {
202  $this->parent->showMessage( 'config-show-table-status' );
203  $existingSchema = false;
204  $existingEngine = false;
205  } else {
206  if ( preg_match( '/^latin1/', $row->Collation ) ) {
207  $existingSchema = 'latin1';
208  } elseif ( preg_match( '/^utf8/', $row->Collation ) ) {
209  $existingSchema = 'utf8';
210  } elseif ( preg_match( '/^binary/', $row->Collation ) ) {
211  $existingSchema = 'binary';
212  } else {
213  $existingSchema = false;
214  $this->parent->showMessage( 'config-unknown-collation' );
215  }
216  $existingEngine = $row->Engine ?? $row->Type;
217  }
218  } else {
219  $existingSchema = false;
220  $existingEngine = false;
221  }
222 
223  if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) {
224  $this->setVar( '_MysqlCharset', $existingSchema );
225  }
226  if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) {
227  $this->setVar( '_MysqlEngine', $existingEngine );
228  }
229 
230  # Normal user and password are selected after this step, so for now
231  # just copy these two
232  $wgDBuser = $this->getVar( '_InstallUser' );
233  $wgDBpassword = $this->getVar( '_InstallPassword' );
234  }
235 
241  protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
242  return str_replace( [ $escapeChar, '%', '_' ],
243  [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
244  $s );
245  }
246 
252  public function getEngines() {
253  $status = $this->getConnection();
254 
258  $conn = $status->value;
259 
260  $engines = [];
261  $res = $conn->query( 'SHOW ENGINES', __METHOD__ );
262  foreach ( $res as $row ) {
263  if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) {
264  $engines[] = $row->Engine;
265  }
266  }
267  $engines = array_intersect( $this->supportedEngines, $engines );
268 
269  return $engines;
270  }
271 
277  public function getCharsets() {
278  return [ 'binary', 'utf8' ];
279  }
280 
286  public function canCreateAccounts() {
287  $status = $this->getConnection();
288  if ( !$status->isOK() ) {
289  return false;
290  }
292  $conn = $status->value;
293 
294  // Get current account name
295  $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ );
296  $parts = explode( '@', $currentName );
297  if ( count( $parts ) != 2 ) {
298  return false;
299  }
300  $quotedUser = $conn->addQuotes( $parts[0] ) .
301  '@' . $conn->addQuotes( $parts[1] );
302 
303  // The user needs to have INSERT on mysql.* to be able to CREATE USER
304  // The grantee will be double-quoted in this query, as required
305  $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*',
306  [ 'GRANTEE' => $quotedUser ], __METHOD__ );
307  $insertMysql = false;
308  $grantOptions = array_fill_keys( $this->webUserPrivs, true );
309  foreach ( $res as $row ) {
310  if ( $row->PRIVILEGE_TYPE == 'INSERT' ) {
311  $insertMysql = true;
312  }
313  if ( $row->IS_GRANTABLE ) {
314  unset( $grantOptions[$row->PRIVILEGE_TYPE] );
315  }
316  }
317 
318  // Check for DB-specific privs for mysql.*
319  if ( !$insertMysql ) {
320  $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
321  [
322  'GRANTEE' => $quotedUser,
323  'TABLE_SCHEMA' => 'mysql',
324  'PRIVILEGE_TYPE' => 'INSERT',
325  ], __METHOD__ );
326  if ( $row ) {
327  $insertMysql = true;
328  }
329  }
330 
331  if ( !$insertMysql ) {
332  return false;
333  }
334 
335  // Check for DB-level grant options
336  $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
337  [
338  'GRANTEE' => $quotedUser,
339  'IS_GRANTABLE' => 1,
340  ], __METHOD__ );
341  foreach ( $res as $row ) {
342  $regex = $this->likeToRegex( $row->TABLE_SCHEMA );
343  if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) {
344  unset( $grantOptions[$row->PRIVILEGE_TYPE] );
345  }
346  }
347  if ( count( $grantOptions ) ) {
348  // Can't grant everything
349  return false;
350  }
351 
352  return true;
353  }
354 
361  protected function likeToRegex( $wildcard ) {
362  $r = preg_quote( $wildcard, '/' );
363  $r = strtr( $r, [
364  '%' => '.*',
365  '_' => '.'
366  ] );
367  return "/$r/s";
368  }
369 
373  public function getSettingsForm() {
374  if ( $this->canCreateAccounts() ) {
375  $noCreateMsg = false;
376  } else {
377  $noCreateMsg = 'config-db-web-no-create-privs';
378  }
379  $s = $this->getWebUserBox( $noCreateMsg );
380 
381  // Do engine selector
382  $engines = $this->getEngines();
383  // If the current default engine is not supported, use an engine that is
384  if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
385  $this->setVar( '_MysqlEngine', reset( $engines ) );
386  }
387 
388  // If the current default charset is not supported, use a charset that is
389  $charsets = $this->getCharsets();
390  if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
391  $this->setVar( '_MysqlCharset', reset( $charsets ) );
392  }
393 
394  return $s;
395  }
396 
400  public function submitSettingsForm() {
401  $this->setVarsFromRequest( [ '_MysqlEngine', '_MysqlCharset' ] );
402  $status = $this->submitWebUserBox();
403  if ( !$status->isOK() ) {
404  return $status;
405  }
406 
407  // Validate the create checkbox
408  $canCreate = $this->canCreateAccounts();
409  if ( !$canCreate ) {
410  $this->setVar( '_CreateDBAccount', false );
411  $create = false;
412  } else {
413  $create = $this->getVar( '_CreateDBAccount' );
414  }
415 
416  if ( !$create ) {
417  // Test the web account
418  try {
419  MediaWikiServices::getInstance()->getDatabaseFactory()->create( 'mysql', [
420  'host' => $this->getVar( 'wgDBserver' ),
421  'user' => $this->getVar( 'wgDBuser' ),
422  'password' => $this->getVar( 'wgDBpassword' ),
423  'ssl' => $this->getVar( 'wgDBssl' ),
424  'dbname' => false,
425  'flags' => 0,
426  'tablePrefix' => $this->getVar( 'wgDBprefix' )
427  ] );
428  } catch ( DBConnectionError $e ) {
429  return Status::newFatal( 'config-connection-error', $e->getMessage() );
430  }
431  }
432 
433  // Validate engines and charsets
434  // This is done pre-submit already so it's just for security
435  $engines = $this->getEngines();
436  if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
437  $this->setVar( '_MysqlEngine', reset( $engines ) );
438  }
439  $charsets = $this->getCharsets();
440  if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
441  $this->setVar( '_MysqlCharset', reset( $charsets ) );
442  }
443 
444  return Status::newGood();
445  }
446 
447  public function preInstall() {
448  # Add our user callback to installSteps, right before the tables are created.
449  $callback = [
450  'name' => 'user',
451  'callback' => [ $this, 'setupUser' ],
452  ];
453  $this->parent->addInstallStep( $callback, 'tables' );
454  }
455 
459  public function setupDatabase() {
460  $status = $this->getConnection();
461  if ( !$status->isOK() ) {
462  return $status;
463  }
465  $conn = $status->value;
466  $dbName = $this->getVar( 'wgDBname' );
467  if ( !$this->databaseExists( $dbName ) ) {
468  $conn->query(
469  "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ) . "CHARACTER SET utf8",
470  __METHOD__
471  );
472  }
473  $this->selectDatabase( $conn, $dbName );
474  $this->setupSchemaVars();
475 
476  return $status;
477  }
478 
484  private function databaseExists( $dbName ) {
485  $encDatabase = $this->db->addQuotes( $dbName );
486 
487  return $this->db->query(
488  "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = $encDatabase",
489  __METHOD__
490  )->numRows() > 0;
491  }
492 
496  public function setupUser() {
497  $dbUser = $this->getVar( 'wgDBuser' );
498  if ( $dbUser == $this->getVar( '_InstallUser' ) ) {
499  return Status::newGood();
500  }
501  $status = $this->getConnection();
502  if ( !$status->isOK() ) {
503  return $status;
504  }
505 
506  $this->setupSchemaVars();
507  $dbName = $this->getVar( 'wgDBname' );
508  $this->selectDatabase( $this->db, $dbName );
509  $server = $this->getVar( 'wgDBserver' );
510  $password = $this->getVar( 'wgDBpassword' );
511  $grantableNames = [];
512 
513  if ( $this->getVar( '_CreateDBAccount' ) ) {
514  // Before we blindly try to create a user that already has access,
515  try { // first attempt to connect to the database
516  ( new DatabaseFactory() )->create( 'mysql', [
517  'host' => $server,
518  'user' => $dbUser,
519  'password' => $password,
520  'ssl' => $this->getVar( 'wgDBssl' ),
521  'dbname' => false,
522  'flags' => 0,
523  'tablePrefix' => $this->getVar( 'wgDBprefix' )
524  ] );
525  $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
526  $tryToCreate = false;
527  } catch ( DBConnectionError $e ) {
528  $tryToCreate = true;
529  }
530  } else {
531  $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
532  $tryToCreate = false;
533  }
534 
535  if ( $tryToCreate ) {
536  $createHostList = [
537  $server,
538  'localhost',
539  'localhost.localdomain',
540  '%'
541  ];
542 
543  $createHostList = array_unique( $createHostList );
544  $escPass = $this->db->addQuotes( $password );
545 
546  foreach ( $createHostList as $host ) {
547  $fullName = $this->buildFullUserName( $dbUser, $host );
548  if ( !$this->userDefinitelyExists( $host, $dbUser ) ) {
549  try {
550  $this->db->begin( __METHOD__ );
551  $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
552  $this->db->commit( __METHOD__ );
553  $grantableNames[] = $fullName;
554  } catch ( DBQueryError $dqe ) {
555  if ( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
556  // User (probably) already exists
557  $this->db->rollback( __METHOD__ );
558  $status->warning( 'config-install-user-alreadyexists', $dbUser );
559  $grantableNames[] = $fullName;
560  break;
561  } else {
562  // If we couldn't create for some bizarre reason and the
563  // user probably doesn't exist, skip the grant
564  $this->db->rollback( __METHOD__ );
565  $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getMessage() );
566  }
567  }
568  } else {
569  $status->warning( 'config-install-user-alreadyexists', $dbUser );
570  $grantableNames[] = $fullName;
571  break;
572  }
573  }
574  }
575 
576  // Try to grant to all the users we know exist or we were able to create
577  $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*';
578  foreach ( $grantableNames as $name ) {
579  try {
580  $this->db->begin( __METHOD__ );
581  $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
582  $this->db->commit( __METHOD__ );
583  } catch ( DBQueryError $dqe ) {
584  $this->db->rollback( __METHOD__ );
585  $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getMessage() );
586  }
587  }
588 
589  return $status;
590  }
591 
598  private function buildFullUserName( $name, $host ) {
599  return $this->db->addQuotes( $name ) . '@' . $this->db->addQuotes( $host );
600  }
601 
609  private function userDefinitelyExists( $host, $user ) {
610  try {
611  $res = $this->db->newSelectQueryBuilder()
612  ->select( [ 'Host', 'User' ] )
613  ->from( 'mysql.user' )
614  ->where( [ 'Host' => $host, 'User' => $user ] )
615  ->caller( __METHOD__ )->fetchRow();
616 
617  return (bool)$res;
618  } catch ( DBQueryError $dqe ) {
619  return false;
620  }
621  }
622 
629  protected function getTableOptions() {
630  $options = [];
631  if ( $this->getVar( '_MysqlEngine' ) !== null ) {
632  $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' );
633  }
634  if ( $this->getVar( '_MysqlCharset' ) !== null ) {
635  $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' );
636  }
637 
638  return implode( ', ', $options );
639  }
640 
646  public function getSchemaVars() {
647  return [
648  'wgDBTableOptions' => $this->getTableOptions(),
649  'wgDBname' => $this->getVar( 'wgDBname' ),
650  'wgDBuser' => $this->getVar( 'wgDBuser' ),
651  'wgDBpassword' => $this->getVar( 'wgDBpassword' ),
652  ];
653  }
654 
655  public function getLocalSettings() {
656  $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
657  $useSsl = $this->getVar( 'wgDBssl' ) ? 'true' : 'false';
659 
660  return "# MySQL specific settings
661 \$wgDBprefix = \"{$prefix}\";
662 \$wgDBssl = {$useSsl};
663 
664 # MySQL table options to use during installation or update
665 \$wgDBTableOptions = \"{$tblOpts}\";";
666  }
667 }
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.
selectDatabase(Database $conn, string $database)
Database $db
The database connection.
setVarsFromRequest( $varNames)
Convenience function to set variables based on form data.
getCheckBox( $var, $label, $attribs=[], $helpData="")
Get a labelled checkbox to configure a local boolean variable.
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.
static escapePhpString( $string)
Returns the escaped version of a string of php code.
This class is a collection of static functions that serve two purposes:
Definition: Html.php:57
Service locator for MediaWiki core services.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
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.
static $minimumVersion
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.
Constructs Database objects.
$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:36
getSoftwareLink()
Returns a wikitext style link to the DB's website (e.g.