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