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