MediaWiki REL1_35
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.2';
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 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ],
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 = $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 $error = $conn->sourceFile( $this->getSchemaPath( $conn ) );
625 if ( $error !== true ) {
626 $conn->reportQueryError( $error, 0, '', __METHOD__ );
627 $conn->rollback( __METHOD__ );
628 $status->fatal( 'config-install-tables-manual-failed', $error );
629 } else {
630 $error = $conn->sourceFile( $this->getGeneratedSchemaPath( $conn ) );
631 if ( $error !== true ) {
632 $conn->reportQueryError( $error, 0, '', __METHOD__ );
633 $conn->rollback( __METHOD__ );
634 $status->fatal( 'config-install-tables-failed', $error );
635 } else {
636 $conn->commit( __METHOD__ );
637 }
638 }
639 // Resume normal operations
640 if ( $status->isOK() ) {
641 $this->enableLB();
642 }
643
644 return $status;
645 }
646
647 public function getGlobalDefaults() {
648 // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
649 // the use of a schema, so we need to set it here
650 return array_merge( parent::getGlobalDefaults(), [
651 'wgDBmwschema' => 'mediawiki',
652 ] );
653 }
654
655 public function setupPLpgSQL() {
656 // Connect as the install user, since it owns the database and so is
657 // the user that needs to run "CREATE LANGUAGE"
658 $status = $this->getPgConnection( 'create-schema' );
659 if ( !$status->isOK() ) {
660 return $status;
661 }
665 $conn = $status->value;
666
667 $exists = $conn->selectField( '"pg_catalog"."pg_language"', '1',
668 [ 'lanname' => 'plpgsql' ], __METHOD__ );
669 if ( $exists ) {
670 // Already exists, nothing to do
671 return Status::newGood();
672 }
673
674 // plpgsql is not installed, but if we have a pg_pltemplate table, we
675 // should be able to create it
676 $exists = $conn->selectField(
677 [ '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ],
678 '1',
679 [
680 'pg_namespace.oid=relnamespace',
681 'nspname' => 'pg_catalog',
682 'relname' => 'pg_pltemplate',
683 ],
684 __METHOD__ );
685 if ( $exists ) {
686 try {
687 $conn->query( 'CREATE LANGUAGE plpgsql' );
688 } catch ( DBQueryError $e ) {
689 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
690 }
691 } else {
692 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
693 }
694
695 return Status::newGood();
696 }
697}
$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 Stable to override.
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.
createTables()
Create database tables from scratch from the automatically generated file Stable to override.
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...
@newable Stable to extend
Relational database abstraction object.
Definition Database.php:50
const DBO_TRX
Definition defines.php:12