[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * MySQL-specific installer. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Deployment 22 */ 23 24 /** 25 * Class for setting up the MediaWiki database using MySQL. 26 * 27 * @ingroup Deployment 28 * @since 1.17 29 */ 30 class MysqlInstaller extends DatabaseInstaller { 31 32 protected $globalNames = array( 33 'wgDBserver', 34 'wgDBname', 35 'wgDBuser', 36 'wgDBpassword', 37 'wgDBprefix', 38 'wgDBTableOptions', 39 'wgDBmysql5', 40 ); 41 42 protected $internalDefaults = array( 43 '_MysqlEngine' => 'InnoDB', 44 '_MysqlCharset' => 'binary', 45 '_InstallUser' => 'root', 46 ); 47 48 public $supportedEngines = array( 'InnoDB', 'MyISAM' ); 49 50 public $minimumVersion = '5.0.2'; 51 52 public $webUserPrivs = array( 53 'DELETE', 54 'INSERT', 55 'SELECT', 56 'UPDATE', 57 'CREATE TEMPORARY TABLES', 58 ); 59 60 /** 61 * @return string 62 */ 63 public function getName() { 64 return 'mysql'; 65 } 66 67 /** 68 * @return bool 69 */ 70 public function isCompiled() { 71 return self::checkExtension( 'mysql' ) || self::checkExtension( 'mysqli' ); 72 } 73 74 /** 75 * @return array 76 */ 77 public function getGlobalDefaults() { 78 return array(); 79 } 80 81 /** 82 * @return string 83 */ 84 public function getConnectForm() { 85 return $this->getTextBox( 86 'wgDBserver', 87 'config-db-host', 88 array(), 89 $this->parent->getHelpBox( 'config-db-host-help' ) 90 ) . 91 Html::openElement( 'fieldset' ) . 92 Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) . 93 $this->getTextBox( 'wgDBname', 'config-db-name', array( 'dir' => 'ltr' ), 94 $this->parent->getHelpBox( 'config-db-name-help' ) ) . 95 $this->getTextBox( 'wgDBprefix', 'config-db-prefix', array( 'dir' => 'ltr' ), 96 $this->parent->getHelpBox( 'config-db-prefix-help' ) ) . 97 Html::closeElement( 'fieldset' ) . 98 $this->getInstallUserBox(); 99 } 100 101 public function submitConnectForm() { 102 // Get variables from the request. 103 $newValues = $this->setVarsFromRequest( array( 104 'wgDBserver', 'wgDBname', 'wgDBprefix', '_InstallUser', '_InstallPassword' 105 ) ); 106 107 // Validate them. 108 $status = Status::newGood(); 109 if ( !strlen( $newValues['wgDBserver'] ) ) { 110 $status->fatal( 'config-missing-db-host' ); 111 } 112 if ( !strlen( $newValues['wgDBname'] ) ) { 113 $status->fatal( 'config-missing-db-name' ); 114 } elseif ( !preg_match( '/^[a-z0-9+_-]+$/i', $newValues['wgDBname'] ) ) { 115 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] ); 116 } 117 if ( !preg_match( '/^[a-z0-9_-]*$/i', $newValues['wgDBprefix'] ) ) { 118 $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] ); 119 } 120 if ( !strlen( $newValues['_InstallUser'] ) ) { 121 $status->fatal( 'config-db-username-empty' ); 122 } 123 if ( !strlen( $newValues['_InstallPassword'] ) ) { 124 $status->fatal( 'config-db-password-empty', $newValues['_InstallUser'] ); 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 } 141 /** 142 * @var $conn DatabaseBase 143 */ 144 $conn = $status->value; 145 146 // Check version 147 $version = $conn->getServerVersion(); 148 if ( version_compare( $version, $this->minimumVersion ) < 0 ) { 149 return Status::newFatal( 'config-mysql-old', $this->minimumVersion, $version ); 150 } 151 152 return $status; 153 } 154 155 /** 156 * @return Status 157 */ 158 public function openConnection() { 159 $status = Status::newGood(); 160 try { 161 $db = DatabaseBase::factory( 'mysql', array( 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->showStatusError( $status ); 182 183 return; 184 } 185 /** 186 * @var $conn DatabaseBase 187 */ 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 = $conn->buildLike( $this->getVar( 'wgDBprefix' ) . 'revision' ); 194 $res = $conn->query( "SHOW TABLE STATUS $revision", __METHOD__ ); 195 $row = $conn->fetchObject( $res ); 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 if ( isset( $row->Engine ) ) { 212 $existingEngine = $row->Engine; 213 } else { 214 $existingEngine = $row->Type; 215 } 216 } 217 } else { 218 $existingSchema = false; 219 $existingEngine = false; 220 } 221 222 if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) { 223 $this->setVar( '_MysqlCharset', $existingSchema ); 224 } 225 if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) { 226 $this->setVar( '_MysqlEngine', $existingEngine ); 227 } 228 229 # Normal user and password are selected after this step, so for now 230 # just copy these two 231 $wgDBuser = $this->getVar( '_InstallUser' ); 232 $wgDBpassword = $this->getVar( '_InstallPassword' ); 233 } 234 235 /** 236 * Get a list of storage engines that are available and supported 237 * 238 * @return array 239 */ 240 public function getEngines() { 241 $status = $this->getConnection(); 242 243 /** 244 * @var $conn DatabaseBase 245 */ 246 $conn = $status->value; 247 248 $engines = array(); 249 $res = $conn->query( 'SHOW ENGINES', __METHOD__ ); 250 foreach ( $res as $row ) { 251 if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) { 252 $engines[] = $row->Engine; 253 } 254 } 255 $engines = array_intersect( $this->supportedEngines, $engines ); 256 257 return $engines; 258 } 259 260 /** 261 * Get a list of character sets that are available and supported 262 * 263 * @return array 264 */ 265 public function getCharsets() { 266 return array( 'binary', 'utf8' ); 267 } 268 269 /** 270 * Return true if the install user can create accounts 271 * 272 * @return bool 273 */ 274 public function canCreateAccounts() { 275 $status = $this->getConnection(); 276 if ( !$status->isOK() ) { 277 return false; 278 } 279 /** @var $conn DatabaseBase */ 280 $conn = $status->value; 281 282 // Get current account name 283 $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ ); 284 $parts = explode( '@', $currentName ); 285 if ( count( $parts ) != 2 ) { 286 return false; 287 } 288 $quotedUser = $conn->addQuotes( $parts[0] ) . 289 '@' . $conn->addQuotes( $parts[1] ); 290 291 // The user needs to have INSERT on mysql.* to be able to CREATE USER 292 // The grantee will be double-quoted in this query, as required 293 $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*', 294 array( 'GRANTEE' => $quotedUser ), __METHOD__ ); 295 $insertMysql = false; 296 $grantOptions = array_flip( $this->webUserPrivs ); 297 foreach ( $res as $row ) { 298 if ( $row->PRIVILEGE_TYPE == 'INSERT' ) { 299 $insertMysql = true; 300 } 301 if ( $row->IS_GRANTABLE ) { 302 unset( $grantOptions[$row->PRIVILEGE_TYPE] ); 303 } 304 } 305 306 // Check for DB-specific privs for mysql.* 307 if ( !$insertMysql ) { 308 $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*', 309 array( 310 'GRANTEE' => $quotedUser, 311 'TABLE_SCHEMA' => 'mysql', 312 'PRIVILEGE_TYPE' => 'INSERT', 313 ), __METHOD__ ); 314 if ( $row ) { 315 $insertMysql = true; 316 } 317 } 318 319 if ( !$insertMysql ) { 320 return false; 321 } 322 323 // Check for DB-level grant options 324 $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*', 325 array( 326 'GRANTEE' => $quotedUser, 327 'IS_GRANTABLE' => 1, 328 ), __METHOD__ ); 329 foreach ( $res as $row ) { 330 $regex = $conn->likeToRegex( $row->TABLE_SCHEMA ); 331 if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) { 332 unset( $grantOptions[$row->PRIVILEGE_TYPE] ); 333 } 334 } 335 if ( count( $grantOptions ) ) { 336 // Can't grant everything 337 return false; 338 } 339 340 return true; 341 } 342 343 /** 344 * @return string 345 */ 346 public function getSettingsForm() { 347 if ( $this->canCreateAccounts() ) { 348 $noCreateMsg = false; 349 } else { 350 $noCreateMsg = 'config-db-web-no-create-privs'; 351 } 352 $s = $this->getWebUserBox( $noCreateMsg ); 353 354 // Do engine selector 355 $engines = $this->getEngines(); 356 // If the current default engine is not supported, use an engine that is 357 if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) { 358 $this->setVar( '_MysqlEngine', reset( $engines ) ); 359 } 360 361 $s .= Xml::openElement( 'div', array( 362 'id' => 'dbMyisamWarning' 363 ) ); 364 $myisamWarning = 'config-mysql-myisam-dep'; 365 if ( count( $engines ) === 1 ) { 366 $myisamWarning = 'config-mysql-only-myisam-dep'; 367 } 368 $s .= $this->parent->getWarningBox( wfMessage( $myisamWarning )->text() ); 369 $s .= Xml::closeElement( 'div' ); 370 371 if ( $this->getVar( '_MysqlEngine' ) != 'MyISAM' ) { 372 $s .= Xml::openElement( 'script', array( 'type' => 'text/javascript' ) ); 373 $s .= '$(\'#dbMyisamWarning\').hide();'; 374 $s .= Xml::closeElement( 'script' ); 375 } 376 377 if ( count( $engines ) >= 2 ) { 378 // getRadioSet() builds a set of labeled radio buttons. 379 // For grep: The following messages are used as the item labels: 380 // config-mysql-innodb, config-mysql-myisam 381 $s .= $this->getRadioSet( array( 382 'var' => '_MysqlEngine', 383 'label' => 'config-mysql-engine', 384 'itemLabelPrefix' => 'config-mysql-', 385 'values' => $engines, 386 'itemAttribs' => array( 387 'MyISAM' => array( 388 'class' => 'showHideRadio', 389 'rel' => 'dbMyisamWarning' 390 ), 391 'InnoDB' => array( 392 'class' => 'hideShowRadio', 393 'rel' => 'dbMyisamWarning' 394 ) 395 ) 396 ) ); 397 $s .= $this->parent->getHelpBox( 'config-mysql-engine-help' ); 398 } 399 400 // If the current default charset is not supported, use a charset that is 401 $charsets = $this->getCharsets(); 402 if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) { 403 $this->setVar( '_MysqlCharset', reset( $charsets ) ); 404 } 405 406 // Do charset selector 407 if ( count( $charsets ) >= 2 ) { 408 // getRadioSet() builds a set of labeled radio buttons. 409 // For grep: The following messages are used as the item labels: 410 // config-mysql-binary, config-mysql-utf8 411 $s .= $this->getRadioSet( array( 412 'var' => '_MysqlCharset', 413 'label' => 'config-mysql-charset', 414 'itemLabelPrefix' => 'config-mysql-', 415 'values' => $charsets 416 ) ); 417 $s .= $this->parent->getHelpBox( 'config-mysql-charset-help' ); 418 } 419 420 return $s; 421 } 422 423 /** 424 * @return Status 425 */ 426 public function submitSettingsForm() { 427 $this->setVarsFromRequest( array( '_MysqlEngine', '_MysqlCharset' ) ); 428 $status = $this->submitWebUserBox(); 429 if ( !$status->isOK() ) { 430 return $status; 431 } 432 433 // Validate the create checkbox 434 $canCreate = $this->canCreateAccounts(); 435 if ( !$canCreate ) { 436 $this->setVar( '_CreateDBAccount', false ); 437 $create = false; 438 } else { 439 $create = $this->getVar( '_CreateDBAccount' ); 440 } 441 442 if ( !$create ) { 443 // Test the web account 444 try { 445 DatabaseBase::factory( 'mysql', array( 446 'host' => $this->getVar( 'wgDBserver' ), 447 'user' => $this->getVar( 'wgDBuser' ), 448 'password' => $this->getVar( 'wgDBpassword' ), 449 'dbname' => false, 450 'flags' => 0, 451 'tablePrefix' => $this->getVar( 'wgDBprefix' ) 452 ) ); 453 } catch ( DBConnectionError $e ) { 454 return Status::newFatal( 'config-connection-error', $e->getMessage() ); 455 } 456 } 457 458 // Validate engines and charsets 459 // This is done pre-submit already so it's just for security 460 $engines = $this->getEngines(); 461 if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) { 462 $this->setVar( '_MysqlEngine', reset( $engines ) ); 463 } 464 $charsets = $this->getCharsets(); 465 if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) { 466 $this->setVar( '_MysqlCharset', reset( $charsets ) ); 467 } 468 469 return Status::newGood(); 470 } 471 472 public function preInstall() { 473 # Add our user callback to installSteps, right before the tables are created. 474 $callback = array( 475 'name' => 'user', 476 'callback' => array( $this, 'setupUser' ), 477 ); 478 $this->parent->addInstallStep( $callback, 'tables' ); 479 } 480 481 /** 482 * @return Status 483 */ 484 public function setupDatabase() { 485 $status = $this->getConnection(); 486 if ( !$status->isOK() ) { 487 return $status; 488 } 489 /** @var DatabaseBase $conn */ 490 $conn = $status->value; 491 $dbName = $this->getVar( 'wgDBname' ); 492 if ( !$conn->selectDB( $dbName ) ) { 493 $conn->query( 494 "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ) . "CHARACTER SET utf8", 495 __METHOD__ 496 ); 497 $conn->selectDB( $dbName ); 498 } 499 $this->setupSchemaVars(); 500 501 return $status; 502 } 503 504 /** 505 * @return Status 506 */ 507 public function setupUser() { 508 $dbUser = $this->getVar( 'wgDBuser' ); 509 if ( $dbUser == $this->getVar( '_InstallUser' ) ) { 510 return Status::newGood(); 511 } 512 $status = $this->getConnection(); 513 if ( !$status->isOK() ) { 514 return $status; 515 } 516 517 $this->setupSchemaVars(); 518 $dbName = $this->getVar( 'wgDBname' ); 519 $this->db->selectDB( $dbName ); 520 $server = $this->getVar( 'wgDBserver' ); 521 $password = $this->getVar( 'wgDBpassword' ); 522 $grantableNames = array(); 523 524 if ( $this->getVar( '_CreateDBAccount' ) ) { 525 // Before we blindly try to create a user that already has access, 526 try { // first attempt to connect to the database 527 DatabaseBase::factory( 'mysql', array( 528 'host' => $server, 529 'user' => $dbUser, 530 'password' => $password, 531 'dbname' => false, 532 'flags' => 0, 533 'tablePrefix' => $this->getVar( 'wgDBprefix' ) 534 ) ); 535 $grantableNames[] = $this->buildFullUserName( $dbUser, $server ); 536 $tryToCreate = false; 537 } catch ( DBConnectionError $e ) { 538 $tryToCreate = true; 539 } 540 } else { 541 $grantableNames[] = $this->buildFullUserName( $dbUser, $server ); 542 $tryToCreate = false; 543 } 544 545 if ( $tryToCreate ) { 546 $createHostList = array( 547 $server, 548 'localhost', 549 'localhost.localdomain', 550 '%' 551 ); 552 553 $createHostList = array_unique( $createHostList ); 554 $escPass = $this->db->addQuotes( $password ); 555 556 foreach ( $createHostList as $host ) { 557 $fullName = $this->buildFullUserName( $dbUser, $host ); 558 if ( !$this->userDefinitelyExists( $dbUser, $host ) ) { 559 try { 560 $this->db->begin( __METHOD__ ); 561 $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ ); 562 $this->db->commit( __METHOD__ ); 563 $grantableNames[] = $fullName; 564 } catch ( DBQueryError $dqe ) { 565 if ( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) { 566 // User (probably) already exists 567 $this->db->rollback( __METHOD__ ); 568 $status->warning( 'config-install-user-alreadyexists', $dbUser ); 569 $grantableNames[] = $fullName; 570 break; 571 } else { 572 // If we couldn't create for some bizzare reason and the 573 // user probably doesn't exist, skip the grant 574 $this->db->rollback( __METHOD__ ); 575 $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() ); 576 } 577 } 578 } else { 579 $status->warning( 'config-install-user-alreadyexists', $dbUser ); 580 $grantableNames[] = $fullName; 581 break; 582 } 583 } 584 } 585 586 // Try to grant to all the users we know exist or we were able to create 587 $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*'; 588 foreach ( $grantableNames as $name ) { 589 try { 590 $this->db->begin( __METHOD__ ); 591 $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ ); 592 $this->db->commit( __METHOD__ ); 593 } catch ( DBQueryError $dqe ) { 594 $this->db->rollback( __METHOD__ ); 595 $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() ); 596 } 597 } 598 599 return $status; 600 } 601 602 /** 603 * Return a formal 'User'@'Host' username for use in queries 604 * @param string $name Username, quotes will be added 605 * @param string $host Hostname, quotes will be added 606 * @return string 607 */ 608 private function buildFullUserName( $name, $host ) { 609 return $this->db->addQuotes( $name ) . '@' . $this->db->addQuotes( $host ); 610 } 611 612 /** 613 * Try to see if the user account exists. Our "superuser" may not have 614 * access to mysql.user, so false means "no" or "maybe" 615 * @param string $host Hostname to check 616 * @param string $user Username to check 617 * @return bool 618 */ 619 private function userDefinitelyExists( $host, $user ) { 620 try { 621 $res = $this->db->selectRow( 'mysql.user', array( 'Host', 'User' ), 622 array( 'Host' => $host, 'User' => $user ), __METHOD__ ); 623 624 return (bool)$res; 625 } catch ( DBQueryError $dqe ) { 626 return false; 627 } 628 } 629 630 /** 631 * Return any table options to be applied to all tables that don't 632 * override them. 633 * 634 * @return string 635 */ 636 protected function getTableOptions() { 637 $options = array(); 638 if ( $this->getVar( '_MysqlEngine' ) !== null ) { 639 $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' ); 640 } 641 if ( $this->getVar( '_MysqlCharset' ) !== null ) { 642 $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' ); 643 } 644 645 return implode( ', ', $options ); 646 } 647 648 /** 649 * Get variables to substitute into tables.sql and the SQL patch files. 650 * 651 * @return array 652 */ 653 public function getSchemaVars() { 654 return array( 655 'wgDBTableOptions' => $this->getTableOptions(), 656 'wgDBname' => $this->getVar( 'wgDBname' ), 657 'wgDBuser' => $this->getVar( 'wgDBuser' ), 658 'wgDBpassword' => $this->getVar( 'wgDBpassword' ), 659 ); 660 } 661 662 public function getLocalSettings() { 663 $dbmysql5 = wfBoolToStr( $this->getVar( 'wgDBmysql5', true ) ); 664 $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) ); 665 $tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() ); 666 667 return "# MySQL specific settings 668 \$wgDBprefix = \"{$prefix}\"; 669 670 # MySQL table options to use during installation or update 671 \$wgDBTableOptions = \"{$tblOpts}\"; 672 673 # Experimental charset support for MySQL 5.0. 674 \$wgDBmysql5 = {$dbmysql5};"; 675 } 676 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |