MediaWiki  master
DatabaseInstaller.php
Go to the documentation of this file.
1 <?php
26 use Wikimedia\AtEase\AtEase;
33 
41 abstract class DatabaseInstaller {
42 
48  public $parent;
49 
53  public static $minimumVersion;
54 
58  protected static $notMinimumVersionMessage;
59 
65  public $db = null;
66 
72  protected $internalDefaults = [];
73 
79  protected $globalNames = [];
80 
88  public static function meetsMinimumRequirement( IDatabase $conn ) {
89  $serverVersion = $conn->getServerVersion();
90  if ( version_compare( $serverVersion, static::$minimumVersion ) < 0 ) {
91  return Status::newFatal(
92  static::$notMinimumVersionMessage, static::$minimumVersion, $serverVersion
93  );
94  }
95 
96  return Status::newGood();
97  }
98 
102  abstract public function getName();
103 
107  abstract public function isCompiled();
108 
115  public function checkPrerequisites() {
116  return Status::newGood();
117  }
118 
126  abstract public function getConnectForm();
127 
137  abstract public function submitConnectForm();
138 
147  public function getSettingsForm() {
148  return false;
149  }
150 
158  public function submitSettingsForm() {
159  return Status::newGood();
160  }
161 
170  abstract public function openConnection();
171 
178  abstract public function setupDatabase();
179 
191  public function getConnection() {
192  if ( $this->db ) {
193  return Status::newGood( $this->db );
194  }
195 
196  $status = $this->openConnection();
197  if ( $status->isOK() ) {
198  $this->db = $status->value;
199  // Enable autocommit
200  $this->db->clearFlag( DBO_TRX );
201  $this->db->commit( __METHOD__ );
202  }
203 
204  return $status;
205  }
206 
215  private function stepApplySourceFile(
216  $sourceFileMethod,
217  $stepName,
218  $tableThatMustNotExist = false
219  ) {
220  $status = $this->getConnection();
221  if ( !$status->isOK() ) {
222  return $status;
223  }
224  $this->selectDatabase( $this->db, $this->getVar( 'wgDBname' ) );
225 
226  if ( $tableThatMustNotExist && $this->db->tableExists( $tableThatMustNotExist, __METHOD__ ) ) {
227  $status->warning( "config-$stepName-tables-exist" );
228  $this->enableLB();
229 
230  return $status;
231  }
232 
233  $this->db->setFlag( DBO_DDLMODE );
234  $this->db->begin( __METHOD__ );
235 
236  $error = $this->db->sourceFile(
237  call_user_func( [ $this, $sourceFileMethod ], $this->db )
238  );
239  if ( $error !== true ) {
240  $this->db->reportQueryError( $error, 0, '', __METHOD__ );
241  $this->db->rollback( __METHOD__ );
242  $status->fatal( "config-$stepName-tables-failed", $error );
243  } else {
244  $this->db->commit( __METHOD__ );
245  }
246  // Resume normal operations
247  if ( $status->isOK() ) {
248  $this->enableLB();
249  }
250 
251  return $status;
252  }
253 
260  public function createTables() {
261  return $this->stepApplySourceFile( 'getGeneratedSchemaPath', 'install', 'archive' );
262  }
263 
270  public function createManualTables() {
271  return $this->stepApplySourceFile( 'getSchemaPath', 'install-manual' );
272  }
273 
280  public function insertUpdateKeys() {
281  return $this->stepApplySourceFile( 'getUpdateKeysPath', 'updates', false );
282  }
283 
292  private function getSqlFilePath( $db, $filename ) {
293  global $IP;
294 
295  $dbmsSpecificFilePath = "$IP/maintenance/" . $db->getType() . "/$filename";
296  if ( file_exists( $dbmsSpecificFilePath ) ) {
297  return $dbmsSpecificFilePath;
298  } else {
299  return "$IP/maintenance/$filename";
300  }
301  }
302 
311  public function getSchemaPath( $db ) {
312  return $this->getSqlFilePath( $db, 'tables.sql' );
313  }
314 
322  public function getGeneratedSchemaPath( $db ) {
323  return $this->getSqlFilePath( $db, 'tables-generated.sql' );
324  }
325 
334  public function getUpdateKeysPath( $db ) {
335  return $this->getSqlFilePath( $db, 'update-keys.sql' );
336  }
337 
343  public function createExtensionTables() {
344  $status = $this->getConnection();
345  if ( !$status->isOK() ) {
346  return $status;
347  }
348 
349  // Now run updates to create tables for old extensions
350  $updater = DatabaseUpdater::newForDB( $this->db );
351  $updater->setAutoExtensionHookContainer( $this->parent->getAutoExtensionHookContainer() );
352  $updater->doUpdates( [ 'extensions' ] );
353 
354  return $status;
355  }
356 
362  abstract public function getLocalSettings();
363 
370  public function getSchemaVars() {
371  return [];
372  }
373 
382  public function setupSchemaVars() {
383  $status = $this->getConnection();
384  if ( $status->isOK() ) {
385  // @phan-suppress-next-line PhanUndeclaredMethod
386  $status->value->setSchemaVars( $this->getSchemaVars() );
387  } else {
388  $msg = __METHOD__ . ': unexpected error while establishing'
389  . ' a database connection with message: '
390  . $status->getMessage()->plain();
391  throw new MWException( $msg );
392  }
393  }
394 
400  public function enableLB() {
401  $status = $this->getConnection();
402  if ( !$status->isOK() ) {
403  throw new MWException( __METHOD__ . ': unexpected DB connection error' );
404  }
405  $connection = $status->value;
406 
407  $this->parent->resetMediaWikiServices( null, [
408  'DBLoadBalancerFactory' => static function () use ( $connection ) {
409  return LBFactorySingle::newFromConnection( $connection );
410  }
411  ] );
412  }
413 
420  public function doUpgrade() {
421  $this->setupSchemaVars();
422  $this->enableLB();
423 
424  $ret = true;
425  ob_start( [ $this, 'outputHandler' ] );
426  $up = DatabaseUpdater::newForDB( $this->db );
427  try {
428  $up->doUpdates();
429  $up->purgeCache();
430  } catch ( MWException $e ) {
431  // TODO: Remove special casing in favour of MWExceptionRenderer
432  echo "\nAn error occurred:\n";
433  echo $e->getText();
434  $ret = false;
435  } catch ( Exception $e ) {
436  echo "\nAn error occurred:\n";
437  echo $e->getMessage();
438  $ret = false;
439  }
440  ob_end_flush();
441 
442  return $ret;
443  }
444 
451  public function preInstall() {
452  }
453 
458  public function preUpgrade() {
459  }
460 
466  public function getGlobalNames() {
467  return $this->globalNames;
468  }
469 
476  public function __construct( $parent ) {
477  $this->parent = $parent;
478  }
479 
487  protected static function checkExtension( $name ) {
488  return extension_loaded( $name );
489  }
490 
496  public function getReadableName() {
497  // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
498  return wfMessage( 'config-type-' . $this->getName() )->text();
499  }
500 
507  public function getGlobalDefaults() {
508  $defaults = [];
509  foreach ( $this->getGlobalNames() as $var ) {
510  if ( isset( $GLOBALS[$var] ) ) {
511  $defaults[$var] = $GLOBALS[$var];
512  }
513  }
514  return $defaults;
515  }
516 
521  public function getInternalDefaults() {
523  }
524 
531  public function getVar( $var, $default = null ) {
532  $defaults = $this->getGlobalDefaults();
533  $internal = $this->getInternalDefaults();
534  if ( isset( $defaults[$var] ) ) {
535  $default = $defaults[$var];
536  } elseif ( isset( $internal[$var] ) ) {
537  $default = $internal[$var];
538  }
539 
540  return $this->parent->getVar( $var, $default );
541  }
542 
548  public function setVar( $name, $value ) {
549  $this->parent->setVar( $name, $value );
550  }
551 
562  public function getTextBox( $var, $label, $attribs = [], $helpData = "" ) {
563  $name = $this->getName() . '_' . $var;
564  $value = $this->getVar( $var );
565  if ( !isset( $attribs ) ) {
566  $attribs = [];
567  }
568 
569  return $this->parent->getTextBox( [
570  'var' => $var,
571  'label' => $label,
572  'attribs' => $attribs,
573  'controlName' => $name,
574  'value' => $value,
575  'help' => $helpData
576  ] );
577  }
578 
590  public function getPasswordBox( $var, $label, $attribs = [], $helpData = "" ) {
591  $name = $this->getName() . '_' . $var;
592  $value = $this->getVar( $var );
593  if ( !isset( $attribs ) ) {
594  $attribs = [];
595  }
596 
597  return $this->parent->getPasswordBox( [
598  'var' => $var,
599  'label' => $label,
600  'attribs' => $attribs,
601  'controlName' => $name,
602  'value' => $value,
603  'help' => $helpData
604  ] );
605  }
606 
616  public function getCheckBox( $var, $label, $attribs = [], $helpData = "" ) {
617  $name = $this->getName() . '_' . $var;
618  $value = $this->getVar( $var );
619 
620  return $this->parent->getCheckBox( [
621  'var' => $var,
622  'label' => $label,
623  'attribs' => $attribs,
624  'controlName' => $name,
625  'value' => $value,
626  'help' => $helpData
627  ] );
628  }
629 
642  public function getRadioSet( $params ) {
643  $params['controlName'] = $this->getName() . '_' . $params['var'];
644  $params['value'] = $this->getVar( $params['var'] );
645 
646  return $this->parent->getRadioSet( $params );
647  }
648 
656  public function setVarsFromRequest( $varNames ) {
657  return $this->parent->setVarsFromRequest( $varNames, $this->getName() . '_' );
658  }
659 
671  public function needsUpgrade() {
672  $status = $this->getConnection();
673  if ( !$status->isOK() ) {
674  return false;
675  }
676 
677  try {
678  $this->selectDatabase( $this->db, $this->getVar( 'wgDBname' ) );
679  } catch ( DBConnectionError $e ) {
680  // Don't catch DBConnectionError
681  throw $e;
682  } catch ( DBExpectedError $e ) {
683  return false;
684  }
685 
686  return $this->db->tableExists( 'cur', __METHOD__ ) ||
687  $this->db->tableExists( 'revision', __METHOD__ );
688  }
689 
695  public function getInstallUserBox() {
696  return Html::openElement( 'fieldset' ) .
697  Html::element( 'legend', [], wfMessage( 'config-db-install-account' )->text() ) .
698  // @phan-suppress-next-line SecurityCheck-DoubleEscaped taint cannot track the helpbox from the rest
699  $this->getTextBox(
700  '_InstallUser',
701  'config-db-username',
702  [ 'dir' => 'ltr' ],
703  $this->parent->getHelpBox( 'config-db-install-username' )
704  ) .
705  // @phan-suppress-next-line SecurityCheck-DoubleEscaped taint cannot track the helpbox from the rest
706  $this->getPasswordBox(
707  '_InstallPassword',
708  'config-db-password',
709  [ 'dir' => 'ltr' ],
710  $this->parent->getHelpBox( 'config-db-install-password' )
711  ) .
712  Html::closeElement( 'fieldset' );
713  }
714 
719  public function submitInstallUserBox() {
720  $this->setVarsFromRequest( [ '_InstallUser', '_InstallPassword' ] );
721 
722  return Status::newGood();
723  }
724 
732  public function getWebUserBox( $noCreateMsg = false ) {
733  $wrapperStyle = $this->getVar( '_SameAccount' ) ? 'display: none' : '';
734  $s = Html::openElement( 'fieldset' ) .
735  Html::element( 'legend', [], wfMessage( 'config-db-web-account' )->text() ) .
736  $this->getCheckBox(
737  '_SameAccount', 'config-db-web-account-same',
738  [ 'class' => 'hideShowRadio', 'rel' => 'dbOtherAccount' ]
739  ) .
740  Html::openElement( 'div', [ 'id' => 'dbOtherAccount', 'style' => $wrapperStyle ] ) .
741  $this->getTextBox( 'wgDBuser', 'config-db-username' ) .
742  $this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) .
743  $this->parent->getHelpBox( 'config-db-web-help' );
744  if ( $noCreateMsg ) {
745  $s .= Html::warningBox( wfMessage( $noCreateMsg )->plain(), 'config-warning-box' );
746  } else {
747  $s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
748  }
749  $s .= Html::closeElement( 'div' ) . Html::closeElement( 'fieldset' );
750 
751  return $s;
752  }
753 
759  public function submitWebUserBox() {
760  $this->setVarsFromRequest(
761  [ 'wgDBuser', 'wgDBpassword', '_SameAccount', '_CreateDBAccount' ]
762  );
763 
764  if ( $this->getVar( '_SameAccount' ) ) {
765  $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
766  $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
767  }
768 
769  if ( $this->getVar( '_CreateDBAccount' ) && strval( $this->getVar( 'wgDBpassword' ) ) == '' ) {
770  return Status::newFatal( 'config-db-password-empty', $this->getVar( 'wgDBuser' ) );
771  }
772 
773  return Status::newGood();
774  }
775 
782  public function populateInterwikiTable() {
783  $status = $this->getConnection();
784  if ( !$status->isOK() ) {
785  return $status;
786  }
787  $this->selectDatabase( $this->db, $this->getVar( 'wgDBname' ) );
788 
789  if ( $this->db->selectRow( 'interwiki', '1', [], __METHOD__ ) ) {
790  $status->warning( 'config-install-interwiki-exists' );
791 
792  return $status;
793  }
794  global $IP;
795  AtEase::suppressWarnings();
796  $rows = file( "$IP/maintenance/interwiki.list",
797  FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
798  AtEase::restoreWarnings();
799  $interwikis = [];
800  if ( !$rows ) {
801  return Status::newFatal( 'config-install-interwiki-list' );
802  }
803  foreach ( $rows as $row ) {
804  $row = preg_replace( '/^\s*([^#]*?)\s*(#.*)?$/', '\\1', $row ); // strip comments - whee
805  if ( $row == "" ) {
806  continue;
807  }
808  $row .= "|";
809  $interwikis[] = array_combine(
810  [ 'iw_prefix', 'iw_url', 'iw_local', 'iw_api', 'iw_wikiid' ],
811  explode( '|', $row )
812  );
813  }
814  $this->db->insert( 'interwiki', $interwikis, __METHOD__ );
815 
816  return Status::newGood();
817  }
818 
819  public function outputHandler( $string ) {
820  return htmlspecialchars( $string );
821  }
822 
829  protected function selectDatabase( Database $conn, string $database ) {
830  $schema = $conn->dbSchema();
831  $prefix = $conn->tablePrefix();
832 
833  $conn->selectDomain( new DatabaseDomain(
834  $database,
835  // DatabaseDomain uses null for unspecified schemas
836  ( $schema !== '' ) ? $schema : null,
837  $prefix
838  ) );
839 
840  return true;
841  }
842 }
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:96
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.
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.
static meetsMinimumRequirement(IDatabase $conn)
Whether the provided version meets the necessary requirements for this type.
getRadioSet( $params)
Get a set of labelled radio buttons.
static newForDB(IMaintainableDatabase $db, $shared=false, Maintenance $maintenance=null)
MediaWiki exception.
Definition: MWException.php:33
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:57
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
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:44
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition: Database.php:308
dbSchema( $schema=null)
Get/set the db schema.
Definition: Database.php:323
selectDomain( $domain)
Set the current domain (database, schema, and table prefix)
Definition: Database.php:1539
Manage a single hardcoded database connection.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36
getServerVersion()
A string describing the current software version, like from mysql_get_server_info()
getType()
Get the RDBMS type of the server (e.g.
const DBO_DDLMODE
Definition: defines.php:16
const DBO_TRX
Definition: defines.php:12