MediaWiki  master
PostgresInstaller.php
Go to the documentation of this file.
1 <?php
29 
37 
38  protected $globalNames = [
39  'wgDBserver',
40  'wgDBport',
41  'wgDBname',
42  'wgDBuser',
43  'wgDBpassword',
44  'wgDBmwschema',
45  ];
46 
47  protected $internalDefaults = [
48  '_InstallUser' => 'postgres',
49  ];
50 
51  public static $minimumVersion = '10';
52  protected static $notMinimumVersionMessage = 'config-postgres-old';
53  public $maxRoleSearchDepth = 5;
54 
55  protected $pgConns = [];
56 
57  public function getName() {
58  return 'postgres';
59  }
60 
61  public function isCompiled() {
62  return self::checkExtension( 'pgsql' );
63  }
64 
65  public function getConnectForm() {
66  return $this->getTextBox(
67  'wgDBserver',
68  'config-db-host',
69  [],
70  $this->parent->getHelpBox( 'config-db-host-help' )
71  ) .
72  $this->getTextBox( 'wgDBport', 'config-db-port' ) .
73  Html::openElement( 'fieldset' ) .
74  Html::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
75  $this->getTextBox(
76  'wgDBname',
77  'config-db-name',
78  [],
79  $this->parent->getHelpBox( 'config-db-name-help' )
80  ) .
81  $this->getTextBox(
82  'wgDBmwschema',
83  'config-db-schema',
84  [],
85  $this->parent->getHelpBox( 'config-db-schema-help' )
86  ) .
87  Html::closeElement( 'fieldset' ) .
88  $this->getInstallUserBox();
89  }
90 
91  public function submitConnectForm() {
92  // Get variables from the request
93  $newValues = $this->setVarsFromRequest( [
94  'wgDBserver',
95  'wgDBport',
96  'wgDBname',
97  'wgDBmwschema'
98  ] );
99 
100  // Validate them
101  $status = Status::newGood();
102  if ( !strlen( $newValues['wgDBname'] ) ) {
103  $status->fatal( 'config-missing-db-name' );
104  } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
105  $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
106  }
107  if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
108  $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
109  }
110 
111  // Submit user box
112  if ( $status->isOK() ) {
113  $status->merge( $this->submitInstallUserBox() );
114  }
115  if ( !$status->isOK() ) {
116  return $status;
117  }
118 
119  $status = $this->getPgConnection( 'create-db' );
120  if ( !$status->isOK() ) {
121  return $status;
122  }
126  $conn = $status->value;
127 
128  // Check version
129  $version = $conn->getServerVersion();
130  $status = static::meetsMinimumRequirement( $version );
131  if ( !$status->isOK() ) {
132  return $status;
133  }
134 
135  $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
136  $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
137 
138  return Status::newGood();
139  }
140 
141  public function getConnection() {
142  $status = $this->getPgConnection( 'create-tables' );
143  if ( $status->isOK() ) {
144  $this->db = $status->value;
145  }
146 
147  return $status;
148  }
149 
150  public function openConnection() {
151  return $this->openPgConnection( 'create-tables' );
152  }
153 
162  protected function openConnectionWithParams( $user, $password, $dbName, $schema ) {
163  $status = Status::newGood();
164  try {
165  $db = MediaWikiServices::getInstance()->getDatabaseFactory()->create( 'postgres', [
166  'host' => $this->getVar( 'wgDBserver' ),
167  'port' => $this->getVar( 'wgDBport' ),
168  'user' => $user,
169  'password' => $password,
170  'dbname' => $dbName,
171  'schema' => $schema,
172  ] );
173  $status->value = $db;
174  } catch ( DBConnectionError $e ) {
175  $status->fatal( 'config-connection-error', $e->getMessage() );
176  }
177 
178  return $status;
179  }
180 
186  protected function getPgConnection( $type ) {
187  if ( isset( $this->pgConns[$type] ) ) {
188  return Status::newGood( $this->pgConns[$type] );
189  }
190  $status = $this->openPgConnection( $type );
191 
192  if ( $status->isOK() ) {
196  $conn = $status->value;
197  $conn->clearFlag( DBO_TRX );
198  $conn->commit( __METHOD__ );
199  $this->pgConns[$type] = $conn;
200  }
201 
202  return $status;
203  }
204 
230  protected function openPgConnection( $type ) {
231  switch ( $type ) {
232  case 'create-db':
233  return $this->openConnectionToAnyDB(
234  $this->getVar( '_InstallUser' ),
235  $this->getVar( '_InstallPassword' ) );
236  case 'create-schema':
237  return $this->openConnectionWithParams(
238  $this->getVar( '_InstallUser' ),
239  $this->getVar( '_InstallPassword' ),
240  $this->getVar( 'wgDBname' ),
241  $this->getVar( 'wgDBmwschema' ) );
242  case 'create-tables':
243  $status = $this->openPgConnection( 'create-schema' );
244  if ( $status->isOK() ) {
248  $conn = $status->value;
249  $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
250  $conn->query( "SET ROLE $safeRole", __METHOD__ );
251  }
252 
253  return $status;
254  default:
255  throw new MWException( "Invalid special connection type: \"$type\"" );
256  }
257  }
258 
259  public function openConnectionToAnyDB( $user, $password ) {
260  $dbs = [
261  'template1',
262  'postgres',
263  ];
264  if ( !in_array( $this->getVar( 'wgDBname' ), $dbs ) ) {
265  array_unshift( $dbs, $this->getVar( 'wgDBname' ) );
266  }
267  $conn = false;
268  $status = Status::newGood();
269  foreach ( $dbs as $db ) {
270  try {
271  $p = [
272  'host' => $this->getVar( 'wgDBserver' ),
273  'port' => $this->getVar( 'wgDBport' ),
274  'user' => $user,
275  'password' => $password,
276  'dbname' => $db
277  ];
278  $conn = Database::factory( 'postgres', $p );
279  } catch ( DBConnectionError $error ) {
280  $conn = false;
281  $status->fatal( 'config-pg-test-error', $db,
282  $error->getMessage() );
283  }
284  if ( $conn !== false ) {
285  break;
286  }
287  }
288  if ( $conn !== false ) {
289  return Status::newGood( $conn );
290  } else {
291  return $status;
292  }
293  }
294 
295  protected function getInstallUserPermissions() {
296  $status = $this->getPgConnection( 'create-db' );
297  if ( !$status->isOK() ) {
298  return false;
299  }
303  $conn = $status->value;
304  $superuser = $this->getVar( '_InstallUser' );
305 
306  $row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*',
307  [ 'rolname' => $superuser ], __METHOD__ );
308 
309  return $row;
310  }
311 
312  protected function canCreateAccounts() {
313  $perms = $this->getInstallUserPermissions();
314  if ( !$perms ) {
315  return false;
316  }
317 
318  return $perms->rolsuper === 't' || $perms->rolcreaterole === 't';
319  }
320 
321  protected function isSuperUser() {
322  $perms = $this->getInstallUserPermissions();
323  if ( !$perms ) {
324  return false;
325  }
326 
327  return $perms->rolsuper === 't';
328  }
329 
330  public function getSettingsForm() {
331  if ( $this->canCreateAccounts() ) {
332  $noCreateMsg = false;
333  } else {
334  $noCreateMsg = 'config-db-web-no-create-privs';
335  }
336  $s = $this->getWebUserBox( $noCreateMsg );
337 
338  return $s;
339  }
340 
341  public function submitSettingsForm() {
342  $status = $this->submitWebUserBox();
343  if ( !$status->isOK() ) {
344  return $status;
345  }
346 
347  $same = $this->getVar( 'wgDBuser' ) === $this->getVar( '_InstallUser' );
348 
349  if ( $same ) {
350  $exists = true;
351  } else {
352  // Check if the web user exists
353  // Connect to the database with the install user
354  $status = $this->getPgConnection( 'create-db' );
355  if ( !$status->isOK() ) {
356  return $status;
357  }
358  // @phan-suppress-next-line PhanUndeclaredMethod
359  $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) );
360  }
361 
362  // Validate the create checkbox
363  if ( $this->canCreateAccounts() && !$same && !$exists ) {
364  $create = $this->getVar( '_CreateDBAccount' );
365  } else {
366  $this->setVar( '_CreateDBAccount', false );
367  $create = false;
368  }
369 
370  if ( !$create && !$exists ) {
371  if ( $this->canCreateAccounts() ) {
372  $msg = 'config-install-user-missing-create';
373  } else {
374  $msg = 'config-install-user-missing';
375  }
376 
377  return Status::newFatal( $msg, $this->getVar( 'wgDBuser' ) );
378  }
379 
380  if ( !$exists ) {
381  // No more checks to do
382  return Status::newGood();
383  }
384 
385  // Existing web account. Test the connection.
386  $status = $this->openConnectionToAnyDB(
387  $this->getVar( 'wgDBuser' ),
388  $this->getVar( 'wgDBpassword' ) );
389  if ( !$status->isOK() ) {
390  return $status;
391  }
392 
393  // The web user is conventionally the table owner in PostgreSQL
394  // installations. Make sure the install user is able to create
395  // objects on behalf of the web user.
396  if ( $same || $this->canCreateObjectsForWebUser() ) {
397  return Status::newGood();
398  } else {
399  return Status::newFatal( 'config-pg-not-in-role' );
400  }
401  }
402 
408  protected function canCreateObjectsForWebUser() {
409  if ( $this->isSuperUser() ) {
410  return true;
411  }
412 
413  $status = $this->getPgConnection( 'create-db' );
414  if ( !$status->isOK() ) {
415  return false;
416  }
417  $conn = $status->value;
418  $installerId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
419  [ 'rolname' => $this->getVar( '_InstallUser' ) ], __METHOD__ );
420  $webId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
421  [ 'rolname' => $this->getVar( 'wgDBuser' ) ], __METHOD__ );
422 
423  return $this->isRoleMember( $conn, $installerId, $webId, $this->maxRoleSearchDepth );
424  }
425 
434  protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) {
435  if ( $targetMember === $group ) {
436  // A role is always a member of itself
437  return true;
438  }
439  // Get all members of the given group
440  $res = $conn->select( '"pg_catalog"."pg_auth_members"', [ 'member' ],
441  [ 'roleid' => $group ], __METHOD__ );
442  foreach ( $res as $row ) {
443  if ( $row->member == $targetMember ) {
444  // Found target member
445  return true;
446  }
447  // Recursively search each member of the group to see if the target
448  // is a member of it, up to the given maximum depth.
449  if ( $maxDepth > 0 &&
450  $this->isRoleMember( $conn, $targetMember, $row->member, $maxDepth - 1 )
451  ) {
452  // Found member of member
453  return true;
454  }
455  }
456 
457  return false;
458  }
459 
460  public function preInstall() {
461  $createDbAccount = [
462  'name' => 'user',
463  'callback' => [ $this, 'setupUser' ],
464  ];
465  $commitCB = [
466  'name' => 'pg-commit',
467  'callback' => [ $this, 'commitChanges' ],
468  ];
469  $plpgCB = [
470  'name' => 'pg-plpgsql',
471  'callback' => [ $this, 'setupPLpgSQL' ],
472  ];
473  $schemaCB = [
474  'name' => 'schema',
475  'callback' => [ $this, 'setupSchema' ]
476  ];
477 
478  if ( $this->getVar( '_CreateDBAccount' ) ) {
479  $this->parent->addInstallStep( $createDbAccount, 'database' );
480  }
481  $this->parent->addInstallStep( $commitCB, 'interwiki' );
482  $this->parent->addInstallStep( $plpgCB, 'database' );
483  $this->parent->addInstallStep( $schemaCB, 'database' );
484  }
485 
486  public function setupDatabase() {
487  $status = $this->getPgConnection( 'create-db' );
488  if ( !$status->isOK() ) {
489  return $status;
490  }
491  $conn = $status->value;
492 
493  $dbName = $this->getVar( 'wgDBname' );
494 
495  $exists = (bool)$conn->selectField( '"pg_catalog"."pg_database"', '1',
496  [ 'datname' => $dbName ], __METHOD__ );
497  if ( !$exists ) {
498  $safedb = $conn->addIdentifierQuotes( $dbName );
499  $conn->query( "CREATE DATABASE $safedb", __METHOD__ );
500  }
501 
502  return Status::newGood();
503  }
504 
505  public function setupSchema() {
506  // Get a connection to the target database
507  $status = $this->getPgConnection( 'create-schema' );
508  if ( !$status->isOK() ) {
509  return $status;
510  }
512  $conn = $status->value;
513  '@phan-var DatabasePostgres $conn';
514 
515  // Create the schema if necessary
516  $schema = $this->getVar( 'wgDBmwschema' );
517  $safeschema = $conn->addIdentifierQuotes( $schema );
518  $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
519  if ( !$conn->schemaExists( $schema ) ) {
520  try {
521  $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser", __METHOD__ );
522  } catch ( DBQueryError $e ) {
523  return Status::newFatal( 'config-install-pg-schema-failed',
524  $this->getVar( '_InstallUser' ), $schema );
525  }
526  }
527 
528  // Select the new schema in the current connection
529  $conn->determineCoreSchema( $schema );
530 
531  return Status::newGood();
532  }
533 
534  public function commitChanges() {
535  $this->db->commit( __METHOD__ );
536 
537  return Status::newGood();
538  }
539 
540  public function setupUser() {
541  if ( !$this->getVar( '_CreateDBAccount' ) ) {
542  return Status::newGood();
543  }
544 
545  $status = $this->getPgConnection( 'create-db' );
546  if ( !$status->isOK() ) {
547  return $status;
548  }
550  $conn = $status->value;
551  '@phan-var DatabasePostgres $conn';
552 
553  $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
554  $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
555 
556  // Check if the user already exists
557  $userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) );
558  if ( !$userExists ) {
559  // Create the user
560  try {
561  $sql = "CREATE ROLE $safeuser NOCREATEDB LOGIN PASSWORD $safepass";
562 
563  // If the install user is not a superuser, we need to make the install
564  // user a member of the new user's group, so that the install user will
565  // be able to create a schema and other objects on behalf of the new user.
566  if ( !$this->isSuperUser() ) {
567  $sql .= ' ROLE' . $conn->addIdentifierQuotes( $this->getVar( '_InstallUser' ) );
568  }
569 
570  $conn->query( $sql, __METHOD__ );
571  } catch ( DBQueryError $e ) {
572  return Status::newFatal( 'config-install-user-create-failed',
573  $this->getVar( 'wgDBuser' ), $e->getMessage() );
574  }
575  }
576 
577  return Status::newGood();
578  }
579 
580  public function getLocalSettings() {
581  $port = $this->getVar( 'wgDBport' );
582  $schema = $this->getVar( 'wgDBmwschema' );
583 
584  return "# Postgres specific settings
585 \$wgDBport = \"{$port}\";
586 \$wgDBmwschema = \"{$schema}\";";
587  }
588 
589  public function preUpgrade() {
590  global $wgDBuser, $wgDBpassword;
591 
592  # Normal user and password are selected after this step, so for now
593  # just copy these two
594  $wgDBuser = $this->getVar( '_InstallUser' );
595  $wgDBpassword = $this->getVar( '_InstallPassword' );
596  }
597 
598  public function createTables() {
599  $schema = $this->getVar( 'wgDBmwschema' );
600 
601  $status = $this->getConnection();
602  if ( !$status->isOK() ) {
603  return $status;
604  }
605 
607  $conn = $status->value;
608  '@phan-var DatabasePostgres $conn';
609 
610  if ( $conn->tableExists( 'archive', __METHOD__ ) ) {
611  $status->warning( 'config-install-tables-exist' );
612  $this->enableLB();
613 
614  return $status;
615  }
616 
617  $conn->begin( __METHOD__ );
618 
619  if ( !$conn->schemaExists( $schema ) ) {
620  $status->fatal( 'config-install-pg-schema-not-exist' );
621 
622  return $status;
623  }
624 
625  $error = $conn->sourceFile( $this->getGeneratedSchemaPath( $conn ) );
626  if ( $error !== true ) {
627  $conn->reportQueryError( $error, 0, '', __METHOD__ );
628  $conn->rollback( __METHOD__ );
629  $status->fatal( 'config-install-tables-failed', $error );
630  } else {
631  $error = $conn->sourceFile( $this->getSchemaPath( $conn ) );
632  if ( $error !== true ) {
633  $conn->reportQueryError( $error, 0, '', __METHOD__ );
634  $conn->rollback( __METHOD__ );
635  $status->fatal( 'config-install-tables-manual-failed', $error );
636  } else {
637  $conn->commit( __METHOD__ );
638  }
639  }
640  // Resume normal operations
641  if ( $status->isOK() ) {
642  $this->enableLB();
643  }
644 
645  return $status;
646  }
647 
648  public function createManualTables() {
649  // Already handled above. Do nothing.
650  return Status::newGood();
651  }
652 
653  public function getGlobalDefaults() {
654  // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
655  // the use of a schema, so we need to set it here
656  return array_merge( parent::getGlobalDefaults(), [
657  'wgDBmwschema' => 'mediawiki',
658  ] );
659  }
660 
661  public function setupPLpgSQL() {
662  // Connect as the install user, since it owns the database and so is
663  // the user that needs to run "CREATE LANGUAGE"
664  $status = $this->getPgConnection( 'create-schema' );
665  if ( !$status->isOK() ) {
666  return $status;
667  }
671  $conn = $status->value;
672 
673  $exists = (bool)$conn->selectField( '"pg_catalog"."pg_language"', '1',
674  [ 'lanname' => 'plpgsql' ], __METHOD__ );
675  if ( $exists ) {
676  // Already exists, nothing to do
677  return Status::newGood();
678  }
679 
680  // plpgsql is not installed, but if we have a pg_pltemplate table, we
681  // should be able to create it
682  $exists = (bool)$conn->selectField(
683  [ '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ],
684  '1',
685  [
686  'pg_namespace.oid=relnamespace',
687  'nspname' => 'pg_catalog',
688  'relname' => 'pg_pltemplate',
689  ],
690  __METHOD__ );
691  if ( $exists ) {
692  try {
693  $conn->query( 'CREATE LANGUAGE plpgsql', __METHOD__ );
694  } catch ( DBQueryError $e ) {
695  return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
696  }
697  } else {
698  return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
699  }
700 
701  return Status::newGood();
702  }
703 }
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
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.
enableLB()
Set up LBFactory so that wfGetDB() etc.
Database $db
The database connection.
getGeneratedSchemaPath( $db)
Return a path to the DBMS-specific automatically generated schema file.
setVarsFromRequest( $varNames)
Convenience function to set variables based on form data.
getSchemaPath( $db)
Return a path to the DBMS-specific schema file, otherwise default to tables.sql.
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.
setVar( $name, $value)
Convenience alias for $this->parent->setVar()
submitInstallUserBox()
Submit a standard install user fieldset.
getInstallUserBox()
Get a standard install-user fieldset.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:236
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:256
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:320
MediaWiki exception.
Definition: MWException.php:29
Service locator for MediaWiki core services.
Class for setting up the MediaWiki database using Postgres.
getConnection()
Connect to the database using the administrative user/password currently defined in the session.
getPgConnection( $type)
Get a special type of connection.
openConnectionToAnyDB( $user, $password)
getName()
Return the internal name, e.g.
openConnectionWithParams( $user, $password, $dbName, $schema)
Open a PG connection with given parameters.
preUpgrade()
Allow DB installers a chance to make checks before upgrade.
setupDatabase()
Create the database and return a Status object indicating success or failure.
getGlobalDefaults()
Get a name=>value map of MW configuration globals for the default values.
createManualTables()
Create database tables from scratch.
createTables()
Create database tables from scratch from the automatically generated file.
getConnectForm()
Get HTML for a web form that configures this database.
submitSettingsForm()
Set variables based on the request array, assuming it was submitted via the form return by getSetting...
preInstall()
Allow DB installers a chance to make last-minute changes before installation occurs.
openConnection()
Open a connection to the database using the administrative user/password currently defined in the ses...
isRoleMember( $conn, $targetMember, $group, $maxDepth)
Recursive helper for canCreateObjectsForWebUser().
canCreateObjectsForWebUser()
Returns true if the install user is able to create objects owned by the web user, false otherwise.
getSettingsForm()
Get HTML for a web form that retrieves settings used for installation.
getLocalSettings()
Get the DBMS-specific options for LocalSettings.php generation.
openPgConnection( $type)
Get a connection of a specific PostgreSQL-specific type.
submitConnectForm()
Set variables based on the request array, assuming it was submitted via the form returned by getConne...
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
Postgres database abstraction layer.
$wgDBuser
Config variable stub for the DBuser setting, for use by phpdoc and IDEs.
$wgDBpassword
Config variable stub for the DBpassword setting, for use by phpdoc and IDEs.
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
const DBO_TRX
Definition: defines.php:12