MediaWiki REL1_37
PostgresInstaller.php
Go to the documentation of this file.
1<?php
28
36
37 protected $globalNames = [
38 'wgDBserver',
39 'wgDBport',
40 'wgDBname',
41 'wgDBuser',
42 'wgDBpassword',
43 'wgDBmwschema',
44 ];
45
46 protected $internalDefaults = [
47 '_InstallUser' => 'postgres',
48 ];
49
50 public static $minimumVersion = '9.4';
51 protected static $notMinimumVersionMessage = 'config-postgres-old';
53
54 protected $pgConns = [];
55
56 public function getName() {
57 return 'postgres';
58 }
59
60 public function isCompiled() {
61 return self::checkExtension( 'pgsql' );
62 }
63
64 public function getConnectForm() {
65 return $this->getTextBox(
66 'wgDBserver',
67 'config-db-host',
68 [],
69 $this->parent->getHelpBox( 'config-db-host-help' )
70 ) .
71 $this->getTextBox( 'wgDBport', 'config-db-port' ) .
72 Html::openElement( 'fieldset' ) .
73 Html::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
74 $this->getTextBox(
75 'wgDBname',
76 'config-db-name',
77 [],
78 $this->parent->getHelpBox( 'config-db-name-help' )
79 ) .
80 $this->getTextBox(
81 'wgDBmwschema',
82 'config-db-schema',
83 [],
84 $this->parent->getHelpBox( 'config-db-schema-help' )
85 ) .
86 Html::closeElement( 'fieldset' ) .
87 $this->getInstallUserBox();
88 }
89
90 public function submitConnectForm() {
91 // Get variables from the request
92 $newValues = $this->setVarsFromRequest( [
93 'wgDBserver',
94 'wgDBport',
95 'wgDBname',
96 'wgDBmwschema'
97 ] );
98
99 // Validate them
100 $status = Status::newGood();
101 if ( !strlen( $newValues['wgDBname'] ) ) {
102 $status->fatal( 'config-missing-db-name' );
103 } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
104 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
105 }
106 if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
107 $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
108 }
109
110 // Submit user box
111 if ( $status->isOK() ) {
112 $status->merge( $this->submitInstallUserBox() );
113 }
114 if ( !$status->isOK() ) {
115 return $status;
116 }
117
118 $status = $this->getPgConnection( 'create-db' );
119 if ( !$status->isOK() ) {
120 return $status;
121 }
125 $conn = $status->value;
126
127 // Check version
128 $version = $conn->getServerVersion();
129 $status = static::meetsMinimumRequirement( $version );
130 if ( !$status->isOK() ) {
131 return $status;
132 }
133
134 $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
135 $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
136
137 return Status::newGood();
138 }
139
140 public function getConnection() {
141 $status = $this->getPgConnection( 'create-tables' );
142 if ( $status->isOK() ) {
143 $this->db = $status->value;
144 }
145
146 return $status;
147 }
148
149 public function openConnection() {
150 return $this->openPgConnection( 'create-tables' );
151 }
152
161 protected function openConnectionWithParams( $user, $password, $dbName, $schema ) {
162 $status = Status::newGood();
163 try {
164 $db = Database::factory( 'postgres', [
165 'host' => $this->getVar( 'wgDBserver' ),
166 'port' => $this->getVar( 'wgDBport' ),
167 'user' => $user,
168 'password' => $password,
169 'dbname' => $dbName,
170 'schema' => $schema,
171 ] );
172 $status->value = $db;
173 } catch ( DBConnectionError $e ) {
174 $status->fatal( 'config-connection-error', $e->getMessage() );
175 }
176
177 return $status;
178 }
179
185 protected function getPgConnection( $type ) {
186 if ( isset( $this->pgConns[$type] ) ) {
187 return Status::newGood( $this->pgConns[$type] );
188 }
189 $status = $this->openPgConnection( $type );
190
191 if ( $status->isOK() ) {
195 $conn = $status->value;
196 $conn->clearFlag( DBO_TRX );
197 $conn->commit( __METHOD__ );
198 // @phan-suppress-next-line SecurityCheck-DoubleEscaped
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}
$wgDBuser
Database username.
$wgDBpassword
Database user's password.
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.
MediaWiki exception.
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...
Relational database abstraction object.
Definition Database.php:52
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s
const DBO_TRX
Definition defines.php:12