MediaWiki 1.41.2
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';
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.
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