MediaWiki  REL1_24
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 
00070     public function isCompiled() {
00071         return self::checkExtension( 'mysql' ) || self::checkExtension( 'mysqli' );
00072     }
00073 
00077     public function getGlobalDefaults() {
00078         return array();
00079     }
00080 
00084     public function getConnectForm() {
00085         return $this->getTextBox(
00086             'wgDBserver',
00087             'config-db-host',
00088             array(),
00089             $this->parent->getHelpBox( 'config-db-host-help' )
00090         ) .
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' ),
00094                 $this->parent->getHelpBox( 'config-db-name-help' ) ) .
00095             $this->getTextBox( 'wgDBprefix', 'config-db-prefix', array( 'dir' => 'ltr' ),
00096                 $this->parent->getHelpBox( 'config-db-prefix-help' ) ) .
00097             Html::closeElement( 'fieldset' ) .
00098             $this->getInstallUserBox();
00099     }
00100 
00101     public function submitConnectForm() {
00102         // Get variables from the request.
00103         $newValues = $this->setVarsFromRequest( array(
00104             'wgDBserver', 'wgDBname', 'wgDBprefix', '_InstallUser', '_InstallPassword'
00105         ) );
00106 
00107         // Validate them.
00108         $status = Status::newGood();
00109         if ( !strlen( $newValues['wgDBserver'] ) ) {
00110             $status->fatal( 'config-missing-db-host' );
00111         }
00112         if ( !strlen( $newValues['wgDBname'] ) ) {
00113             $status->fatal( 'config-missing-db-name' );
00114         } elseif ( !preg_match( '/^[a-z0-9+_-]+$/i', $newValues['wgDBname'] ) ) {
00115             $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
00116         }
00117         if ( !preg_match( '/^[a-z0-9_-]*$/i', $newValues['wgDBprefix'] ) ) {
00118             $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] );
00119         }
00120         if ( !strlen( $newValues['_InstallUser'] ) ) {
00121             $status->fatal( 'config-db-username-empty' );
00122         }
00123         if ( !strlen( $newValues['_InstallPassword'] ) ) {
00124             $status->fatal( 'config-db-password-empty', $newValues['_InstallUser'] );
00125         }
00126         if ( !$status->isOK() ) {
00127             return $status;
00128         }
00129 
00130         // Submit user box
00131         $status = $this->submitInstallUserBox();
00132         if ( !$status->isOK() ) {
00133             return $status;
00134         }
00135 
00136         // Try to connect
00137         $status = $this->getConnection();
00138         if ( !$status->isOK() ) {
00139             return $status;
00140         }
00144         $conn = $status->value;
00145 
00146         // Check version
00147         $version = $conn->getServerVersion();
00148         if ( version_compare( $version, $this->minimumVersion ) < 0 ) {
00149             return Status::newFatal( 'config-mysql-old', $this->minimumVersion, $version );
00150         }
00151 
00152         return $status;
00153     }
00154 
00158     public function openConnection() {
00159         $status = Status::newGood();
00160         try {
00161             $db = DatabaseBase::factory( 'mysql', array(
00162                 'host' => $this->getVar( 'wgDBserver' ),
00163                 'user' => $this->getVar( '_InstallUser' ),
00164                 'password' => $this->getVar( '_InstallPassword' ),
00165                 'dbname' => false,
00166                 'flags' => 0,
00167                 'tablePrefix' => $this->getVar( 'wgDBprefix' ) ) );
00168             $status->value = $db;
00169         } catch ( DBConnectionError $e ) {
00170             $status->fatal( 'config-connection-error', $e->getMessage() );
00171         }
00172 
00173         return $status;
00174     }
00175 
00176     public function preUpgrade() {
00177         global $wgDBuser, $wgDBpassword;
00178 
00179         $status = $this->getConnection();
00180         if ( !$status->isOK() ) {
00181             $this->parent->showStatusError( $status );
00182 
00183             return;
00184         }
00188         $conn = $status->value;
00189         $conn->selectDB( $this->getVar( 'wgDBname' ) );
00190 
00191         # Determine existing default character set
00192         if ( $conn->tableExists( "revision", __METHOD__ ) ) {
00193             $revision = $conn->buildLike( $this->getVar( 'wgDBprefix' ) . 'revision' );
00194             $res = $conn->query( "SHOW TABLE STATUS $revision", __METHOD__ );
00195             $row = $conn->fetchObject( $res );
00196             if ( !$row ) {
00197                 $this->parent->showMessage( 'config-show-table-status' );
00198                 $existingSchema = false;
00199                 $existingEngine = false;
00200             } else {
00201                 if ( preg_match( '/^latin1/', $row->Collation ) ) {
00202                     $existingSchema = 'latin1';
00203                 } elseif ( preg_match( '/^utf8/', $row->Collation ) ) {
00204                     $existingSchema = 'utf8';
00205                 } elseif ( preg_match( '/^binary/', $row->Collation ) ) {
00206                     $existingSchema = 'binary';
00207                 } else {
00208                     $existingSchema = false;
00209                     $this->parent->showMessage( 'config-unknown-collation' );
00210                 }
00211                 if ( isset( $row->Engine ) ) {
00212                     $existingEngine = $row->Engine;
00213                 } else {
00214                     $existingEngine = $row->Type;
00215                 }
00216             }
00217         } else {
00218             $existingSchema = false;
00219             $existingEngine = false;
00220         }
00221 
00222         if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) {
00223             $this->setVar( '_MysqlCharset', $existingSchema );
00224         }
00225         if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) {
00226             $this->setVar( '_MysqlEngine', $existingEngine );
00227         }
00228 
00229         # Normal user and password are selected after this step, so for now
00230         # just copy these two
00231         $wgDBuser = $this->getVar( '_InstallUser' );
00232         $wgDBpassword = $this->getVar( '_InstallPassword' );
00233     }
00234 
00240     public function getEngines() {
00241         $status = $this->getConnection();
00242 
00246         $conn = $status->value;
00247 
00248         $engines = array();
00249         $res = $conn->query( 'SHOW ENGINES', __METHOD__ );
00250         foreach ( $res as $row ) {
00251             if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) {
00252                 $engines[] = $row->Engine;
00253             }
00254         }
00255         $engines = array_intersect( $this->supportedEngines, $engines );
00256 
00257         return $engines;
00258     }
00259 
00265     public function getCharsets() {
00266         return array( 'binary', 'utf8' );
00267     }
00268 
00274     public function canCreateAccounts() {
00275         $status = $this->getConnection();
00276         if ( !$status->isOK() ) {
00277             return false;
00278         }
00280         $conn = $status->value;
00281 
00282         // Get current account name
00283         $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ );
00284         $parts = explode( '@', $currentName );
00285         if ( count( $parts ) != 2 ) {
00286             return false;
00287         }
00288         $quotedUser = $conn->addQuotes( $parts[0] ) .
00289             '@' . $conn->addQuotes( $parts[1] );
00290 
00291         // The user needs to have INSERT on mysql.* to be able to CREATE USER
00292         // The grantee will be double-quoted in this query, as required
00293         $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*',
00294             array( 'GRANTEE' => $quotedUser ), __METHOD__ );
00295         $insertMysql = false;
00296         $grantOptions = array_flip( $this->webUserPrivs );
00297         foreach ( $res as $row ) {
00298             if ( $row->PRIVILEGE_TYPE == 'INSERT' ) {
00299                 $insertMysql = true;
00300             }
00301             if ( $row->IS_GRANTABLE ) {
00302                 unset( $grantOptions[$row->PRIVILEGE_TYPE] );
00303             }
00304         }
00305 
00306         // Check for DB-specific privs for mysql.*
00307         if ( !$insertMysql ) {
00308             $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
00309                 array(
00310                     'GRANTEE' => $quotedUser,
00311                     'TABLE_SCHEMA' => 'mysql',
00312                     'PRIVILEGE_TYPE' => 'INSERT',
00313                 ), __METHOD__ );
00314             if ( $row ) {
00315                 $insertMysql = true;
00316             }
00317         }
00318 
00319         if ( !$insertMysql ) {
00320             return false;
00321         }
00322 
00323         // Check for DB-level grant options
00324         $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
00325             array(
00326                 'GRANTEE' => $quotedUser,
00327                 'IS_GRANTABLE' => 1,
00328             ), __METHOD__ );
00329         foreach ( $res as $row ) {
00330             $regex = $conn->likeToRegex( $row->TABLE_SCHEMA );
00331             if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) {
00332                 unset( $grantOptions[$row->PRIVILEGE_TYPE] );
00333             }
00334         }
00335         if ( count( $grantOptions ) ) {
00336             // Can't grant everything
00337             return false;
00338         }
00339 
00340         return true;
00341     }
00342 
00346     public function getSettingsForm() {
00347         if ( $this->canCreateAccounts() ) {
00348             $noCreateMsg = false;
00349         } else {
00350             $noCreateMsg = 'config-db-web-no-create-privs';
00351         }
00352         $s = $this->getWebUserBox( $noCreateMsg );
00353 
00354         // Do engine selector
00355         $engines = $this->getEngines();
00356         // If the current default engine is not supported, use an engine that is
00357         if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
00358             $this->setVar( '_MysqlEngine', reset( $engines ) );
00359         }
00360 
00361         $s .= Xml::openElement( 'div', array(
00362             'id' => 'dbMyisamWarning'
00363         ) );
00364         $myisamWarning = 'config-mysql-myisam-dep';
00365         if ( count( $engines ) === 1 ) {
00366             $myisamWarning = 'config-mysql-only-myisam-dep';
00367         }
00368         $s .= $this->parent->getWarningBox( wfMessage( $myisamWarning )->text() );
00369         $s .= Xml::closeElement( 'div' );
00370 
00371         if ( $this->getVar( '_MysqlEngine' ) != 'MyISAM' ) {
00372             $s .= Xml::openElement( 'script', array( 'type' => 'text/javascript' ) );
00373             $s .= '$(\'#dbMyisamWarning\').hide();';
00374             $s .= Xml::closeElement( 'script' );
00375         }
00376 
00377         if ( count( $engines ) >= 2 ) {
00378             // getRadioSet() builds a set of labeled radio buttons.
00379             // For grep: The following messages are used as the item labels:
00380             // config-mysql-innodb, config-mysql-myisam
00381             $s .= $this->getRadioSet( array(
00382                 'var' => '_MysqlEngine',
00383                 'label' => 'config-mysql-engine',
00384                 'itemLabelPrefix' => 'config-mysql-',
00385                 'values' => $engines,
00386                 'itemAttribs' => array(
00387                     'MyISAM' => array(
00388                         'class' => 'showHideRadio',
00389                         'rel' => 'dbMyisamWarning'
00390                     ),
00391                     'InnoDB' => array(
00392                         'class' => 'hideShowRadio',
00393                         'rel' => 'dbMyisamWarning'
00394                     )
00395                 )
00396             ) );
00397             $s .= $this->parent->getHelpBox( 'config-mysql-engine-help' );
00398         }
00399 
00400         // If the current default charset is not supported, use a charset that is
00401         $charsets = $this->getCharsets();
00402         if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
00403             $this->setVar( '_MysqlCharset', reset( $charsets ) );
00404         }
00405 
00406         // Do charset selector
00407         if ( count( $charsets ) >= 2 ) {
00408             // getRadioSet() builds a set of labeled radio buttons.
00409             // For grep: The following messages are used as the item labels:
00410             // config-mysql-binary, config-mysql-utf8
00411             $s .= $this->getRadioSet( array(
00412                 'var' => '_MysqlCharset',
00413                 'label' => 'config-mysql-charset',
00414                 'itemLabelPrefix' => 'config-mysql-',
00415                 'values' => $charsets
00416             ) );
00417             $s .= $this->parent->getHelpBox( 'config-mysql-charset-help' );
00418         }
00419 
00420         return $s;
00421     }
00422 
00426     public function submitSettingsForm() {
00427         $this->setVarsFromRequest( array( '_MysqlEngine', '_MysqlCharset' ) );
00428         $status = $this->submitWebUserBox();
00429         if ( !$status->isOK() ) {
00430             return $status;
00431         }
00432 
00433         // Validate the create checkbox
00434         $canCreate = $this->canCreateAccounts();
00435         if ( !$canCreate ) {
00436             $this->setVar( '_CreateDBAccount', false );
00437             $create = false;
00438         } else {
00439             $create = $this->getVar( '_CreateDBAccount' );
00440         }
00441 
00442         if ( !$create ) {
00443             // Test the web account
00444             try {
00445                 DatabaseBase::factory( 'mysql', array(
00446                     'host' => $this->getVar( 'wgDBserver' ),
00447                     'user' => $this->getVar( 'wgDBuser' ),
00448                     'password' => $this->getVar( 'wgDBpassword' ),
00449                     'dbname' => false,
00450                     'flags' => 0,
00451                     'tablePrefix' => $this->getVar( 'wgDBprefix' )
00452                 ) );
00453             } catch ( DBConnectionError $e ) {
00454                 return Status::newFatal( 'config-connection-error', $e->getMessage() );
00455             }
00456         }
00457 
00458         // Validate engines and charsets
00459         // This is done pre-submit already so it's just for security
00460         $engines = $this->getEngines();
00461         if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
00462             $this->setVar( '_MysqlEngine', reset( $engines ) );
00463         }
00464         $charsets = $this->getCharsets();
00465         if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
00466             $this->setVar( '_MysqlCharset', reset( $charsets ) );
00467         }
00468 
00469         return Status::newGood();
00470     }
00471 
00472     public function preInstall() {
00473         # Add our user callback to installSteps, right before the tables are created.
00474         $callback = array(
00475             'name' => 'user',
00476             'callback' => array( $this, 'setupUser' ),
00477         );
00478         $this->parent->addInstallStep( $callback, 'tables' );
00479     }
00480 
00484     public function setupDatabase() {
00485         $status = $this->getConnection();
00486         if ( !$status->isOK() ) {
00487             return $status;
00488         }
00490         $conn = $status->value;
00491         $dbName = $this->getVar( 'wgDBname' );
00492         if ( !$conn->selectDB( $dbName ) ) {
00493             $conn->query(
00494                 "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ) . "CHARACTER SET utf8",
00495                 __METHOD__
00496             );
00497             $conn->selectDB( $dbName );
00498         }
00499         $this->setupSchemaVars();
00500 
00501         return $status;
00502     }
00503 
00507     public function setupUser() {
00508         $dbUser = $this->getVar( 'wgDBuser' );
00509         if ( $dbUser == $this->getVar( '_InstallUser' ) ) {
00510             return Status::newGood();
00511         }
00512         $status = $this->getConnection();
00513         if ( !$status->isOK() ) {
00514             return $status;
00515         }
00516 
00517         $this->setupSchemaVars();
00518         $dbName = $this->getVar( 'wgDBname' );
00519         $this->db->selectDB( $dbName );
00520         $server = $this->getVar( 'wgDBserver' );
00521         $password = $this->getVar( 'wgDBpassword' );
00522         $grantableNames = array();
00523 
00524         if ( $this->getVar( '_CreateDBAccount' ) ) {
00525             // Before we blindly try to create a user that already has access,
00526             try { // first attempt to connect to the database
00527                 DatabaseBase::factory( 'mysql', array(
00528                     'host' => $server,
00529                     'user' => $dbUser,
00530                     'password' => $password,
00531                     'dbname' => false,
00532                     'flags' => 0,
00533                     'tablePrefix' => $this->getVar( 'wgDBprefix' )
00534                 ) );
00535                 $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
00536                 $tryToCreate = false;
00537             } catch ( DBConnectionError $e ) {
00538                 $tryToCreate = true;
00539             }
00540         } else {
00541             $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
00542             $tryToCreate = false;
00543         }
00544 
00545         if ( $tryToCreate ) {
00546             $createHostList = array(
00547                 $server,
00548                 'localhost',
00549                 'localhost.localdomain',
00550                 '%'
00551             );
00552 
00553             $createHostList = array_unique( $createHostList );
00554             $escPass = $this->db->addQuotes( $password );
00555 
00556             foreach ( $createHostList as $host ) {
00557                 $fullName = $this->buildFullUserName( $dbUser, $host );
00558                 if ( !$this->userDefinitelyExists( $dbUser, $host ) ) {
00559                     try {
00560                         $this->db->begin( __METHOD__ );
00561                         $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
00562                         $this->db->commit( __METHOD__ );
00563                         $grantableNames[] = $fullName;
00564                     } catch ( DBQueryError $dqe ) {
00565                         if ( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
00566                             // User (probably) already exists
00567                             $this->db->rollback( __METHOD__ );
00568                             $status->warning( 'config-install-user-alreadyexists', $dbUser );
00569                             $grantableNames[] = $fullName;
00570                             break;
00571                         } else {
00572                             // If we couldn't create for some bizzare reason and the
00573                             // user probably doesn't exist, skip the grant
00574                             $this->db->rollback( __METHOD__ );
00575                             $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() );
00576                         }
00577                     }
00578                 } else {
00579                     $status->warning( 'config-install-user-alreadyexists', $dbUser );
00580                     $grantableNames[] = $fullName;
00581                     break;
00582                 }
00583             }
00584         }
00585 
00586         // Try to grant to all the users we know exist or we were able to create
00587         $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*';
00588         foreach ( $grantableNames as $name ) {
00589             try {
00590                 $this->db->begin( __METHOD__ );
00591                 $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
00592                 $this->db->commit( __METHOD__ );
00593             } catch ( DBQueryError $dqe ) {
00594                 $this->db->rollback( __METHOD__ );
00595                 $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() );
00596             }
00597         }
00598 
00599         return $status;
00600     }
00601 
00608     private function buildFullUserName( $name, $host ) {
00609         return $this->db->addQuotes( $name ) . '@' . $this->db->addQuotes( $host );
00610     }
00611 
00619     private function userDefinitelyExists( $host, $user ) {
00620         try {
00621             $res = $this->db->selectRow( 'mysql.user', array( 'Host', 'User' ),
00622                 array( 'Host' => $host, 'User' => $user ), __METHOD__ );
00623 
00624             return (bool)$res;
00625         } catch ( DBQueryError $dqe ) {
00626             return false;
00627         }
00628     }
00629 
00636     protected function getTableOptions() {
00637         $options = array();
00638         if ( $this->getVar( '_MysqlEngine' ) !== null ) {
00639             $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' );
00640         }
00641         if ( $this->getVar( '_MysqlCharset' ) !== null ) {
00642             $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' );
00643         }
00644 
00645         return implode( ', ', $options );
00646     }
00647 
00653     public function getSchemaVars() {
00654         return array(
00655             'wgDBTableOptions' => $this->getTableOptions(),
00656             'wgDBname' => $this->getVar( 'wgDBname' ),
00657             'wgDBuser' => $this->getVar( 'wgDBuser' ),
00658             'wgDBpassword' => $this->getVar( 'wgDBpassword' ),
00659         );
00660     }
00661 
00662     public function getLocalSettings() {
00663         $dbmysql5 = wfBoolToStr( $this->getVar( 'wgDBmysql5', true ) );
00664         $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
00665         $tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() );
00666 
00667         return "# MySQL specific settings
00668 \$wgDBprefix = \"{$prefix}\";
00669 
00670 # MySQL table options to use during installation or update
00671 \$wgDBTableOptions = \"{$tblOpts}\";
00672 
00673 # Experimental charset support for MySQL 5.0.
00674 \$wgDBmysql5 = {$dbmysql5};";
00675     }
00676 }