]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/installer/MysqlInstaller.php
MediaWiki 1.17.0-scripts
[autoinstallsdev/mediawiki.git] / includes / installer / MysqlInstaller.php
1 <?php
2 /**
3  * MySQL-specific installer.
4  *
5  * @file
6  * @ingroup Deployment
7  */
8
9 /**
10  * Class for setting up the MediaWiki database using MySQL.
11  *
12  * @ingroup Deployment
13  * @since 1.17
14  */
15 class MysqlInstaller extends DatabaseInstaller {
16
17         protected $globalNames = array(
18                 'wgDBserver',
19                 'wgDBname',
20                 'wgDBuser',
21                 'wgDBpassword',
22                 'wgDBprefix',
23                 'wgDBTableOptions',
24                 'wgDBmysql5',
25         );
26
27         protected $internalDefaults = array(
28                 '_MysqlEngine' => 'InnoDB',
29                 '_MysqlCharset' => 'binary',
30                 '_InstallUser' => 'root',
31         );
32
33         public $supportedEngines = array( 'InnoDB', 'MyISAM' );
34
35         public $minimumVersion = '4.0.14';
36
37         public $webUserPrivs = array(
38                 'DELETE',
39                 'INSERT',
40                 'SELECT',
41                 'UPDATE',
42                 'CREATE TEMPORARY TABLES',
43         );
44
45         public function getName() {
46                 return 'mysql';
47         }
48
49         public function __construct( $parent ) {
50                 parent::__construct( $parent );
51         }
52
53         public function isCompiled() {
54                 return self::checkExtension( 'mysql' );
55         }
56
57         public function getGlobalDefaults() {
58                 return array();
59         }
60
61         public function getConnectForm() {
62                 return
63                         $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) .
64                         Html::openElement( 'fieldset' ) .
65                         Html::element( 'legend', array(), wfMsg( 'config-db-wiki-settings' ) ) .
66                         $this->getTextBox( 'wgDBname', 'config-db-name', array(), $this->parent->getHelpBox( 'config-db-name-help' ) ) .
67                         $this->getTextBox( 'wgDBprefix', 'config-db-prefix', array(), $this->parent->getHelpBox( 'config-db-prefix-help' ) ) .
68                         Html::closeElement( 'fieldset' ) .
69                         $this->getInstallUserBox();
70         }
71
72         public function submitConnectForm() {
73                 // Get variables from the request.
74                 $newValues = $this->setVarsFromRequest( array( 'wgDBserver', 'wgDBname', 'wgDBprefix' ) );
75
76                 // Validate them.
77                 $status = Status::newGood();
78                 if ( !strlen( $newValues['wgDBserver'] ) ) {
79                         $status->fatal( 'config-missing-db-host' );
80                 }
81                 if ( !strlen( $newValues['wgDBname'] ) ) {
82                         $status->fatal( 'config-missing-db-name' );
83                 } elseif ( !preg_match( '/^[a-z0-9_-]+$/i', $newValues['wgDBname'] ) ) {
84                         $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
85                 }
86                 if ( !preg_match( '/^[a-z0-9_-]*$/i', $newValues['wgDBprefix'] ) ) {
87                         $status->fatal( 'config-invalid-db-prefix', $newValues['wgDBprefix'] );
88                 }
89                 if ( !$status->isOK() ) {
90                         return $status;
91                 }
92
93                 // Submit user box
94                 $status = $this->submitInstallUserBox();
95                 if ( !$status->isOK() ) {
96                         return $status;
97                 }
98
99                 // Try to connect
100                 $status = $this->getConnection();
101                 if ( !$status->isOK() ) {
102                         return $status;
103                 }
104                 $conn = $status->value;
105
106                 // Check version
107                 $version = $conn->getServerVersion();
108                 if ( version_compare( $version, $this->minimumVersion ) < 0 ) {
109                         return Status::newFatal( 'config-mysql-old', $this->minimumVersion, $version );
110                 }
111
112                 return $status;
113         }
114
115         public function openConnection() {
116                 $status = Status::newGood();
117                 try {
118                         $db = new DatabaseMysql(
119                                 $this->getVar( 'wgDBserver' ),
120                                 $this->getVar( '_InstallUser' ),
121                                 $this->getVar( '_InstallPassword' ),
122                                 false,
123                                 false,
124                                 0,
125                                 $this->getVar( 'wgDBprefix' )
126                         );
127                         $status->value = $db;
128                 } catch ( DBConnectionError $e ) {
129                         $status->fatal( 'config-connection-error', $e->getMessage() );
130                 }
131                 return $status;
132         }
133
134         public function preUpgrade() {
135                 global $wgDBuser, $wgDBpassword;
136
137                 $status = $this->getConnection();
138                 if ( !$status->isOK() ) {
139                         $this->parent->showStatusError( $status );
140                         return;
141                 }
142                 $conn = $status->value;
143                 $conn->selectDB( $this->getVar( 'wgDBname' ) );
144
145                 # Determine existing default character set
146                 if ( $conn->tableExists( "revision" ) ) {
147                         $revision = $conn->buildLike( $this->getVar( 'wgDBprefix' ) . 'revision' );
148                         $res = $conn->query( "SHOW TABLE STATUS $revision", __METHOD__ );
149                         $row = $conn->fetchObject( $res );
150                         if ( !$row ) {
151                                 $this->parent->showMessage( 'config-show-table-status' );
152                                 $existingSchema = false;
153                                 $existingEngine = false;
154                         } else {
155                                 if ( preg_match( '/^latin1/', $row->Collation ) ) {
156                                         $existingSchema = 'mysql4';
157                                 } elseif ( preg_match( '/^utf8/', $row->Collation ) ) {
158                                         $existingSchema = 'utf8';
159                                 } elseif ( preg_match( '/^binary/', $row->Collation ) ) {
160                                         $existingSchema = 'binary';
161                                 } else {
162                                         $existingSchema = false;
163                                         $this->parent->showMessage( 'config-unknown-collation' );
164                                 }
165                                 if ( isset( $row->Engine ) ) {
166                                         $existingEngine = $row->Engine;
167                                 } else {
168                                         $existingEngine = $row->Type;
169                                 }
170                         }
171                 } else {
172                         $existingSchema = false;
173                         $existingEngine = false;
174                 }
175
176                 if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) {
177                         $this->setVar( '_MysqlCharset', $existingSchema );
178                 }
179                 if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) {
180                         $this->setVar( '_MysqlEngine', $existingEngine );
181                 }
182
183                 # Normal user and password are selected after this step, so for now
184                 # just copy these two
185                 $wgDBuser = $this->getVar( '_InstallUser' );
186                 $wgDBpassword = $this->getVar( '_InstallPassword' );
187         }
188
189         /**
190          * Get a list of storage engines that are available and supported
191          */
192         public function getEngines() {
193                 $engines = array( 'InnoDB', 'MyISAM' );
194                 $status = $this->getConnection();
195                 if ( !$status->isOK() ) {
196                         return $engines;
197                 }
198                 $conn = $status->value;
199
200                 $version = $conn->getServerVersion();
201                 if ( version_compare( $version, "4.1.2", "<" ) ) {
202                         // No SHOW ENGINES in this version
203                         return $engines;
204                 }
205
206                 $engines = array();
207                 $res = $conn->query( 'SHOW ENGINES', __METHOD__ );
208                 foreach ( $res as $row ) {
209                         if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) {
210                                 $engines[] = $row->Engine;
211                         }
212                 }
213                 $engines = array_intersect( $this->supportedEngines, $engines );
214                 return $engines;
215         }
216
217         /**
218          * Get a list of character sets that are available and supported
219          */
220         public function getCharsets() {
221                 $status = $this->getConnection();
222                 $mysql5 = array( 'binary', 'utf8' );
223                 $mysql4 = array( 'mysql4' );
224                 if ( !$status->isOK() ) {
225                         return $mysql5;
226                 }
227                 if ( version_compare( $status->value->getServerVersion(), '4.1.0', '>=' ) ) {
228                         return $mysql5;
229                 }
230                 return $mysql4;
231         }
232
233         /**
234          * Return true if the install user can create accounts
235          */
236         public function canCreateAccounts() {
237                 $status = $this->getConnection();
238                 if ( !$status->isOK() ) {
239                         return false;
240                 }
241                 $conn = $status->value;
242
243                 // Check version, need INFORMATION_SCHEMA and CREATE USER
244                 if ( version_compare( $conn->getServerVersion(), '5.0.2', '<' ) ) {
245                         return false;
246                 }
247
248                 // Get current account name
249                 $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ );
250                 $parts = explode( '@', $currentName );
251                 if ( count( $parts ) != 2 ) {
252                         return false;
253                 }
254                 $quotedUser = $conn->addQuotes( $parts[0] ) .
255                         '@' . $conn->addQuotes( $parts[1] );
256
257                 // The user needs to have INSERT on mysql.* to be able to CREATE USER
258                 // The grantee will be double-quoted in this query, as required
259                 $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*',
260                         array( 'GRANTEE' => $quotedUser ), __METHOD__ );
261                 $insertMysql = false;
262                 $grantOptions = array_flip( $this->webUserPrivs );
263                 foreach ( $res as $row ) {
264                         if ( $row->PRIVILEGE_TYPE == 'INSERT' ) {
265                                 $insertMysql = true;
266                         }
267                         if ( $row->IS_GRANTABLE ) {
268                                 unset( $grantOptions[$row->PRIVILEGE_TYPE] );
269                         }
270                 }
271
272                 // Check for DB-specific privs for mysql.*
273                 if ( !$insertMysql ) {
274                         $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
275                                 array(
276                                         'GRANTEE' => $quotedUser,
277                                         'TABLE_SCHEMA' => 'mysql',
278                                         'PRIVILEGE_TYPE' => 'INSERT',
279                                 ), __METHOD__ );
280                         if ( $row ) {
281                                 $insertMysql = true;
282                         }
283                 }
284
285                 if ( !$insertMysql ) {
286                         return false;
287                 }
288
289                 // Check for DB-level grant options
290                 $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
291                         array(
292                                 'GRANTEE' => $quotedUser,
293                                 'IS_GRANTABLE' => 1,
294                         ), __METHOD__ );
295                 foreach ( $res as $row ) {
296                         $regex = $conn->likeToRegex( $row->TABLE_SCHEMA );
297                         if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) {
298                                 unset( $grantOptions[$row->PRIVILEGE_TYPE] );
299                         }
300                 }
301                 if ( count( $grantOptions ) ) {
302                         // Can't grant everything
303                         return false;
304                 }
305                 return true;
306         }
307
308         public function getSettingsForm() {
309                 if ( $this->canCreateAccounts() ) {
310                         $noCreateMsg = false;
311                 } else {
312                         $noCreateMsg = 'config-db-web-no-create-privs';
313                 }
314                 $s = $this->getWebUserBox( $noCreateMsg );
315
316                 // Do engine selector
317                 $engines = $this->getEngines();
318                 // If the current default engine is not supported, use an engine that is
319                 if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
320                         $this->setVar( '_MysqlEngine', reset( $engines ) );
321                 }
322                 if ( count( $engines ) >= 2 ) {
323                         $s .= $this->getRadioSet( array(
324                                 'var' => '_MysqlEngine',
325                                 'label' => 'config-mysql-engine',
326                                 'itemLabelPrefix' => 'config-mysql-',
327                                 'values' => $engines
328                         ));
329                         $s .= $this->parent->getHelpBox( 'config-mysql-engine-help' );
330                 }
331
332                 // If the current default charset is not supported, use a charset that is
333                 $charsets = $this->getCharsets();
334                 if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
335                         $this->setVar( '_MysqlCharset', reset( $charsets ) );
336                 }
337
338                 // Do charset selector
339                 if ( count( $charsets ) >= 2 ) {
340                         $s .= $this->getRadioSet( array(
341                                 'var' => '_MysqlCharset',
342                                 'label' => 'config-mysql-charset',
343                                 'itemLabelPrefix' => 'config-mysql-',
344                                 'values' => $charsets
345                         ));
346                         $s .= $this->parent->getHelpBox( 'config-mysql-charset-help' );
347                 }
348
349                 return $s;
350         }
351
352         public function submitSettingsForm() {
353                 $this->setVarsFromRequest( array( '_MysqlEngine', '_MysqlCharset' ) );
354                 $status = $this->submitWebUserBox();
355                 if ( !$status->isOK() ) {
356                         return $status;
357                 }
358
359                 // Validate the create checkbox
360                 $canCreate = $this->canCreateAccounts();
361                 if ( !$canCreate ) {
362                         $this->setVar( '_CreateDBAccount', false );
363                         $create = false;
364                 } else {
365                         $create = $this->getVar( '_CreateDBAccount' );
366                 }
367
368                 if ( !$create ) {
369                         // Test the web account
370                         try {
371                                 new DatabaseMysql(
372                                         $this->getVar( 'wgDBserver' ),
373                                         $this->getVar( 'wgDBuser' ),
374                                         $this->getVar( 'wgDBpassword' ),
375                                         false,
376                                         false,
377                                         0,
378                                         $this->getVar( 'wgDBprefix' )
379                                 );
380                         } catch ( DBConnectionError $e ) {
381                                 return Status::newFatal( 'config-connection-error', $e->getMessage() );
382                         }
383                 }
384
385                 // Validate engines and charsets
386                 // This is done pre-submit already so it's just for security
387                 $engines = $this->getEngines();
388                 if ( !in_array( $this->getVar( '_MysqlEngine' ), $engines ) ) {
389                         $this->setVar( '_MysqlEngine', reset( $engines ) );
390                 }
391                 $charsets = $this->getCharsets();
392                 if ( !in_array( $this->getVar( '_MysqlCharset' ), $charsets ) ) {
393                         $this->setVar( '_MysqlCharset', reset( $charsets ) );
394                 }
395                 return Status::newGood();
396         }
397
398         public function preInstall() {
399                 # Add our user callback to installSteps, right before the tables are created.
400                 $callback = array(
401                         'name' => 'user',
402                         'callback' => array( $this, 'setupUser' ),
403                 );
404                 $this->parent->addInstallStep( $callback, 'tables' );
405         }
406
407         public function setupDatabase() {
408                 $status = $this->getConnection();
409                 if ( !$status->isOK() ) {
410                         return $status;
411                 }
412                 $conn = $status->value;
413                 $dbName = $this->getVar( 'wgDBname' );
414                 if( !$conn->selectDB( $dbName ) ) {
415                         $conn->query( "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ), __METHOD__ );
416                         $conn->selectDB( $dbName );
417                 }
418                 $this->setupSchemaVars();
419                 return $status;
420         }
421
422         public function setupUser() {
423                 $dbUser = $this->getVar( 'wgDBuser' );
424                 if( $dbUser == $this->getVar( '_InstallUser' ) ) {
425                         return Status::newGood();
426                 }
427                 $status = $this->getConnection();
428                 if ( !$status->isOK() ) {
429                         return $status;
430                 }
431
432                 $this->setupSchemaVars();
433                 $dbName = $this->getVar( 'wgDBname' );
434                 $this->db->selectDB( $dbName );
435                 $server = $this->getVar( 'wgDBserver' );
436                 $password = $this->getVar( 'wgDBpassword' );
437                 $grantableNames = array();
438
439                 if ( $this->getVar( '_CreateDBAccount' ) ) {
440                         // Before we blindly try to create a user that already has access,
441                         try { // first attempt to connect to the database
442                                 new DatabaseMysql(
443                                         $server,
444                                         $dbUser,
445                                         $password,
446                                         false,
447                                         false,
448                                         0,
449                                         $this->getVar( 'wgDBprefix' )
450                                 );
451                                 $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
452                                 $tryToCreate = false;
453                         } catch ( DBConnectionError $e ) {
454                                 $tryToCreate = true;
455                         }
456                 } else {
457                         $grantableNames[] = $this->buildFullUserName( $dbUser, $server );
458                         $tryToCreate = false;
459                 }
460
461                 if( $tryToCreate ) {
462                         $createHostList = array($server,
463                                 'localhost',
464                                 'localhost.localdomain',
465                                 '%'
466                         );
467
468                         $createHostList = array_unique( $createHostList );
469                         $escPass = $this->db->addQuotes( $password );
470
471                         foreach( $createHostList as $host ) {
472                                 $fullName = $this->buildFullUserName( $dbUser, $host );
473                                 if( !$this->userDefinitelyExists( $dbUser, $host ) ) {
474                                         try{
475                                                 $this->db->begin();
476                                                 $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
477                                                 $this->db->commit();
478                                                 $grantableNames[] = $fullName;
479                                         } catch( DBQueryError $dqe ) {
480                                                 if( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
481                                                         // User (probably) already exists
482                                                         $this->db->rollback();
483                                                         $status->warning( 'config-install-user-alreadyexists', $dbUser );
484                                                         $grantableNames[] = $fullName;
485                                                         break;
486                                                 } else {
487                                                         // If we couldn't create for some bizzare reason and the
488                                                         // user probably doesn't exist, skip the grant
489                                                         $this->db->rollback();
490                                                         $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() );
491                                                 }
492                                         }
493                                 } else {
494                                         $status->warning( 'config-install-user-alreadyexists', $dbUser );
495                                         $grantableNames[] = $fullName;
496                                         break;
497                                 }
498                         }
499                 }
500
501                 // Try to grant to all the users we know exist or we were able to create
502                 $escPass = $this->db->addQuotes( $password );
503                 $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*';
504                 foreach( $grantableNames as $name ) {
505                         try {
506                                 $this->db->begin();
507                                 $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
508                                 $this->db->commit();
509                         } catch( DBQueryError $dqe ) {
510                                 $this->db->rollback();
511                                 $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() );
512                         }
513                 }
514
515                 return $status;
516         }
517
518         /**
519          * Return a formal 'User'@'Host' username for use in queries
520          * @param $name String Username, quotes will be added
521          * @param $host String Hostname, quotes will be added
522          * @return String
523          */
524         private function buildFullUserName( $name, $host ) {
525                 return $this->db->addQuotes( $name ) . '@' . $this->db->addQuotes( $host );
526         }
527
528         /**
529          * Try to see if the user account exists. Our "superuser" may not have
530          * access to mysql.user, so false means "no" or "maybe"
531          * @param $host String Hostname to check
532          * @param $user String Username to check
533          * @return boolean
534          */
535         private function userDefinitelyExists( $host, $user ) {
536                 try {
537                         $res = $this->db->selectRow( 'mysql.user', array( 'Host', 'User' ),
538                                 array( 'Host' => $host, 'User' => $user ), __METHOD__ );
539                         return (bool)$res;
540                 } catch( DBQueryError $dqe ) {
541                         return false;
542                 }
543                 
544         }
545
546         /**
547          * Return any table options to be applied to all tables that don't
548          * override them.
549          *
550          * @return String
551          */
552         protected function getTableOptions() {
553                 $options = array();
554                 if ( $this->getVar( '_MysqlEngine' ) !== null ) {
555                         $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' );
556                 }
557                 if ( $this->getVar( '_MysqlCharset' ) !== null ) {
558                         $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' );
559                 }
560                 return implode( ', ', $options );
561         }
562
563         /**
564          * Get variables to substitute into tables.sql and the SQL patch files.
565          */
566         public function getSchemaVars() {
567                 return array(
568                         'wgDBTableOptions' => $this->getTableOptions(),
569                         'wgDBname' => $this->getVar( 'wgDBname' ),
570                         'wgDBuser' => $this->getVar( 'wgDBuser' ),
571                         'wgDBpassword' => $this->getVar( 'wgDBpassword' ),
572                 );
573         }
574
575         public function getLocalSettings() {
576                 $dbmysql5 = wfBoolToStr( $this->getVar( 'wgDBmysql5', true ) );
577                 $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
578                 $tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() );
579                 return
580 "# MySQL specific settings
581 \$wgDBprefix         = \"{$prefix}\";
582
583 # MySQL table options to use during installation or update
584 \$wgDBTableOptions   = \"{$tblOpts}\";
585
586 # Experimental charset support for MySQL 4.1/5.0.
587 \$wgDBmysql5 = {$dbmysql5};";
588         }
589 }