MediaWiki  REL1_20
MysqlInstaller.php
Go to the documentation of this file.
00001 <?php
00030 class MysqlInstaller extends DatabaseInstaller {
00031 
00032         protected $globalNames = array(
00033                 'wgDBserver',
00034                 'wgDBname',
00035                 'wgDBuser',
00036                 'wgDBpassword',
00037                 'wgDBprefix',
00038                 'wgDBTableOptions',
00039                 'wgDBmysql5',
00040         );
00041 
00042         protected $internalDefaults = array(
00043                 '_MysqlEngine' => 'InnoDB',
00044                 '_MysqlCharset' => 'binary',
00045                 '_InstallUser' => 'root',
00046         );
00047 
00048         public $supportedEngines = array( 'InnoDB', 'MyISAM' );
00049 
00050         public $minimumVersion = '5.0.2';
00051 
00052         public $webUserPrivs = array(
00053                 'DELETE',
00054                 'INSERT',
00055                 'SELECT',
00056                 'UPDATE',
00057                 'CREATE TEMPORARY TABLES',
00058         );
00059 
00063         public function getName() {
00064                 return 'mysql';
00065         }
00066 
00067         public function __construct( $parent ) {
00068                 parent::__construct( $parent );
00069         }
00070 
00074         public function isCompiled() {
00075                 return self::checkExtension( 'mysql' );
00076         }
00077 
00081         public function getGlobalDefaults() {
00082                 return array();
00083         }
00084 
00088         public function getConnectForm() {
00089                 return
00090                         $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) .
00091                         Html::openElement( 'fieldset' ) .
00092                         Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) .
00093                         $this->getTextBox( 'wgDBname', 'config-db-name', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-name-help' ) ) .
00094                         $this->getTextBox( 'wgDBprefix', 'config-db-prefix', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-prefix-help' ) ) .
00095                         Html::closeElement( 'fieldset' ) .
00096                         $this->getInstallUserBox();
00097         }
00098 
00099         public function submitConnectForm() {
00100                 // Get variables from the request.
00101                 $newValues = $this->setVarsFromRequest( array( 'wgDBserver', 'wgDBname', 'wgDBprefix' ) );
00102 
00103                 // Validate them.
00104                 $status = Status::newGood();
00105                 if ( !strlen( $newValues['wgDBserver'] ) ) {
00106                         $status->fatal( 'config-missing-db-host' );
00107                 }
00108                 if ( !strlen( $newValues['wgDBname'] ) ) {
00109                         $status->fatal( 'config-missing-db-name' );
00110                 } elseif ( !preg_match( '/^[a-z0-9_-]+$/i', $newValues['wgDBname'] ) ) {
00111                         $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
00112                 }
00113                 if ( !preg_match( '/^[a-z0-9_-]*$/i', $newValues['wgDBprefix'] ) ) {
00114                         $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] );
00115                 }
00116                 if ( !$status->isOK() ) {
00117                         return $status;
00118                 }
00119 
00120                 // Submit user box
00121                 $status = $this->submitInstallUserBox();
00122                 if ( !$status->isOK() ) {
00123                         return $status;
00124                 }
00125 
00126                 // Try to connect
00127                 $status = $this->getConnection();
00128                 if ( !$status->isOK() ) {
00129                         return $status;
00130                 }
00134                 $conn = $status->value;
00135 
00136                 // Check version
00137                 $version = $conn->getServerVersion();
00138                 if ( version_compare( $version, $this->minimumVersion ) < 0 ) {
00139                         return Status::newFatal( 'config-mysql-old', $this->minimumVersion, $version );
00140                 }
00141 
00142                 return $status;
00143         }
00144 
00148         public function openConnection() {
00149                 $status = Status::newGood();
00150                 try {
00151                         $db = new DatabaseMysql(
00152                                 $this->getVar( 'wgDBserver' ),
00153                                 $this->getVar( '_InstallUser' ),
00154                                 $this->getVar( '_InstallPassword' ),
00155                                 false,
00156                                 false,
00157                                 0,
00158                                 $this->getVar( 'wgDBprefix' )
00159                         );
00160                         $status->value = $db;
00161                 } catch ( DBConnectionError $e ) {
00162                         $status->fatal( 'config-connection-error', $e->getMessage() );
00163                 }
00164                 return $status;
00165         }
00166 
00167         public function preUpgrade() {
00168                 global $wgDBuser, $wgDBpassword;
00169 
00170                 $status = $this->getConnection();
00171                 if ( !$status->isOK() ) {
00172                         $this->parent->showStatusError( $status );
00173                         return;
00174                 }
00178                 $conn = $status->value;
00179                 $conn->selectDB( $this->getVar( 'wgDBname' ) );
00180 
00181                 # Determine existing default character set
00182                 if ( $conn->tableExists( "revision", __METHOD__ ) ) {
00183                         $revision = $conn->buildLike( $this->getVar( 'wgDBprefix' ) . 'revision' );
00184                         $res = $conn->query( "SHOW TABLE STATUS $revision", __METHOD__ );
00185                         $row = $conn->fetchObject( $res );
00186                         if ( !$row ) {
00187                                 $this->parent->showMessage( 'config-show-table-status' );
00188                                 $existingSchema = false;
00189                                 $existingEngine = false;
00190                         } else {
00191                                 if ( preg_match( '/^latin1/', $row->Collation ) ) {
00192                                         $existingSchema = 'latin1';
00193                                 } elseif ( preg_match( '/^utf8/', $row->Collation ) ) {
00194                                         $existingSchema = 'utf8';
00195                                 } elseif ( preg_match( '/^binary/', $row->Collation ) ) {
00196                                         $existingSchema = 'binary';
00197                                 } else {
00198                                         $existingSchema = false;
00199                                         $this->parent->showMessage( 'config-unknown-collation' );
00200                                 }
00201                                 if ( isset( $row->Engine ) ) {
00202                                         $existingEngine = $row->Engine;
00203                                 } else {
00204                                         $existingEngine = $row->Type;
00205                                 }
00206                         }
00207                 } else {
00208                         $existingSchema = false;
00209                         $existingEngine = false;
00210                 }
00211 
00212                 if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) {
00213                         $this->setVar( '_MysqlCharset', $existingSchema );
00214                 }
00215                 if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) {
00216                         $this->setVar( '_MysqlEngine', $existingEngine );
00217                 }
00218 
00219                 # Normal user and password are selected after this step, so for now
00220                 # just copy these two
00221                 $wgDBuser = $this->getVar( '_InstallUser' );
00222                 $wgDBpassword = $this->getVar( '_InstallPassword' );
00223         }
00224 
00230         public function getEngines() {
00231                 $status = $this->getConnection();
00232 
00236                 $conn = $status->value;
00237 
00238                 $engines = array();
00239                 $res = $conn->query( 'SHOW ENGINES', __METHOD__ );
00240                 foreach ( $res as $row ) {
00241                         if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) {
00242                                 $engines[] = $row->Engine;
00243                         }
00244                 }
00245                 $engines = array_intersect( $this->supportedEngines, $engines );
00246                 return $engines;
00247         }
00248 
00254         public function getCharsets() {
00255                 return array( 'binary', 'utf8' );
00256         }
00257 
00263         public function canCreateAccounts() {
00264                 $status = $this->getConnection();
00265                 if ( !$status->isOK() ) {
00266                         return false;
00267                 }
00271                 $conn = $status->value;
00272 
00273                 // Get current account name
00274                 $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ );
00275                 $parts = explode( '@', $currentName );
00276                 if ( count( $parts ) != 2 ) {
00277                         return false;
00278                 }
00279                 $quotedUser = $conn->addQuotes( $parts[0] ) .
00280                         '@' . $conn->addQuotes( $parts[1] );
00281 
00282                 // The user needs to have INSERT on mysql.* to be able to CREATE USER
00283                 // The grantee will be double-quoted in this query, as required
00284                 $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*',
00285                         array( 'GRANTEE' => $quotedUser ), __METHOD__ );
00286                 $insertMysql = false;
00287                 $grantOptions = array_flip( $this->webUserPrivs );
00288                 foreach ( $res as $row ) {
00289                         if ( $row->PRIVILEGE_TYPE == 'INSERT' ) {
00290                                 $insertMysql = true;
00291                         }
00292                         if ( $row->IS_GRANTABLE ) {
00293                                 unset( $grantOptions[$row->PRIVILEGE_TYPE] );
00294                         }
00295                 }
00296 
00297                 // Check for DB-specific privs for mysql.*
00298                 if ( !$insertMysql ) {
00299                         $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
00300                                 array(
00301                                         'GRANTEE' => $quotedUser,
00302                                         'TABLE_SCHEMA' => 'mysql',
00303                                         'PRIVILEGE_TYPE' => 'INSERT',
00304                                 ), __METHOD__ );
00305                         if ( $row ) {
00306                                 $insertMysql = true;
00307                         }
00308                 }
00309 
00310                 if ( !$insertMysql ) {
00311                         return false;
00312                 }
00313 
00314                 // Check for DB-level grant options
00315                 $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
00316                         array(
00317                                 'GRANTEE' => $quotedUser,
00318                                 'IS_GRANTABLE' => 1,
00319                         ), __METHOD__ );
00320                 foreach ( $res as $row ) {
00321                         $regex = $conn->likeToRegex( $row->TABLE_SCHEMA );
00322                         if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) {
00323                                 unset( $grantOptions[$row->PRIVILEGE_TYPE] );
00324                         }
00325                 }
00326                 if ( count( $grantOptions ) ) {
00327                         // Can't grant everything
00328                         return false;
00329                 }
00330                 return true;
00331         }
00332 
00336         public function getSettingsForm() {
00337                 if ( $this->canCreateAccounts() ) {
00338                         $noCreateMsg = false;
00339                 } else {
00340                         $noCreateMsg = 'config-db-web-no-create-privs';
00341                 }
00342                 $s = $this->getWebUserBox( $noCreateMsg );
00343 
00344                 // Do engine selector
00345                 $engines = $this->getEngines();
00346                 // If the current default engine is not supported, use an engine that is
00347                 if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
00348                         $this->setVar( '_MysqlEngine', reset( $engines ) );
00349                 }
00350 
00351                 $s .= Xml::openElement( 'div', array(
00352                         'id' => 'dbMyisamWarning'
00353                 ));
00354                 $s .= $this->parent->getWarningBox( wfMessage( 'config-mysql-myisam-dep' )->text() );
00355                 $s .= Xml::closeElement( 'div' );
00356 
00357                 if( $this->getVar( '_MysqlEngine' ) != 'MyISAM' ) {
00358                         $s .= Xml::openElement( 'script', array( 'type' => 'text/javascript' ) );
00359                         $s .= '$(\'#dbMyisamWarning\').hide();';
00360                         $s .= Xml::closeElement( 'script' );
00361                 }
00362 
00363                 if ( count( $engines ) >= 2 ) {
00364                         $s .= $this->getRadioSet( array(
00365                                 'var' => '_MysqlEngine',
00366                                 'label' => 'config-mysql-engine',
00367                                 'itemLabelPrefix' => 'config-mysql-',
00368                                 'values' => $engines,
00369                                 'itemAttribs' => array(
00370                                         'MyISAM' => array(
00371                                                 'class' => 'showHideRadio',
00372                                                 'rel'   => 'dbMyisamWarning'),
00373                                         'InnoDB' => array(
00374                                                 'class' => 'hideShowRadio',
00375                                                 'rel'   => 'dbMyisamWarning')
00376                         )));
00377                         $s .= $this->parent->getHelpBox( 'config-mysql-engine-help' );
00378                 }
00379 
00380                 // If the current default charset is not supported, use a charset that is
00381                 $charsets = $this->getCharsets();
00382                 if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
00383                         $this->setVar( '_MysqlCharset', reset( $charsets ) );
00384                 }
00385 
00386                 // Do charset selector
00387                 if ( count( $charsets ) >= 2 ) {
00388                         $s .= $this->getRadioSet( array(
00389                                 'var' => '_MysqlCharset',
00390                                 'label' => 'config-mysql-charset',
00391                                 'itemLabelPrefix' => 'config-mysql-',
00392                                 'values' => $charsets
00393                         ));
00394                         $s .= $this->parent->getHelpBox( 'config-mysql-charset-help' );
00395                 }
00396 
00397                 return $s;
00398         }
00399 
00403         public function submitSettingsForm() {
00404                 $this->setVarsFromRequest( array( '_MysqlEngine', '_MysqlCharset' ) );
00405                 $status = $this->submitWebUserBox();
00406                 if ( !$status->isOK() ) {
00407                         return $status;
00408                 }
00409 
00410                 // Validate the create checkbox
00411                 $canCreate = $this->canCreateAccounts();
00412                 if ( !$canCreate ) {
00413                         $this->setVar( '_CreateDBAccount', false );
00414                         $create = false;
00415                 } else {
00416                         $create = $this->getVar( '_CreateDBAccount' );
00417                 }
00418 
00419                 if ( !$create ) {
00420                         // Test the web account
00421                         try {
00422                                 new DatabaseMysql(
00423                                         $this->getVar( 'wgDBserver' ),
00424                                         $this->getVar( 'wgDBuser' ),
00425                                         $this->getVar( 'wgDBpassword' ),
00426                                         false,
00427                                         false,
00428                                         0,
00429                                         $this->getVar( 'wgDBprefix' )
00430                                 );
00431                         } catch ( DBConnectionError $e ) {
00432                                 return Status::newFatal( 'config-connection-error', $e->getMessage() );
00433                         }
00434                 }
00435 
00436                 // Validate engines and charsets
00437                 // This is done pre-submit already so it's just for security
00438                 $engines = $this->getEngines();
00439                 if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
00440                         $this->setVar( '_MysqlEngine', reset( $engines ) );
00441                 }
00442                 $charsets = $this->getCharsets();
00443                 if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
00444                         $this->setVar( '_MysqlCharset', reset( $charsets ) );
00445                 }
00446                 return Status::newGood();
00447         }
00448 
00449         public function preInstall() {
00450                 # Add our user callback to installSteps, right before the tables are created.
00451                 $callback = array(
00452                         'name' => 'user',
00453                         'callback' => array( $this, 'setupUser' ),
00454                 );
00455                 $this->parent->addInstallStep( $callback, 'tables' );
00456         }
00457 
00461         public function setupDatabase() {
00462                 $status = $this->getConnection();
00463                 if ( !$status->isOK() ) {
00464                         return $status;
00465                 }
00466                 $conn = $status->value;
00467                 $dbName = $this->getVar( 'wgDBname' );
00468                 if( !$conn->selectDB( $dbName ) ) {
00469                         $conn->query( "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ), __METHOD__ );
00470                         $conn->selectDB( $dbName );
00471                 }
00472                 $this->setupSchemaVars();
00473                 return $status;
00474         }
00475 
00479         public function setupUser() {
00480                 $dbUser = $this->getVar( 'wgDBuser' );
00481                 if( $dbUser == $this->getVar( '_InstallUser' ) ) {
00482                         return Status::newGood();
00483                 }
00484                 $status = $this->getConnection();
00485                 if ( !$status->isOK() ) {
00486                         return $status;
00487                 }
00488 
00489                 $this->setupSchemaVars();
00490                 $dbName = $this->getVar( 'wgDBname' );
00491                 $this->db->selectDB( $dbName );
00492                 $server = $this->getVar( 'wgDBserver' );
00493                 $password = $this->getVar( 'wgDBpassword' );
00494                 $grantableNames = array();
00495 
00496                 if ( $this->getVar( '_CreateDBAccount' ) ) {
00497                         // Before we blindly try to create a user that already has access,
00498                         try { // first attempt to connect to the database
00499                                 new DatabaseMysql(
00500                                         $server,
00501                                         $dbUser,
00502                                         $password,
00503                                         false,
00504                                         false,
00505                                         0,
00506                                         $this->getVar( 'wgDBprefix' )
00507                                 );
00508                                 $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
00509                                 $tryToCreate = false;
00510                         } catch ( DBConnectionError $e ) {
00511                                 $tryToCreate = true;
00512                         }
00513                 } else {
00514                         $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
00515                         $tryToCreate = false;
00516                 }
00517 
00518                 if( $tryToCreate ) {
00519                         $createHostList = array($server,
00520                                 'localhost',
00521                                 'localhost.localdomain',
00522                                 '%'
00523                         );
00524 
00525                         $createHostList = array_unique( $createHostList );
00526                         $escPass = $this->db->addQuotes( $password );
00527 
00528                         foreach( $createHostList as $host ) {
00529                                 $fullName = $this->buildFullUserName( $dbUser, $host );
00530                                 if( !$this->userDefinitelyExists( $dbUser, $host ) ) {
00531                                         try{
00532                                                 $this->db->begin( __METHOD__ );
00533                                                 $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
00534                                                 $this->db->commit( __METHOD__ );
00535                                                 $grantableNames[] = $fullName;
00536                                         } catch( DBQueryError $dqe ) {
00537                                                 if( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
00538                                                         // User (probably) already exists
00539                                                         $this->db->rollback( __METHOD__ );
00540                                                         $status->warning( 'config-install-user-alreadyexists', $dbUser );
00541                                                         $grantableNames[] = $fullName;
00542                                                         break;
00543                                                 } else {
00544                                                         // If we couldn't create for some bizzare reason and the
00545                                                         // user probably doesn't exist, skip the grant
00546                                                         $this->db->rollback( __METHOD__ );
00547                                                         $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() );
00548                                                 }
00549                                         }
00550                                 } else {
00551                                         $status->warning( 'config-install-user-alreadyexists', $dbUser );
00552                                         $grantableNames[] = $fullName;
00553                                         break;
00554                                 }
00555                         }
00556                 }
00557 
00558                 // Try to grant to all the users we know exist or we were able to create
00559                 $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*';
00560                 foreach( $grantableNames as $name ) {
00561                         try {
00562                                 $this->db->begin( __METHOD__ );
00563                                 $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
00564                                 $this->db->commit( __METHOD__ );
00565                         } catch( DBQueryError $dqe ) {
00566                                 $this->db->rollback( __METHOD__ );
00567                                 $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() );
00568                         }
00569                 }
00570 
00571                 return $status;
00572         }
00573 
00580         private function buildFullUserName( $name, $host ) {
00581                 return $this->db->addQuotes( $name ) . '@' . $this->db->addQuotes( $host );
00582         }
00583 
00591         private function userDefinitelyExists( $host, $user ) {
00592                 try {
00593                         $res = $this->db->selectRow( 'mysql.user', array( 'Host', 'User' ),
00594                                 array( 'Host' => $host, 'User' => $user ), __METHOD__ );
00595                         return (bool)$res;
00596                 } catch( DBQueryError $dqe ) {
00597                         return false;
00598                 }
00599 
00600         }
00601 
00608         protected function getTableOptions() {
00609                 $options = array();
00610                 if ( $this->getVar( '_MysqlEngine' ) !== null ) {
00611                         $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' );
00612                 }
00613                 if ( $this->getVar( '_MysqlCharset' ) !== null ) {
00614                         $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' );
00615                 }
00616                 return implode( ', ', $options );
00617         }
00618 
00624         public function getSchemaVars() {
00625                 return array(
00626                         'wgDBTableOptions' => $this->getTableOptions(),
00627                         'wgDBname' => $this->getVar( 'wgDBname' ),
00628                         'wgDBuser' => $this->getVar( 'wgDBuser' ),
00629                         'wgDBpassword' => $this->getVar( 'wgDBpassword' ),
00630                 );
00631         }
00632 
00633         public function getLocalSettings() {
00634                 $dbmysql5 = wfBoolToStr( $this->getVar( 'wgDBmysql5', true ) );
00635                 $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
00636                 $tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() );
00637                 return
00638 "# MySQL specific settings
00639 \$wgDBprefix         = \"{$prefix}\";
00640 
00641 # MySQL table options to use during installation or update
00642 \$wgDBTableOptions   = \"{$tblOpts}\";
00643 
00644 # Experimental charset support for MySQL 5.0.
00645 \$wgDBmysql5 = {$dbmysql5};";
00646         }
00647 }