MediaWiki  master
MysqlInstaller.php
Go to the documentation of this file.
1 <?php
27 
35 
36  protected $globalNames = [
37  'wgDBserver',
38  'wgDBname',
39  'wgDBuser',
40  'wgDBpassword',
41  'wgDBprefix',
42  'wgDBTableOptions',
43  ];
44 
45  protected $internalDefaults = [
46  '_MysqlEngine' => 'InnoDB',
47  '_MysqlCharset' => 'binary',
48  '_InstallUser' => 'root',
49  ];
50 
51  public $supportedEngines = [ 'InnoDB' ];
52 
53  public static $minimumVersion = '5.5.8';
54  protected static $notMinimumVersionMessage = 'config-mysql-old';
55 
56  public $webUserPrivs = [
57  'DELETE',
58  'INSERT',
59  'SELECT',
60  'UPDATE',
61  'CREATE TEMPORARY TABLES',
62  ];
63 
67  public function getName() {
68  return 'mysql';
69  }
70 
74  public function isCompiled() {
75  return self::checkExtension( 'mysqli' );
76  }
77 
81  public function getConnectForm() {
82  return $this->getTextBox(
83  'wgDBserver',
84  'config-db-host',
85  [],
86  $this->parent->getHelpBox( 'config-db-host-help' )
87  ) .
88  Html::openElement( 'fieldset' ) .
89  Html::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
90  $this->getTextBox( 'wgDBname', 'config-db-name', [ 'dir' => 'ltr' ],
91  $this->parent->getHelpBox( 'config-db-name-help' ) ) .
92  $this->getTextBox( 'wgDBprefix', 'config-db-prefix', [ 'dir' => 'ltr' ],
93  $this->parent->getHelpBox( 'config-db-prefix-help' ) ) .
94  Html::closeElement( 'fieldset' ) .
95  $this->getInstallUserBox();
96  }
97 
98  public function submitConnectForm() {
99  // Get variables from the request.
100  $newValues = $this->setVarsFromRequest( [ 'wgDBserver', 'wgDBname', 'wgDBprefix' ] );
101 
102  // Validate them.
103  $status = Status::newGood();
104  if ( !strlen( $newValues['wgDBserver'] ) ) {
105  $status->fatal( 'config-missing-db-host' );
106  }
107  if ( !strlen( $newValues['wgDBname'] ) ) {
108  $status->fatal( 'config-missing-db-name' );
109  } elseif ( !preg_match( '/^[a-z0-9+_-]+$/i', $newValues['wgDBname'] ) ) {
110  $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
111  }
112  if ( !preg_match( '/^[a-z0-9_-]*$/i', $newValues['wgDBprefix'] ) ) {
113  $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] );
114  }
115  if ( !$status->isOK() ) {
116  return $status;
117  }
118 
119  // Submit user box
120  $status = $this->submitInstallUserBox();
121  if ( !$status->isOK() ) {
122  return $status;
123  }
124 
125  // Try to connect
126  $status = $this->getConnection();
127  if ( !$status->isOK() ) {
128  return $status;
129  }
133  $conn = $status->value;
134  '@phan-var Database $conn';
135 
136  // Check version
137  return static::meetsMinimumRequirement( $conn->getServerVersion() );
138  }
139 
143  public function openConnection() {
144  $status = Status::newGood();
145  try {
147  $db = Database::factory( 'mysql', [
148  'host' => $this->getVar( 'wgDBserver' ),
149  'user' => $this->getVar( '_InstallUser' ),
150  'password' => $this->getVar( '_InstallPassword' ),
151  'dbname' => false,
152  'flags' => 0,
153  'tablePrefix' => $this->getVar( 'wgDBprefix' ) ] );
154  $status->value = $db;
155  } catch ( DBConnectionError $e ) {
156  $status->fatal( 'config-connection-error', $e->getMessage() );
157  }
158 
159  return $status;
160  }
161 
162  public function preUpgrade() {
163  global $wgDBuser, $wgDBpassword;
164 
165  $status = $this->getConnection();
166  if ( !$status->isOK() ) {
167  $this->parent->showStatusMessage( $status );
168 
169  return;
170  }
174  $conn = $status->value;
175  $conn->selectDB( $this->getVar( 'wgDBname' ) );
176 
177  # Determine existing default character set
178  if ( $conn->tableExists( "revision", __METHOD__ ) ) {
179  $revision = $this->escapeLikeInternal( $this->getVar( 'wgDBprefix' ) . 'revision', '\\' );
180  $res = $conn->query( "SHOW TABLE STATUS LIKE '$revision'", __METHOD__ );
181  $row = $conn->fetchObject( $res );
182  if ( !$row ) {
183  $this->parent->showMessage( 'config-show-table-status' );
184  $existingSchema = false;
185  $existingEngine = false;
186  } else {
187  if ( preg_match( '/^latin1/', $row->Collation ) ) {
188  $existingSchema = 'latin1';
189  } elseif ( preg_match( '/^utf8/', $row->Collation ) ) {
190  $existingSchema = 'utf8';
191  } elseif ( preg_match( '/^binary/', $row->Collation ) ) {
192  $existingSchema = 'binary';
193  } else {
194  $existingSchema = false;
195  $this->parent->showMessage( 'config-unknown-collation' );
196  }
197  $existingEngine = $row->Engine ?? $row->Type;
198  }
199  } else {
200  $existingSchema = false;
201  $existingEngine = false;
202  }
203 
204  if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) {
205  $this->setVar( '_MysqlCharset', $existingSchema );
206  }
207  if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) {
208  $this->setVar( '_MysqlEngine', $existingEngine );
209  }
210 
211  # Normal user and password are selected after this step, so for now
212  # just copy these two
213  $wgDBuser = $this->getVar( '_InstallUser' );
214  $wgDBpassword = $this->getVar( '_InstallPassword' );
215  }
216 
222  protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
223  return str_replace( [ $escapeChar, '%', '_' ],
224  [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
225  $s );
226  }
227 
233  public function getEngines() {
234  $status = $this->getConnection();
235 
239  $conn = $status->value;
240 
241  $engines = [];
242  $res = $conn->query( 'SHOW ENGINES', __METHOD__ );
243  foreach ( $res as $row ) {
244  if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) {
245  $engines[] = $row->Engine;
246  }
247  }
248  $engines = array_intersect( $this->supportedEngines, $engines );
249 
250  return $engines;
251  }
252 
258  public function getCharsets() {
259  return [ 'binary', 'utf8' ];
260  }
261 
267  public function canCreateAccounts() {
268  $status = $this->getConnection();
269  if ( !$status->isOK() ) {
270  return false;
271  }
273  $conn = $status->value;
274 
275  // Get current account name
276  $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ );
277  $parts = explode( '@', $currentName );
278  if ( count( $parts ) != 2 ) {
279  return false;
280  }
281  $quotedUser = $conn->addQuotes( $parts[0] ) .
282  '@' . $conn->addQuotes( $parts[1] );
283 
284  // The user needs to have INSERT on mysql.* to be able to CREATE USER
285  // The grantee will be double-quoted in this query, as required
286  $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*',
287  [ 'GRANTEE' => $quotedUser ], __METHOD__ );
288  $insertMysql = false;
289  $grantOptions = array_flip( $this->webUserPrivs );
290  foreach ( $res as $row ) {
291  if ( $row->PRIVILEGE_TYPE == 'INSERT' ) {
292  $insertMysql = true;
293  }
294  if ( $row->IS_GRANTABLE ) {
295  unset( $grantOptions[$row->PRIVILEGE_TYPE] );
296  }
297  }
298 
299  // Check for DB-specific privs for mysql.*
300  if ( !$insertMysql ) {
301  $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
302  [
303  'GRANTEE' => $quotedUser,
304  'TABLE_SCHEMA' => 'mysql',
305  'PRIVILEGE_TYPE' => 'INSERT',
306  ], __METHOD__ );
307  if ( $row ) {
308  $insertMysql = true;
309  }
310  }
311 
312  if ( !$insertMysql ) {
313  return false;
314  }
315 
316  // Check for DB-level grant options
317  $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
318  [
319  'GRANTEE' => $quotedUser,
320  'IS_GRANTABLE' => 1,
321  ], __METHOD__ );
322  foreach ( $res as $row ) {
323  $regex = $this->likeToRegex( $row->TABLE_SCHEMA );
324  if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) {
325  unset( $grantOptions[$row->PRIVILEGE_TYPE] );
326  }
327  }
328  if ( count( $grantOptions ) ) {
329  // Can't grant everything
330  return false;
331  }
332 
333  return true;
334  }
335 
342  protected function likeToRegex( $wildcard ) {
343  $r = preg_quote( $wildcard, '/' );
344  $r = strtr( $r, [
345  '%' => '.*',
346  '_' => '.'
347  ] );
348  return "/$r/s";
349  }
350 
354  public function getSettingsForm() {
355  if ( $this->canCreateAccounts() ) {
356  $noCreateMsg = false;
357  } else {
358  $noCreateMsg = 'config-db-web-no-create-privs';
359  }
360  $s = $this->getWebUserBox( $noCreateMsg );
361 
362  // Do engine selector
363  $engines = $this->getEngines();
364  // If the current default engine is not supported, use an engine that is
365  if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
366  $this->setVar( '_MysqlEngine', reset( $engines ) );
367  }
368 
369  // If the current default charset is not supported, use a charset that is
370  $charsets = $this->getCharsets();
371  if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
372  $this->setVar( '_MysqlCharset', reset( $charsets ) );
373  }
374 
375  return $s;
376  }
377 
381  public function submitSettingsForm() {
382  $this->setVarsFromRequest( [ '_MysqlEngine', '_MysqlCharset' ] );
383  $status = $this->submitWebUserBox();
384  if ( !$status->isOK() ) {
385  return $status;
386  }
387 
388  // Validate the create checkbox
389  $canCreate = $this->canCreateAccounts();
390  if ( !$canCreate ) {
391  $this->setVar( '_CreateDBAccount', false );
392  $create = false;
393  } else {
394  $create = $this->getVar( '_CreateDBAccount' );
395  }
396 
397  if ( !$create ) {
398  // Test the web account
399  try {
400  Database::factory( 'mysql', [
401  'host' => $this->getVar( 'wgDBserver' ),
402  'user' => $this->getVar( 'wgDBuser' ),
403  'password' => $this->getVar( 'wgDBpassword' ),
404  'dbname' => false,
405  'flags' => 0,
406  'tablePrefix' => $this->getVar( 'wgDBprefix' )
407  ] );
408  } catch ( DBConnectionError $e ) {
409  return Status::newFatal( 'config-connection-error', $e->getMessage() );
410  }
411  }
412 
413  // Validate engines and charsets
414  // This is done pre-submit already so it's just for security
415  $engines = $this->getEngines();
416  if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
417  $this->setVar( '_MysqlEngine', reset( $engines ) );
418  }
419  $charsets = $this->getCharsets();
420  if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
421  $this->setVar( '_MysqlCharset', reset( $charsets ) );
422  }
423 
424  return Status::newGood();
425  }
426 
427  public function preInstall() {
428  # Add our user callback to installSteps, right before the tables are created.
429  $callback = [
430  'name' => 'user',
431  'callback' => [ $this, 'setupUser' ],
432  ];
433  $this->parent->addInstallStep( $callback, 'tables' );
434  }
435 
439  public function setupDatabase() {
440  $status = $this->getConnection();
441  if ( !$status->isOK() ) {
442  return $status;
443  }
445  $conn = $status->value;
446  $dbName = $this->getVar( 'wgDBname' );
447  if ( !$this->databaseExists( $dbName ) ) {
448  $conn->query(
449  "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ) . "CHARACTER SET utf8",
450  __METHOD__
451  );
452  }
453  $conn->selectDB( $dbName );
454  $this->setupSchemaVars();
455 
456  return $status;
457  }
458 
464  private function databaseExists( $dbName ) {
465  $encDatabase = $this->db->addQuotes( $dbName );
466 
467  return $this->db->query(
468  "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = $encDatabase",
469  __METHOD__
470  )->numRows() > 0;
471  }
472 
476  public function setupUser() {
477  $dbUser = $this->getVar( 'wgDBuser' );
478  if ( $dbUser == $this->getVar( '_InstallUser' ) ) {
479  return Status::newGood();
480  }
481  $status = $this->getConnection();
482  if ( !$status->isOK() ) {
483  return $status;
484  }
485 
486  $this->setupSchemaVars();
487  $dbName = $this->getVar( 'wgDBname' );
488  $this->db->selectDB( $dbName );
489  $server = $this->getVar( 'wgDBserver' );
490  $password = $this->getVar( 'wgDBpassword' );
491  $grantableNames = [];
492 
493  if ( $this->getVar( '_CreateDBAccount' ) ) {
494  // Before we blindly try to create a user that already has access,
495  try { // first attempt to connect to the database
496  Database::factory( 'mysql', [
497  'host' => $server,
498  'user' => $dbUser,
499  'password' => $password,
500  'dbname' => false,
501  'flags' => 0,
502  'tablePrefix' => $this->getVar( 'wgDBprefix' )
503  ] );
504  $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
505  $tryToCreate = false;
506  } catch ( DBConnectionError $e ) {
507  $tryToCreate = true;
508  }
509  } else {
510  $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
511  $tryToCreate = false;
512  }
513 
514  if ( $tryToCreate ) {
515  $createHostList = [
516  $server,
517  'localhost',
518  'localhost.localdomain',
519  '%'
520  ];
521 
522  $createHostList = array_unique( $createHostList );
523  $escPass = $this->db->addQuotes( $password );
524 
525  foreach ( $createHostList as $host ) {
526  $fullName = $this->buildFullUserName( $dbUser, $host );
527  if ( !$this->userDefinitelyExists( $host, $dbUser ) ) {
528  try {
529  $this->db->begin( __METHOD__ );
530  $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
531  $this->db->commit( __METHOD__ );
532  $grantableNames[] = $fullName;
533  } catch ( DBQueryError $dqe ) {
534  if ( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
535  // User (probably) already exists
536  $this->db->rollback( __METHOD__ );
537  $status->warning( 'config-install-user-alreadyexists', $dbUser );
538  $grantableNames[] = $fullName;
539  break;
540  } else {
541  // If we couldn't create for some bizzare reason and the
542  // user probably doesn't exist, skip the grant
543  $this->db->rollback( __METHOD__ );
544  $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getMessage() );
545  }
546  }
547  } else {
548  $status->warning( 'config-install-user-alreadyexists', $dbUser );
549  $grantableNames[] = $fullName;
550  break;
551  }
552  }
553  }
554 
555  // Try to grant to all the users we know exist or we were able to create
556  $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*';
557  foreach ( $grantableNames as $name ) {
558  try {
559  $this->db->begin( __METHOD__ );
560  $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
561  $this->db->commit( __METHOD__ );
562  } catch ( DBQueryError $dqe ) {
563  $this->db->rollback( __METHOD__ );
564  $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getMessage() );
565  }
566  }
567 
568  return $status;
569  }
570 
577  private function buildFullUserName( $name, $host ) {
578  return $this->db->addQuotes( $name ) . '@' . $this->db->addQuotes( $host );
579  }
580 
588  private function userDefinitelyExists( $host, $user ) {
589  try {
590  $res = $this->db->selectRow( 'mysql.user', [ 'Host', 'User' ],
591  [ 'Host' => $host, 'User' => $user ], __METHOD__ );
592 
593  return (bool)$res;
594  } catch ( DBQueryError $dqe ) {
595  return false;
596  }
597  }
598 
605  protected function getTableOptions() {
606  $options = [];
607  if ( $this->getVar( '_MysqlEngine' ) !== null ) {
608  $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' );
609  }
610  if ( $this->getVar( '_MysqlCharset' ) !== null ) {
611  $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' );
612  }
613 
614  return implode( ', ', $options );
615  }
616 
622  public function getSchemaVars() {
623  return [
624  'wgDBTableOptions' => $this->getTableOptions(),
625  'wgDBname' => $this->getVar( 'wgDBname' ),
626  'wgDBuser' => $this->getVar( 'wgDBuser' ),
627  'wgDBpassword' => $this->getVar( 'wgDBpassword' ),
628  ];
629  }
630 
631  public function getLocalSettings() {
632  $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
634 
635  return "# MySQL specific settings
636 \$wgDBprefix = \"{$prefix}\";
637 
638 # MySQL table options to use during installation or update
639 \$wgDBTableOptions = \"{$tblOpts}\";";
640  }
641 }
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
userDefinitelyExists( $host, $user)
Try to see if the user account exists.
getSchemaVars()
Get variables to substitute into tables.sql and the SQL patch files.
Class for setting up the MediaWiki database using MySQL.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
static $notMinimumVersionMessage
$wgDBpassword
Database user&#39;s password.
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing &#39;/&#39;...
Definition: Html.php:251
escapeLikeInternal( $s, $escapeChar='`')
likeToRegex( $wildcard)
Convert a wildcard (as used in LIKE) to a regex Slashes are escaped, slash terminators included...
$wgDBuser
Database username.
buildFullUserName( $name, $host)
Return a formal &#39;User&#39;@&#39;Host&#39; username for use in queries.
getTextBox( $var, $label, $attribs=[], $helpData="")
Get a labelled text box to configure a local variable.
getCharsets()
Get a list of character sets that are available and supported.
getConnection()
Connect to the database using the administrative user/password currently defined in the session...
$fullName
submitWebUserBox()
Submit the form from getWebUserBox().
setupSchemaVars()
Set appropriate schema variables in the current database connection.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:315
static $minimumVersion
getWebUserBox( $noCreateMsg=false)
Get a standard web-user fieldset.
submitInstallUserBox()
Submit a standard install user fieldset.
getInstallUserBox()
Get a standard install-user fieldset.
getVar( $var, $default=null)
Get a variable, taking local defaults into account.
databaseExists( $dbName)
Try to see if a given database exists.
Database $db
The database connection.
canCreateAccounts()
Return true if the install user can create accounts.
setVar( $name, $value)
Convenience alias for $this->parent->setVar()
Base class for DBMS-specific installation helper classes.
getEngines()
Get a list of storage engines that are available and supported.
static escapePhpString( $string)
Returns the escaped version of a string of php code.
getTableOptions()
Return any table options to be applied to all tables that don&#39;t override them.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
setVarsFromRequest( $varNames)
Convenience function to set variables based on form data.