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 'wgDBmwschema',
45 ];
46
47 protected $internalDefaults = [
48 '_InstallUser' => 'postgres',
49 ];
50
51 public static $minimumVersion = '10';
52 protected static $notMinimumVersionMessage = 'config-postgres-old';
54
55 protected $pgConns = [];
56
57 public function getName() {
58 return 'postgres';
59 }
60
61 public function isCompiled() {
62 return self::checkExtension( 'pgsql' );
63 }
64
65 public function getConnectForm() {
66 return $this->getTextBox(
67 'wgDBserver',
68 'config-db-host',
69 [],
70 $this->parent->getHelpBox( 'config-db-host-help' )
71 ) .
72 $this->getTextBox( 'wgDBport', 'config-db-port' ) .
73 Html::openElement( 'fieldset' ) .
74 Html::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
75 $this->getTextBox(
76 'wgDBname',
77 'config-db-name',
78 [],
79 $this->parent->getHelpBox( 'config-db-name-help' )
80 ) .
81 $this->getTextBox(
82 'wgDBmwschema',
83 'config-db-schema',
84 [],
85 $this->parent->getHelpBox( 'config-db-schema-help' )
86 ) .
87 Html::closeElement( 'fieldset' ) .
88 $this->getInstallUserBox();
89 }
90
91 public function submitConnectForm() {
92 // Get variables from the request
93 $newValues = $this->setVarsFromRequest( [
94 'wgDBserver',
95 'wgDBport',
96 'wgDBname',
97 'wgDBmwschema'
98 ] );
99
100 // Validate them
101 $status = Status::newGood();
102 if ( !strlen( $newValues['wgDBname'] ) ) {
103 $status->fatal( 'config-missing-db-name' );
104 } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
105 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
106 }
107 if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
108 $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
109 }
110
111 // Submit user box
112 if ( $status->isOK() ) {
113 $status->merge( $this->submitInstallUserBox() );
114 }
115 if ( !$status->isOK() ) {
116 return $status;
117 }
118
119 $status = $this->getPgConnection( 'create-db' );
120 if ( !$status->isOK() ) {
121 return $status;
122 }
126 $conn = $status->value;
127
128 // Check version
129 $status = static::meetsMinimumRequirement( $conn );
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 = MediaWikiServices::getInstance()->getDatabaseFactory()->create( '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 $this->pgConns[$type] = $conn;
199 }
200
201 return $status;
202 }
203
229 protected function openPgConnection( $type ) {
230 switch ( $type ) {
231 case 'create-db':
232 return $this->openConnectionToAnyDB(
233 $this->getVar( '_InstallUser' ),
234 $this->getVar( '_InstallPassword' ) );
235 case 'create-schema':
236 return $this->openConnectionWithParams(
237 $this->getVar( '_InstallUser' ),
238 $this->getVar( '_InstallPassword' ),
239 $this->getVar( 'wgDBname' ),
240 $this->getVar( 'wgDBmwschema' ) );
241 case 'create-tables':
242 $status = $this->openPgConnection( 'create-schema' );
243 if ( $status->isOK() ) {
247 $conn = $status->value;
248 $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
249 $conn->query( "SET ROLE $safeRole", __METHOD__ );
250 }
251
252 return $status;
253 default:
254 throw new MWException( "Invalid special connection type: \"$type\"" );
255 }
256 }
257
258 public function openConnectionToAnyDB( $user, $password ) {
259 $dbs = [
260 'template1',
261 'postgres',
262 ];
263 if ( !in_array( $this->getVar( 'wgDBname' ), $dbs ) ) {
264 array_unshift( $dbs, $this->getVar( 'wgDBname' ) );
265 }
266 $conn = false;
267 $status = Status::newGood();
268 foreach ( $dbs as $db ) {
269 try {
270 $p = [
271 'host' => $this->getVar( 'wgDBserver' ),
272 'port' => $this->getVar( 'wgDBport' ),
273 'user' => $user,
274 'password' => $password,
275 'dbname' => $db
276 ];
277 $conn = Database::factory( 'postgres', $p );
278 } catch ( DBConnectionError $error ) {
279 $conn = false;
280 $status->fatal( 'config-pg-test-error', $db,
281 $error->getMessage() );
282 }
283 if ( $conn !== false ) {
284 break;
285 }
286 }
287 if ( $conn !== false ) {
288 return Status::newGood( $conn );
289 } else {
290 return $status;
291 }
292 }
293
294 protected function getInstallUserPermissions() {
295 $status = $this->getPgConnection( 'create-db' );
296 if ( !$status->isOK() ) {
297 return false;
298 }
302 $conn = $status->value;
303 $superuser = $this->getVar( '_InstallUser' );
304
305 $row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*',
306 [ 'rolname' => $superuser ], __METHOD__ );
307
308 return $row;
309 }
310
311 protected function canCreateAccounts() {
312 $perms = $this->getInstallUserPermissions();
313 if ( !$perms ) {
314 return false;
315 }
316
317 return $perms->rolsuper === 't' || $perms->rolcreaterole === 't';
318 }
319
320 protected function isSuperUser() {
321 $perms = $this->getInstallUserPermissions();
322 if ( !$perms ) {
323 return false;
324 }
325
326 return $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 $schema = $this->getVar( 'wgDBmwschema' );
582
583 return "# Postgres specific settings
584\$wgDBport = \"{$port}\";
585\$wgDBmwschema = \"{$schema}\";";
586 }
587
588 public function preUpgrade() {
589 global $wgDBuser, $wgDBpassword;
590
591 # Normal user and password are selected after this step, so for now
592 # just copy these two
593 $wgDBuser = $this->getVar( '_InstallUser' );
594 $wgDBpassword = $this->getVar( '_InstallPassword' );
595 }
596
597 public function createTables() {
598 $schema = $this->getVar( 'wgDBmwschema' );
599
600 $status = $this->getConnection();
601 if ( !$status->isOK() ) {
602 return $status;
603 }
604
606 $conn = $status->value;
607 '@phan-var DatabasePostgres $conn';
608
609 if ( $conn->tableExists( 'archive', __METHOD__ ) ) {
610 $status->warning( 'config-install-tables-exist' );
611 $this->enableLB();
612
613 return $status;
614 }
615
616 $conn->begin( __METHOD__ );
617
618 if ( !$conn->schemaExists( $schema ) ) {
619 $status->fatal( 'config-install-pg-schema-not-exist' );
620
621 return $status;
622 }
623
624 $error = $conn->sourceFile( $this->getGeneratedSchemaPath( $conn ) );
625 if ( $error !== true ) {
626 $conn->reportQueryError( $error, 0, '', __METHOD__ );
627 $conn->rollback( __METHOD__ );
628 $status->fatal( 'config-install-tables-failed', $error );
629 } else {
630 $error = $conn->sourceFile( $this->getSchemaPath( $conn ) );
631 if ( $error !== true ) {
632 $conn->reportQueryError( $error, 0, '', __METHOD__ );
633 $conn->rollback( __METHOD__ );
634 $status->fatal( 'config-install-tables-manual-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 createManualTables() {
648 // Already handled above. Do nothing.
649 return Status::newGood();
650 }
651
652 public function getGlobalDefaults() {
653 // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
654 // the use of a schema, so we need to set it here
655 return array_merge( parent::getGlobalDefaults(), [
656 'wgDBmwschema' => 'mediawiki',
657 ] );
658 }
659
660 public function setupPLpgSQL() {
661 // Connect as the install user, since it owns the database and so is
662 // the user that needs to run "CREATE LANGUAGE"
663 $status = $this->getPgConnection( 'create-schema' );
664 if ( !$status->isOK() ) {
665 return $status;
666 }
670 $conn = $status->value;
671
672 $exists = (bool)$conn->selectField( '"pg_catalog"."pg_language"', '1',
673 [ 'lanname' => 'plpgsql' ], __METHOD__ );
674 if ( $exists ) {
675 // Already exists, nothing to do
676 return Status::newGood();
677 }
678
679 // plpgsql is not installed, but if we have a pg_pltemplate table, we
680 // should be able to create it
681 $exists = (bool)$conn->selectField(
682 [ '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ],
683 '1',
684 [
685 'pg_namespace.oid=relnamespace',
686 'nspname' => 'pg_catalog',
687 'relname' => 'pg_pltemplate',
688 ],
689 __METHOD__ );
690 if ( $exists ) {
691 try {
692 $conn->query( 'CREATE LANGUAGE plpgsql', __METHOD__ );
693 } catch ( DBQueryError $e ) {
694 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
695 }
696 } else {
697 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
698 }
699
700 return Status::newGood();
701 }
702}
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.
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