MediaWiki REL1_28
PostgresInstaller.php
Go to the documentation of this file.
1<?php
31
32 protected $globalNames = [
33 'wgDBserver',
34 'wgDBport',
35 'wgDBname',
36 'wgDBuser',
37 'wgDBpassword',
38 'wgDBmwschema',
39 ];
40
41 protected $internalDefaults = [
42 '_InstallUser' => 'postgres',
43 ];
44
45 public $minimumVersion = '8.3';
47
48 protected $pgConns = [];
49
50 function getName() {
51 return 'postgres';
52 }
53
54 public function isCompiled() {
55 return self::checkExtension( 'pgsql' );
56 }
57
58 function getConnectForm() {
59 return $this->getTextBox(
60 'wgDBserver',
61 'config-db-host',
62 [],
63 $this->parent->getHelpBox( 'config-db-host-help' )
64 ) .
65 $this->getTextBox( 'wgDBport', 'config-db-port' ) .
66 Html::openElement( 'fieldset' ) .
67 Html::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
68 $this->getTextBox(
69 'wgDBname',
70 'config-db-name',
71 [],
72 $this->parent->getHelpBox( 'config-db-name-help' )
73 ) .
74 $this->getTextBox(
75 'wgDBmwschema',
76 'config-db-schema',
77 [],
78 $this->parent->getHelpBox( 'config-db-schema-help' )
79 ) .
80 Html::closeElement( 'fieldset' ) .
81 $this->getInstallUserBox();
82 }
83
84 function submitConnectForm() {
85 // Get variables from the request
86 $newValues = $this->setVarsFromRequest( [
87 'wgDBserver',
88 'wgDBport',
89 'wgDBname',
90 'wgDBmwschema'
91 ] );
92
93 // Validate them
94 $status = Status::newGood();
95 if ( !strlen( $newValues['wgDBname'] ) ) {
96 $status->fatal( 'config-missing-db-name' );
97 } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
98 $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
99 }
100 if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
101 $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
102 }
103
104 // Submit user box
105 if ( $status->isOK() ) {
106 $status->merge( $this->submitInstallUserBox() );
107 }
108 if ( !$status->isOK() ) {
109 return $status;
110 }
111
112 $status = $this->getPgConnection( 'create-db' );
113 if ( !$status->isOK() ) {
114 return $status;
115 }
119 $conn = $status->value;
120
121 // Check version
122 $version = $conn->getServerVersion();
123 if ( version_compare( $version, $this->minimumVersion ) < 0 ) {
124 return Status::newFatal( 'config-postgres-old', $this->minimumVersion, $version );
125 }
126
127 $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
128 $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
129
130 return Status::newGood();
131 }
132
133 public function getConnection() {
134 $status = $this->getPgConnection( 'create-tables' );
135 if ( $status->isOK() ) {
136 $this->db = $status->value;
137 }
138
139 return $status;
140 }
141
142 public function openConnection() {
143 return $this->openPgConnection( 'create-tables' );
144 }
145
154 protected function openConnectionWithParams( $user, $password, $dbName, $schema ) {
155 $status = Status::newGood();
156 try {
157 $db = Database::factory( 'postgres', [
158 'host' => $this->getVar( 'wgDBserver' ),
159 'user' => $user,
160 'password' => $password,
161 'dbname' => $dbName,
162 'schema' => $schema ] );
163 $status->value = $db;
164 } catch ( DBConnectionError $e ) {
165 $status->fatal( 'config-connection-error', $e->getMessage() );
166 }
167
168 return $status;
169 }
170
176 protected function getPgConnection( $type ) {
177 if ( isset( $this->pgConns[$type] ) ) {
178 return Status::newGood( $this->pgConns[$type] );
179 }
180 $status = $this->openPgConnection( $type );
181
182 if ( $status->isOK() ) {
186 $conn = $status->value;
187 $conn->clearFlag( DBO_TRX );
188 $conn->commit( __METHOD__ );
189 $this->pgConns[$type] = $conn;
190 }
191
192 return $status;
193 }
194
220 protected function openPgConnection( $type ) {
221 switch ( $type ) {
222 case 'create-db':
223 return $this->openConnectionToAnyDB(
224 $this->getVar( '_InstallUser' ),
225 $this->getVar( '_InstallPassword' ) );
226 case 'create-schema':
227 return $this->openConnectionWithParams(
228 $this->getVar( '_InstallUser' ),
229 $this->getVar( '_InstallPassword' ),
230 $this->getVar( 'wgDBname' ),
231 $this->getVar( 'wgDBmwschema' ) );
232 case 'create-tables':
233 $status = $this->openPgConnection( 'create-schema' );
234 if ( $status->isOK() ) {
238 $conn = $status->value;
239 $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
240 $conn->query( "SET ROLE $safeRole" );
241 }
242
243 return $status;
244 default:
245 throw new MWException( "Invalid special connection type: \"$type\"" );
246 }
247 }
248
249 public function openConnectionToAnyDB( $user, $password ) {
250 $dbs = [
251 'template1',
252 'postgres',
253 ];
254 if ( !in_array( $this->getVar( 'wgDBname' ), $dbs ) ) {
255 array_unshift( $dbs, $this->getVar( 'wgDBname' ) );
256 }
257 $conn = false;
258 $status = Status::newGood();
259 foreach ( $dbs as $db ) {
260 try {
261 $p = [
262 'host' => $this->getVar( 'wgDBserver' ),
263 'user' => $user,
264 'password' => $password,
265 'dbname' => $db
266 ];
267 $conn = Database::factory( 'postgres', $p );
268 } catch ( DBConnectionError $error ) {
269 $conn = false;
270 $status->fatal( 'config-pg-test-error', $db,
271 $error->getMessage() );
272 }
273 if ( $conn !== false ) {
274 break;
275 }
276 }
277 if ( $conn !== false ) {
278 return Status::newGood( $conn );
279 } else {
280 return $status;
281 }
282 }
283
284 protected function getInstallUserPermissions() {
285 $status = $this->getPgConnection( 'create-db' );
286 if ( !$status->isOK() ) {
287 return false;
288 }
292 $conn = $status->value;
293 $superuser = $this->getVar( '_InstallUser' );
294
295 $row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*',
296 [ 'rolname' => $superuser ], __METHOD__ );
297
298 return $row;
299 }
300
301 protected function canCreateAccounts() {
302 $perms = $this->getInstallUserPermissions();
303 if ( !$perms ) {
304 return false;
305 }
306
307 return $perms->rolsuper === 't' || $perms->rolcreaterole === 't';
308 }
309
310 protected function isSuperUser() {
311 $perms = $this->getInstallUserPermissions();
312 if ( !$perms ) {
313 return false;
314 }
315
316 return $perms->rolsuper === 't';
317 }
318
319 public function getSettingsForm() {
320 if ( $this->canCreateAccounts() ) {
321 $noCreateMsg = false;
322 } else {
323 $noCreateMsg = 'config-db-web-no-create-privs';
324 }
325 $s = $this->getWebUserBox( $noCreateMsg );
326
327 return $s;
328 }
329
330 public function submitSettingsForm() {
331 $status = $this->submitWebUserBox();
332 if ( !$status->isOK() ) {
333 return $status;
334 }
335
336 $same = $this->getVar( 'wgDBuser' ) === $this->getVar( '_InstallUser' );
337
338 if ( $same ) {
339 $exists = true;
340 } else {
341 // Check if the web user exists
342 // Connect to the database with the install user
343 $status = $this->getPgConnection( 'create-db' );
344 if ( !$status->isOK() ) {
345 return $status;
346 }
347 $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) );
348 }
349
350 // Validate the create checkbox
351 if ( $this->canCreateAccounts() && !$same && !$exists ) {
352 $create = $this->getVar( '_CreateDBAccount' );
353 } else {
354 $this->setVar( '_CreateDBAccount', false );
355 $create = false;
356 }
357
358 if ( !$create && !$exists ) {
359 if ( $this->canCreateAccounts() ) {
360 $msg = 'config-install-user-missing-create';
361 } else {
362 $msg = 'config-install-user-missing';
363 }
364
365 return Status::newFatal( $msg, $this->getVar( 'wgDBuser' ) );
366 }
367
368 if ( !$exists ) {
369 // No more checks to do
370 return Status::newGood();
371 }
372
373 // Existing web account. Test the connection.
375 $this->getVar( 'wgDBuser' ),
376 $this->getVar( 'wgDBpassword' ) );
377 if ( !$status->isOK() ) {
378 return $status;
379 }
380
381 // The web user is conventionally the table owner in PostgreSQL
382 // installations. Make sure the install user is able to create
383 // objects on behalf of the web user.
384 if ( $same || $this->canCreateObjectsForWebUser() ) {
385 return Status::newGood();
386 } else {
387 return Status::newFatal( 'config-pg-not-in-role' );
388 }
389 }
390
396 protected function canCreateObjectsForWebUser() {
397 if ( $this->isSuperUser() ) {
398 return true;
399 }
400
401 $status = $this->getPgConnection( 'create-db' );
402 if ( !$status->isOK() ) {
403 return false;
404 }
405 $conn = $status->value;
406 $installerId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
407 [ 'rolname' => $this->getVar( '_InstallUser' ) ], __METHOD__ );
408 $webId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
409 [ 'rolname' => $this->getVar( 'wgDBuser' ) ], __METHOD__ );
410
411 return $this->isRoleMember( $conn, $installerId, $webId, $this->maxRoleSearchDepth );
412 }
413
422 protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) {
423 if ( $targetMember === $group ) {
424 // A role is always a member of itself
425 return true;
426 }
427 // Get all members of the given group
428 $res = $conn->select( '"pg_catalog"."pg_auth_members"', [ 'member' ],
429 [ 'roleid' => $group ], __METHOD__ );
430 foreach ( $res as $row ) {
431 if ( $row->member == $targetMember ) {
432 // Found target member
433 return true;
434 }
435 // Recursively search each member of the group to see if the target
436 // is a member of it, up to the given maximum depth.
437 if ( $maxDepth > 0 ) {
438 if ( $this->isRoleMember( $conn, $targetMember, $row->member, $maxDepth - 1 ) ) {
439 // Found member of member
440 return true;
441 }
442 }
443 }
444
445 return false;
446 }
447
448 public function preInstall() {
449 $createDbAccount = [
450 'name' => 'user',
451 'callback' => [ $this, 'setupUser' ],
452 ];
453 $commitCB = [
454 'name' => 'pg-commit',
455 'callback' => [ $this, 'commitChanges' ],
456 ];
457 $plpgCB = [
458 'name' => 'pg-plpgsql',
459 'callback' => [ $this, 'setupPLpgSQL' ],
460 ];
461 $schemaCB = [
462 'name' => 'schema',
463 'callback' => [ $this, 'setupSchema' ]
464 ];
465
466 if ( $this->getVar( '_CreateDBAccount' ) ) {
467 $this->parent->addInstallStep( $createDbAccount, 'database' );
468 }
469 $this->parent->addInstallStep( $commitCB, 'interwiki' );
470 $this->parent->addInstallStep( $plpgCB, 'database' );
471 $this->parent->addInstallStep( $schemaCB, 'database' );
472 }
473
474 function setupDatabase() {
475 $status = $this->getPgConnection( 'create-db' );
476 if ( !$status->isOK() ) {
477 return $status;
478 }
479 $conn = $status->value;
480
481 $dbName = $this->getVar( 'wgDBname' );
482
483 $exists = $conn->selectField( '"pg_catalog"."pg_database"', '1',
484 [ 'datname' => $dbName ], __METHOD__ );
485 if ( !$exists ) {
486 $safedb = $conn->addIdentifierQuotes( $dbName );
487 $conn->query( "CREATE DATABASE $safedb", __METHOD__ );
488 }
489
490 return Status::newGood();
491 }
492
493 function setupSchema() {
494 // Get a connection to the target database
495 $status = $this->getPgConnection( 'create-schema' );
496 if ( !$status->isOK() ) {
497 return $status;
498 }
499 $conn = $status->value;
500
501 // Create the schema if necessary
502 $schema = $this->getVar( 'wgDBmwschema' );
503 $safeschema = $conn->addIdentifierQuotes( $schema );
504 $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
505 if ( !$conn->schemaExists( $schema ) ) {
506 try {
507 $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" );
508 } catch ( DBQueryError $e ) {
509 return Status::newFatal( 'config-install-pg-schema-failed',
510 $this->getVar( '_InstallUser' ), $schema );
511 }
512 }
513
514 // Select the new schema in the current connection
515 $conn->determineCoreSchema( $schema );
516
517 return Status::newGood();
518 }
519
520 function commitChanges() {
521 $this->db->commit( __METHOD__ );
522
523 return Status::newGood();
524 }
525
526 function setupUser() {
527 if ( !$this->getVar( '_CreateDBAccount' ) ) {
528 return Status::newGood();
529 }
530
531 $status = $this->getPgConnection( 'create-db' );
532 if ( !$status->isOK() ) {
533 return $status;
534 }
535 $conn = $status->value;
536
537 $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
538 $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
539
540 // Check if the user already exists
541 $userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) );
542 if ( !$userExists ) {
543 // Create the user
544 try {
545 $sql = "CREATE ROLE $safeuser NOCREATEDB LOGIN PASSWORD $safepass";
546
547 // If the install user is not a superuser, we need to make the install
548 // user a member of the new user's group, so that the install user will
549 // be able to create a schema and other objects on behalf of the new user.
550 if ( !$this->isSuperUser() ) {
551 $sql .= ' ROLE' . $conn->addIdentifierQuotes( $this->getVar( '_InstallUser' ) );
552 }
553
554 $conn->query( $sql, __METHOD__ );
555 } catch ( DBQueryError $e ) {
556 return Status::newFatal( 'config-install-user-create-failed',
557 $this->getVar( 'wgDBuser' ), $e->getMessage() );
558 }
559 }
560
561 return Status::newGood();
562 }
563
564 function getLocalSettings() {
565 $port = $this->getVar( 'wgDBport' );
566 $schema = $this->getVar( 'wgDBmwschema' );
567
568 return "# Postgres specific settings
569\$wgDBport = \"{$port}\";
570\$wgDBmwschema = \"{$schema}\";";
571 }
572
573 public function preUpgrade() {
575
576 # Normal user and password are selected after this step, so for now
577 # just copy these two
578 $wgDBuser = $this->getVar( '_InstallUser' );
579 $wgDBpassword = $this->getVar( '_InstallPassword' );
580 }
581
582 public function createTables() {
583 $schema = $this->getVar( 'wgDBmwschema' );
584
585 $status = $this->getConnection();
586 if ( !$status->isOK() ) {
587 return $status;
588 }
589
591 $conn = $status->value;
592
593 if ( $conn->tableExists( 'archive' ) ) {
594 $status->warning( 'config-install-tables-exist' );
595 $this->enableLB();
596
597 return $status;
598 }
599
600 $conn->begin( __METHOD__ );
601
602 if ( !$conn->schemaExists( $schema ) ) {
603 $status->fatal( 'config-install-pg-schema-not-exist' );
604
605 return $status;
606 }
607 $error = $conn->sourceFile( $this->getSchemaPath( $conn ) );
608 if ( $error !== true ) {
609 $conn->reportQueryError( $error, 0, '', __METHOD__ );
610 $conn->rollback( __METHOD__ );
611 $status->fatal( 'config-install-tables-failed', $error );
612 } else {
613 $conn->commit( __METHOD__ );
614 }
615 // Resume normal operations
616 if ( $status->isOK() ) {
617 $this->enableLB();
618 }
619
620 return $status;
621 }
622
623 public function getGlobalDefaults() {
624 // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
625 // the use of a schema, so we need to set it here
626 return array_merge( parent::getGlobalDefaults(), [
627 'wgDBmwschema' => 'mediawiki',
628 ] );
629 }
630
631 public function setupPLpgSQL() {
632 // Connect as the install user, since it owns the database and so is
633 // the user that needs to run "CREATE LANGAUGE"
634 $status = $this->getPgConnection( 'create-schema' );
635 if ( !$status->isOK() ) {
636 return $status;
637 }
641 $conn = $status->value;
642
643 $exists = $conn->selectField( '"pg_catalog"."pg_language"', 1,
644 [ 'lanname' => 'plpgsql' ], __METHOD__ );
645 if ( $exists ) {
646 // Already exists, nothing to do
647 return Status::newGood();
648 }
649
650 // plpgsql is not installed, but if we have a pg_pltemplate table, we
651 // should be able to create it
652 $exists = $conn->selectField(
653 [ '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ],
654 1,
655 [
656 'pg_namespace.oid=relnamespace',
657 'nspname' => 'pg_catalog',
658 'relname' => 'pg_pltemplate',
659 ],
660 __METHOD__ );
661 if ( $exists ) {
662 try {
663 $conn->query( 'CREATE LANGUAGE plpgsql' );
664 } catch ( DBQueryError $e ) {
665 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
666 }
667 } else {
668 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
669 }
670
671 return Status::newGood();
672 }
673}
$wgDBuser
Database username.
$wgDBpassword
Database user's password.
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.
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.
createTables()
Create database tables from scratch.
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...
$res
Definition database.txt:21
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition design.txt:18
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition hooks.txt:1049
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:249
namespace are movable Hooks may change this value to override the return value of MWNamespace::isMovable(). 'NewDifferenceEngine' do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition hooks.txt:2568
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
returning false will NOT prevent logging $e
Definition hooks.txt:2110
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
const DBO_TRX
Definition defines.php:9