MediaWiki  master
DatabaseInstaller.php
Go to the documentation of this file.
1 <?php
25 use Wikimedia\AtEase\AtEase;
32 
40 abstract class DatabaseInstaller {
41 
47  public $parent;
48 
52  public static $minimumVersion;
53 
57  protected static $notMinimumVersionMessage;
58 
64  public $db = null;
65 
71  protected $internalDefaults = [];
72 
78  protected $globalNames = [];
79 
87  public static function meetsMinimumRequirement( $serverVersion ) {
88  if ( version_compare( $serverVersion, static::$minimumVersion ) < 0 ) {
89  return Status::newFatal(
90  static::$notMinimumVersionMessage, static::$minimumVersion, $serverVersion
91  );
92  }
93 
94  return Status::newGood();
95  }
96 
100  abstract public function getName();
101 
105  abstract public function isCompiled();
106 
113  public function checkPrerequisites() {
114  return Status::newGood();
115  }
116 
124  abstract public function getConnectForm();
125 
135  abstract public function submitConnectForm();
136 
145  public function getSettingsForm() {
146  return false;
147  }
148 
156  public function submitSettingsForm() {
157  return Status::newGood();
158  }
159 
168  abstract public function openConnection();
169 
176  abstract public function setupDatabase();
177 
189  public function getConnection() {
190  if ( $this->db ) {
191  return Status::newGood( $this->db );
192  }
193 
194  $status = $this->openConnection();
195  if ( $status->isOK() ) {
196  $this->db = $status->value;
197  // Enable autocommit
198  $this->db->clearFlag( DBO_TRX );
199  $this->db->commit( __METHOD__ );
200  }
201 
202  return $status;
203  }
204 
213  private function stepApplySourceFile(
214  $sourceFileMethod,
215  $stepName,
216  $tableThatMustNotExist = false
217  ) {
218  $status = $this->getConnection();
219  if ( !$status->isOK() ) {
220  return $status;
221  }
222  $this->selectDatabase( $this->db, $this->getVar( 'wgDBname' ) );
223 
224  if ( $tableThatMustNotExist && $this->db->tableExists( $tableThatMustNotExist, __METHOD__ ) ) {
225  $status->warning( "config-$stepName-tables-exist" );
226  $this->enableLB();
227 
228  return $status;
229  }
230 
231  $this->db->setFlag( DBO_DDLMODE );
232  $this->db->begin( __METHOD__ );
233 
234  $error = $this->db->sourceFile(
235  call_user_func( [ $this, $sourceFileMethod ], $this->db )
236  );
237  if ( $error !== true ) {
238  $this->db->reportQueryError( $error, 0, '', __METHOD__ );
239  $this->db->rollback( __METHOD__ );
240  $status->fatal( "config-$stepName-tables-failed", $error );
241  } else {
242  $this->db->commit( __METHOD__ );
243  }
244  // Resume normal operations
245  if ( $status->isOK() ) {
246  $this->enableLB();
247  }
248 
249  return $status;
250  }
251 
258  public function createTables() {
259  return $this->stepApplySourceFile( 'getGeneratedSchemaPath', 'install', 'archive' );
260  }
261 
268  public function createManualTables() {
269  return $this->stepApplySourceFile( 'getSchemaPath', 'install-manual' );
270  }
271 
278  public function insertUpdateKeys() {
279  return $this->stepApplySourceFile( 'getUpdateKeysPath', 'updates', false );
280  }
281 
290  private function getSqlFilePath( $db, $filename ) {
291  global $IP;
292 
293  $dbmsSpecificFilePath = "$IP/maintenance/" . $db->getType() . "/$filename";
294  if ( file_exists( $dbmsSpecificFilePath ) ) {
295  return $dbmsSpecificFilePath;
296  } else {
297  return "$IP/maintenance/$filename";
298  }
299  }
300 
309  public function getSchemaPath( $db ) {
310  return $this->getSqlFilePath( $db, 'tables.sql' );
311  }
312 
320  public function getGeneratedSchemaPath( $db ) {
321  return $this->getSqlFilePath( $db, 'tables-generated.sql' );
322  }
323 
332  public function getUpdateKeysPath( $db ) {
333  return $this->getSqlFilePath( $db, 'update-keys.sql' );
334  }
335 
341  public function createExtensionTables() {
342  $status = $this->getConnection();
343  if ( !$status->isOK() ) {
344  return $status;
345  }
346 
347  // Now run updates to create tables for old extensions
348  $updater = DatabaseUpdater::newForDB( $this->db );
349  $updater->setAutoExtensionHookContainer( $this->parent->getAutoExtensionHookContainer() );
350  $updater->doUpdates( [ 'extensions' ] );
351 
352  return $status;
353  }
354 
360  abstract public function getLocalSettings();
361 
368  public function getSchemaVars() {
369  return [];
370  }
371 
380  public function setupSchemaVars() {
381  $status = $this->getConnection();
382  if ( $status->isOK() ) {
383  // @phan-suppress-next-line PhanUndeclaredMethod
384  $status->value->setSchemaVars( $this->getSchemaVars() );
385  } else {
386  $msg = __METHOD__ . ': unexpected error while establishing'
387  . ' a database connection with message: '
388  . $status->getMessage()->plain();
389  throw new MWException( $msg );
390  }
391  }
392 
398  public function enableLB() {
399  $status = $this->getConnection();
400  if ( !$status->isOK() ) {
401  throw new MWException( __METHOD__ . ': unexpected DB connection error' );
402  }
403  $connection = $status->value;
404 
405  $this->parent->resetMediaWikiServices( null, [
406  'DBLoadBalancerFactory' => static function () use ( $connection ) {
407  return LBFactorySingle::newFromConnection( $connection );
408  }
409  ] );
410  }
411 
418  public function doUpgrade() {
419  $this->setupSchemaVars();
420  $this->enableLB();
421 
422  $ret = true;
423  ob_start( [ $this, 'outputHandler' ] );
424  $up = DatabaseUpdater::newForDB( $this->db );
425  try {
426  $up->doUpdates();
427  $up->purgeCache();
428  } catch ( MWException $e ) {
429  // TODO: Remove special casing in favour of MWExceptionRenderer
430  echo "\nAn error occurred:\n";
431  echo $e->getText();
432  $ret = false;
433  } catch ( Exception $e ) {
434  echo "\nAn error occurred:\n";
435  echo $e->getMessage();
436  $ret = false;
437  }
438  ob_end_flush();
439 
440  return $ret;
441  }
442 
449  public function preInstall() {
450  }
451 
456  public function preUpgrade() {
457  }
458 
464  public function getGlobalNames() {
465  return $this->globalNames;
466  }
467 
474  public function __construct( $parent ) {
475  $this->parent = $parent;
476  }
477 
485  protected static function checkExtension( $name ) {
486  return extension_loaded( $name );
487  }
488 
494  public function getReadableName() {
495  // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
496  return wfMessage( 'config-type-' . $this->getName() )->text();
497  }
498 
505  public function getGlobalDefaults() {
506  $defaults = [];
507  foreach ( $this->getGlobalNames() as $var ) {
508  if ( isset( $GLOBALS[$var] ) ) {
509  $defaults[$var] = $GLOBALS[$var];
510  }
511  }
512  return $defaults;
513  }
514 
519  public function getInternalDefaults() {
521  }
522 
529  public function getVar( $var, $default = null ) {
530  $defaults = $this->getGlobalDefaults();
531  $internal = $this->getInternalDefaults();
532  if ( isset( $defaults[$var] ) ) {
533  $default = $defaults[$var];
534  } elseif ( isset( $internal[$var] ) ) {
535  $default = $internal[$var];
536  }
537 
538  return $this->parent->getVar( $var, $default );
539  }
540 
546  public function setVar( $name, $value ) {
547  $this->parent->setVar( $name, $value );
548  }
549 
560  public function getTextBox( $var, $label, $attribs = [], $helpData = "" ) {
561  $name = $this->getName() . '_' . $var;
562  $value = $this->getVar( $var );
563  if ( !isset( $attribs ) ) {
564  $attribs = [];
565  }
566 
567  return $this->parent->getTextBox( [
568  'var' => $var,
569  'label' => $label,
570  'attribs' => $attribs,
571  'controlName' => $name,
572  'value' => $value,
573  'help' => $helpData
574  ] );
575  }
576 
588  public function getPasswordBox( $var, $label, $attribs = [], $helpData = "" ) {
589  $name = $this->getName() . '_' . $var;
590  $value = $this->getVar( $var );
591  if ( !isset( $attribs ) ) {
592  $attribs = [];
593  }
594 
595  return $this->parent->getPasswordBox( [
596  'var' => $var,
597  'label' => $label,
598  'attribs' => $attribs,
599  'controlName' => $name,
600  'value' => $value,
601  'help' => $helpData
602  ] );
603  }
604 
614  public function getCheckBox( $var, $label, $attribs = [], $helpData = "" ) {
615  $name = $this->getName() . '_' . $var;
616  $value = $this->getVar( $var );
617 
618  return $this->parent->getCheckBox( [
619  'var' => $var,
620  'label' => $label,
621  'attribs' => $attribs,
622  'controlName' => $name,
623  'value' => $value,
624  'help' => $helpData
625  ] );
626  }
627 
640  public function getRadioSet( $params ) {
641  $params['controlName'] = $this->getName() . '_' . $params['var'];
642  $params['value'] = $this->getVar( $params['var'] );
643 
644  return $this->parent->getRadioSet( $params );
645  }
646 
654  public function setVarsFromRequest( $varNames ) {
655  return $this->parent->setVarsFromRequest( $varNames, $this->getName() . '_' );
656  }
657 
669  public function needsUpgrade() {
670  $status = $this->getConnection();
671  if ( !$status->isOK() ) {
672  return false;
673  }
674 
675  try {
676  $this->selectDatabase( $this->db, $this->getVar( 'wgDBname' ) );
677  } catch ( DBConnectionError $e ) {
678  // Don't catch DBConnectionError
679  throw $e;
680  } catch ( DBExpectedError $e ) {
681  return false;
682  }
683 
684  return $this->db->tableExists( 'cur', __METHOD__ ) ||
685  $this->db->tableExists( 'revision', __METHOD__ );
686  }
687 
693  public function getInstallUserBox() {
694  return Html::openElement( 'fieldset' ) .
695  Html::element( 'legend', [], wfMessage( 'config-db-install-account' )->text() ) .
696  // @phan-suppress-next-line SecurityCheck-DoubleEscaped taint cannot track the helpbox from the rest
697  $this->getTextBox(
698  '_InstallUser',
699  'config-db-username',
700  [ 'dir' => 'ltr' ],
701  $this->parent->getHelpBox( 'config-db-install-username' )
702  ) .
703  // @phan-suppress-next-line SecurityCheck-DoubleEscaped taint cannot track the helpbox from the rest
704  $this->getPasswordBox(
705  '_InstallPassword',
706  'config-db-password',
707  [ 'dir' => 'ltr' ],
708  $this->parent->getHelpBox( 'config-db-install-password' )
709  ) .
710  Html::closeElement( 'fieldset' );
711  }
712 
717  public function submitInstallUserBox() {
718  $this->setVarsFromRequest( [ '_InstallUser', '_InstallPassword' ] );
719 
720  return Status::newGood();
721  }
722 
730  public function getWebUserBox( $noCreateMsg = false ) {
731  $wrapperStyle = $this->getVar( '_SameAccount' ) ? 'display: none' : '';
732  $s = Html::openElement( 'fieldset' ) .
733  Html::element( 'legend', [], wfMessage( 'config-db-web-account' )->text() ) .
734  $this->getCheckBox(
735  '_SameAccount', 'config-db-web-account-same',
736  [ 'class' => 'hideShowRadio', 'rel' => 'dbOtherAccount' ]
737  ) .
738  Html::openElement( 'div', [ 'id' => 'dbOtherAccount', 'style' => $wrapperStyle ] ) .
739  $this->getTextBox( 'wgDBuser', 'config-db-username' ) .
740  $this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) .
741  $this->parent->getHelpBox( 'config-db-web-help' );
742  if ( $noCreateMsg ) {
743  $s .= Html::warningBox( wfMessage( $noCreateMsg )->plain(), 'config-warning-box' );
744  } else {
745  $s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
746  }
747  $s .= Html::closeElement( 'div' ) . Html::closeElement( 'fieldset' );
748 
749  return $s;
750  }
751 
757  public function submitWebUserBox() {
758  $this->setVarsFromRequest(
759  [ 'wgDBuser', 'wgDBpassword', '_SameAccount', '_CreateDBAccount' ]
760  );
761 
762  if ( $this->getVar( '_SameAccount' ) ) {
763  $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
764  $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
765  }
766 
767  if ( $this->getVar( '_CreateDBAccount' ) && strval( $this->getVar( 'wgDBpassword' ) ) == '' ) {
768  return Status::newFatal( 'config-db-password-empty', $this->getVar( 'wgDBuser' ) );
769  }
770 
771  return Status::newGood();
772  }
773 
780  public function populateInterwikiTable() {
781  $status = $this->getConnection();
782  if ( !$status->isOK() ) {
783  return $status;
784  }
785  $this->selectDatabase( $this->db, $this->getVar( 'wgDBname' ) );
786 
787  if ( $this->db->selectRow( 'interwiki', '1', [], __METHOD__ ) ) {
788  $status->warning( 'config-install-interwiki-exists' );
789 
790  return $status;
791  }
792  global $IP;
793  AtEase::suppressWarnings();
794  $rows = file( "$IP/maintenance/interwiki.list",
795  FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
796  AtEase::restoreWarnings();
797  $interwikis = [];
798  if ( !$rows ) {
799  return Status::newFatal( 'config-install-interwiki-list' );
800  }
801  foreach ( $rows as $row ) {
802  $row = preg_replace( '/^\s*([^#]*?)\s*(#.*)?$/', '\\1', $row ); // strip comments - whee
803  if ( $row == "" ) {
804  continue;
805  }
806  $row .= "|";
807  $interwikis[] = array_combine(
808  [ 'iw_prefix', 'iw_url', 'iw_local', 'iw_api', 'iw_wikiid' ],
809  explode( '|', $row )
810  );
811  }
812  $this->db->insert( 'interwiki', $interwikis, __METHOD__ );
813 
814  return Status::newGood();
815  }
816 
817  public function outputHandler( $string ) {
818  return htmlspecialchars( $string );
819  }
820 
827  protected function selectDatabase( Database $conn, string $database ) {
828  $schema = $conn->dbSchema();
829  $prefix = $conn->tablePrefix();
830 
831  $conn->selectDomain( new DatabaseDomain(
832  $database,
833  // DatabaseDomain uses null for unspecified schemas
834  ( $schema !== '' ) ? $schema : null,
835  $prefix
836  ) );
837 
838  return true;
839  }
840 }
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition: Setup.php:93
Base class for DBMS-specific installation helper classes.
getWebUserBox( $noCreateMsg=false)
Get a standard web-user fieldset.
submitWebUserBox()
Submit the form from getWebUserBox().
static checkExtension( $name)
Convenience function.
preUpgrade()
Allow DB installers a chance to make checks before upgrade.
__construct( $parent)
Construct and initialise parent.
enableLB()
Set up LBFactory so that wfGetDB() etc.
getUpdateKeysPath( $db)
Return a path to the DBMS-specific update key file, otherwise default to update-keys....
getGlobalNames()
Get an array of MW configuration globals that will be configured by this class.
getReadableName()
Get the internationalised name for this DBMS.
WebInstaller $parent
The Installer object.
submitSettingsForm()
Set variables based on the request array, assuming it was submitted via the form return by getSetting...
selectDatabase(Database $conn, string $database)
Database $db
The database connection.
static meetsMinimumRequirement( $serverVersion)
Whether the provided version meets the necessary requirements for this type.
createExtensionTables()
Create the tables for each extension the user enabled.
getGeneratedSchemaPath( $db)
Return a path to the DBMS-specific automatically generated schema file.
static string $notMinimumVersionMessage
Set by subclasses.
setupDatabase()
Create the database and return a Status object indicating success or failure.
getPasswordBox( $var, $label, $attribs=[], $helpData="")
Get a labelled password box to configure a local variable.
setVarsFromRequest( $varNames)
Convenience function to set variables based on form data.
getSettingsForm()
Get HTML for a web form that retrieves settings used for installation.
static string $minimumVersion
Set by subclasses.
needsUpgrade()
Determine whether an existing installation of MediaWiki is present in the configured administrative c...
submitConnectForm()
Set variables based on the request array, assuming it was submitted via the form returned by getConne...
getCheckBox( $var, $label, $attribs=[], $helpData="")
Get a labelled checkbox to configure a local boolean variable.
getSchemaPath( $db)
Return a path to the DBMS-specific schema file, otherwise default to tables.sql.
getConnection()
Connect to the database using the administrative user/password currently defined in the session.
getSchemaVars()
Override this to provide DBMS-specific schema variables, to be substituted into tables....
getVar( $var, $default=null)
Get a variable, taking local defaults into account.
getTextBox( $var, $label, $attribs=[], $helpData="")
Get a labelled text box to configure a local variable.
preInstall()
Allow DB installers a chance to make last-minute changes before installation occurs.
createTables()
Create database tables from scratch from the automatically generated file.
populateInterwikiTable()
Common function for databases that don't understand the MySQLish syntax of interwiki....
array $internalDefaults
Internal variables for installation.
doUpgrade()
Perform database upgrades.
checkPrerequisites()
Checks for installation prerequisites other than those checked by isCompiled()
getInternalDefaults()
Get a name=>value map of internal variables used during installation.
setVar( $name, $value)
Convenience alias for $this->parent->setVar()
submitInstallUserBox()
Submit a standard install user fieldset.
getConnectForm()
Get HTML for a web form that configures this database.
openConnection()
Open a connection to the database using the administrative user/password currently defined in the ses...
getName()
Return the internal name, e.g.
array $globalNames
Array of MW configuration globals this class uses.
getLocalSettings()
Get the DBMS-specific options for LocalSettings.php generation.
getGlobalDefaults()
Get a name=>value map of MW configuration globals for the default values.
createManualTables()
Create database tables from scratch.
getInstallUserBox()
Get a standard install-user fieldset.
insertUpdateKeys()
Insert update keys into table to prevent running unneeded updates.
setupSchemaVars()
Set appropriate schema variables in the current database connection.
getRadioSet( $params)
Get a set of labelled radio buttons.
static newForDB(IMaintainableDatabase $db, $shared=false, Maintenance $maintenance=null)
MediaWiki exception.
Definition: MWException.php:32
getText()
Format plain text message for the current exception object.
This class is a collection of static functions that serve two purposes:
Definition: Html.php:55
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Base class for the more common types of database errors.
Class to handle database/schema/prefix specifications for IDatabase.
Relational database abstraction object.
Definition: Database.php:45
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition: Database.php:395
dbSchema( $schema=null)
Get/set the db schema.
Definition: Database.php:410
selectDomain( $domain)
Set the current domain (database, schema, and table prefix)
Definition: Database.php:1775
Manage a single hardcoded database connection.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36
getType()
Get the RDBMS type of the server (e.g.
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
const DBO_DDLMODE
Definition: defines.php:16
const DBO_TRX
Definition: defines.php:12