]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/installer/PostgresInstaller.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / includes / installer / PostgresInstaller.php
1 <?php
2 /**
3  * PostgreSQL-specific installer.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Deployment
22  */
23
24 use Wikimedia\Rdbms\Database;
25 use Wikimedia\Rdbms\DBQueryError;
26 use Wikimedia\Rdbms\DBConnectionError;
27
28 /**
29  * Class for setting up the MediaWiki database using Postgres.
30  *
31  * @ingroup Deployment
32  * @since 1.17
33  */
34 class PostgresInstaller extends DatabaseInstaller {
35
36         protected $globalNames = [
37                 'wgDBserver',
38                 'wgDBport',
39                 'wgDBname',
40                 'wgDBuser',
41                 'wgDBpassword',
42                 'wgDBmwschema',
43         ];
44
45         protected $internalDefaults = [
46                 '_InstallUser' => 'postgres',
47         ];
48
49         public static $minimumVersion = '9.1';
50         protected static $notMiniumumVerisonMessage = 'config-postgres-old';
51         public $maxRoleSearchDepth = 5;
52
53         protected $pgConns = [];
54
55         function getName() {
56                 return 'postgres';
57         }
58
59         public function isCompiled() {
60                 return self::checkExtension( 'pgsql' );
61         }
62
63         function getConnectForm() {
64                 return $this->getTextBox(
65                         'wgDBserver',
66                         'config-db-host',
67                         [],
68                         $this->parent->getHelpBox( 'config-db-host-help' )
69                 ) .
70                         $this->getTextBox( 'wgDBport', 'config-db-port' ) .
71                         Html::openElement( 'fieldset' ) .
72                         Html::element( 'legend', [], wfMessage( 'config-db-wiki-settings' )->text() ) .
73                         $this->getTextBox(
74                                 'wgDBname',
75                                 'config-db-name',
76                                 [],
77                                 $this->parent->getHelpBox( 'config-db-name-help' )
78                         ) .
79                         $this->getTextBox(
80                                 'wgDBmwschema',
81                                 'config-db-schema',
82                                 [],
83                                 $this->parent->getHelpBox( 'config-db-schema-help' )
84                         ) .
85                         Html::closeElement( 'fieldset' ) .
86                         $this->getInstallUserBox();
87         }
88
89         function submitConnectForm() {
90                 // Get variables from the request
91                 $newValues = $this->setVarsFromRequest( [
92                         'wgDBserver',
93                         'wgDBport',
94                         'wgDBname',
95                         'wgDBmwschema'
96                 ] );
97
98                 // Validate them
99                 $status = Status::newGood();
100                 if ( !strlen( $newValues['wgDBname'] ) ) {
101                         $status->fatal( 'config-missing-db-name' );
102                 } elseif ( !preg_match( '/^[a-zA-Z0-9_]+$/', $newValues['wgDBname'] ) ) {
103                         $status->fatal( 'config-invalid-db-name', $newValues['wgDBname'] );
104                 }
105                 if ( !preg_match( '/^[a-zA-Z0-9_]*$/', $newValues['wgDBmwschema'] ) ) {
106                         $status->fatal( 'config-invalid-schema', $newValues['wgDBmwschema'] );
107                 }
108
109                 // Submit user box
110                 if ( $status->isOK() ) {
111                         $status->merge( $this->submitInstallUserBox() );
112                 }
113                 if ( !$status->isOK() ) {
114                         return $status;
115                 }
116
117                 $status = $this->getPgConnection( 'create-db' );
118                 if ( !$status->isOK() ) {
119                         return $status;
120                 }
121                 /**
122                  * @var $conn Database
123                  */
124                 $conn = $status->value;
125
126                 // Check version
127                 $version = $conn->getServerVersion();
128                 $status = static::meetsMinimumRequirement( $conn->getServerVersion() );
129                 if ( !$status->isOK() ) {
130                         return $status;
131                 }
132
133                 $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
134                 $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
135
136                 return Status::newGood();
137         }
138
139         public function getConnection() {
140                 $status = $this->getPgConnection( 'create-tables' );
141                 if ( $status->isOK() ) {
142                         $this->db = $status->value;
143                 }
144
145                 return $status;
146         }
147
148         public function openConnection() {
149                 return $this->openPgConnection( 'create-tables' );
150         }
151
152         /**
153          * Open a PG connection with given parameters
154          * @param string $user User name
155          * @param string $password Password
156          * @param string $dbName Database name
157          * @param string $schema Database schema
158          * @return Status
159          */
160         protected function openConnectionWithParams( $user, $password, $dbName, $schema ) {
161                 $status = Status::newGood();
162                 try {
163                         $db = Database::factory( 'postgres', [
164                                 'host' => $this->getVar( 'wgDBserver' ),
165                                 'port' => $this->getVar( 'wgDBport' ),
166                                 'user' => $user,
167                                 'password' => $password,
168                                 'dbname' => $dbName,
169                                 'schema' => $schema,
170                                 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ],
171                         ] );
172                         $status->value = $db;
173                 } catch ( DBConnectionError $e ) {
174                         $status->fatal( 'config-connection-error', $e->getMessage() );
175                 }
176
177                 return $status;
178         }
179
180         /**
181          * Get a special type of connection
182          * @param string $type See openPgConnection() for details.
183          * @return Status
184          */
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() ) {
192                         /**
193                          * @var $conn Database
194                          */
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
204         /**
205          * Get a connection of a specific PostgreSQL-specific type. Connections
206          * of a given type are cached.
207          *
208          * PostgreSQL lacks cross-database operations, so after the new database is
209          * created, you need to make a separate connection to connect to that
210          * database and add tables to it.
211          *
212          * New tables are owned by the user that creates them, and MediaWiki's
213          * PostgreSQL support has always assumed that the table owner will be
214          * $wgDBuser. So before we create new tables, we either need to either
215          * connect as the other user or to execute a SET ROLE command. Using a
216          * separate connection for this allows us to avoid accidental cross-module
217          * dependencies.
218          *
219          * @param string $type The type of connection to get:
220          *    - create-db:     A connection for creating DBs, suitable for pre-
221          *                     installation.
222          *    - create-schema: A connection to the new DB, for creating schemas and
223          *                     other similar objects in the new DB.
224          *    - create-tables: A connection with a role suitable for creating tables.
225          *
226          * @throws MWException
227          * @return Status On success, a connection object will be in the value member.
228          */
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() ) {
244                                         /**
245                                          * @var $conn Database
246                                          */
247                                         $conn = $status->value;
248                                         $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
249                                         $conn->query( "SET ROLE $safeRole" );
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                                         'user' => $user,
273                                         'password' => $password,
274                                         'dbname' => $db
275                                 ];
276                                 $conn = Database::factory( 'postgres', $p );
277                         } catch ( DBConnectionError $error ) {
278                                 $conn = false;
279                                 $status->fatal( 'config-pg-test-error', $db,
280                                         $error->getMessage() );
281                         }
282                         if ( $conn !== false ) {
283                                 break;
284                         }
285                 }
286                 if ( $conn !== false ) {
287                         return Status::newGood( $conn );
288                 } else {
289                         return $status;
290                 }
291         }
292
293         protected function getInstallUserPermissions() {
294                 $status = $this->getPgConnection( 'create-db' );
295                 if ( !$status->isOK() ) {
296                         return false;
297                 }
298                 /**
299                  * @var $conn Database
300                  */
301                 $conn = $status->value;
302                 $superuser = $this->getVar( '_InstallUser' );
303
304                 $row = $conn->selectRow( '"pg_catalog"."pg_roles"', '*',
305                         [ 'rolname' => $superuser ], __METHOD__ );
306
307                 return $row;
308         }
309
310         protected function canCreateAccounts() {
311                 $perms = $this->getInstallUserPermissions();
312                 if ( !$perms ) {
313                         return false;
314                 }
315
316                 return $perms->rolsuper === 't' || $perms->rolcreaterole === 't';
317         }
318
319         protected function isSuperUser() {
320                 $perms = $this->getInstallUserPermissions();
321                 if ( !$perms ) {
322                         return false;
323                 }
324
325                 return $perms->rolsuper === 't';
326         }
327
328         public function getSettingsForm() {
329                 if ( $this->canCreateAccounts() ) {
330                         $noCreateMsg = false;
331                 } else {
332                         $noCreateMsg = 'config-db-web-no-create-privs';
333                 }
334                 $s = $this->getWebUserBox( $noCreateMsg );
335
336                 return $s;
337         }
338
339         public function submitSettingsForm() {
340                 $status = $this->submitWebUserBox();
341                 if ( !$status->isOK() ) {
342                         return $status;
343                 }
344
345                 $same = $this->getVar( 'wgDBuser' ) === $this->getVar( '_InstallUser' );
346
347                 if ( $same ) {
348                         $exists = true;
349                 } else {
350                         // Check if the web user exists
351                         // Connect to the database with the install user
352                         $status = $this->getPgConnection( 'create-db' );
353                         if ( !$status->isOK() ) {
354                                 return $status;
355                         }
356                         $exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) );
357                 }
358
359                 // Validate the create checkbox
360                 if ( $this->canCreateAccounts() && !$same && !$exists ) {
361                         $create = $this->getVar( '_CreateDBAccount' );
362                 } else {
363                         $this->setVar( '_CreateDBAccount', false );
364                         $create = false;
365                 }
366
367                 if ( !$create && !$exists ) {
368                         if ( $this->canCreateAccounts() ) {
369                                 $msg = 'config-install-user-missing-create';
370                         } else {
371                                 $msg = 'config-install-user-missing';
372                         }
373
374                         return Status::newFatal( $msg, $this->getVar( 'wgDBuser' ) );
375                 }
376
377                 if ( !$exists ) {
378                         // No more checks to do
379                         return Status::newGood();
380                 }
381
382                 // Existing web account. Test the connection.
383                 $status = $this->openConnectionToAnyDB(
384                         $this->getVar( 'wgDBuser' ),
385                         $this->getVar( 'wgDBpassword' ) );
386                 if ( !$status->isOK() ) {
387                         return $status;
388                 }
389
390                 // The web user is conventionally the table owner in PostgreSQL
391                 // installations. Make sure the install user is able to create
392                 // objects on behalf of the web user.
393                 if ( $same || $this->canCreateObjectsForWebUser() ) {
394                         return Status::newGood();
395                 } else {
396                         return Status::newFatal( 'config-pg-not-in-role' );
397                 }
398         }
399
400         /**
401          * Returns true if the install user is able to create objects owned
402          * by the web user, false otherwise.
403          * @return bool
404          */
405         protected function canCreateObjectsForWebUser() {
406                 if ( $this->isSuperUser() ) {
407                         return true;
408                 }
409
410                 $status = $this->getPgConnection( 'create-db' );
411                 if ( !$status->isOK() ) {
412                         return false;
413                 }
414                 $conn = $status->value;
415                 $installerId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
416                         [ 'rolname' => $this->getVar( '_InstallUser' ) ], __METHOD__ );
417                 $webId = $conn->selectField( '"pg_catalog"."pg_roles"', 'oid',
418                         [ 'rolname' => $this->getVar( 'wgDBuser' ) ], __METHOD__ );
419
420                 return $this->isRoleMember( $conn, $installerId, $webId, $this->maxRoleSearchDepth );
421         }
422
423         /**
424          * Recursive helper for canCreateObjectsForWebUser().
425          * @param Database $conn
426          * @param int $targetMember Role ID of the member to look for
427          * @param int $group Role ID of the group to look for
428          * @param int $maxDepth Maximum recursive search depth
429          * @return bool
430          */
431         protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) {
432                 if ( $targetMember === $group ) {
433                         // A role is always a member of itself
434                         return true;
435                 }
436                 // Get all members of the given group
437                 $res = $conn->select( '"pg_catalog"."pg_auth_members"', [ 'member' ],
438                         [ 'roleid' => $group ], __METHOD__ );
439                 foreach ( $res as $row ) {
440                         if ( $row->member == $targetMember ) {
441                                 // Found target member
442                                 return true;
443                         }
444                         // Recursively search each member of the group to see if the target
445                         // is a member of it, up to the given maximum depth.
446                         if ( $maxDepth > 0 ) {
447                                 if ( $this->isRoleMember( $conn, $targetMember, $row->member, $maxDepth - 1 ) ) {
448                                         // Found member of member
449                                         return true;
450                                 }
451                         }
452                 }
453
454                 return false;
455         }
456
457         public function preInstall() {
458                 $createDbAccount = [
459                         'name' => 'user',
460                         'callback' => [ $this, 'setupUser' ],
461                 ];
462                 $commitCB = [
463                         'name' => 'pg-commit',
464                         'callback' => [ $this, 'commitChanges' ],
465                 ];
466                 $plpgCB = [
467                         'name' => 'pg-plpgsql',
468                         'callback' => [ $this, 'setupPLpgSQL' ],
469                 ];
470                 $schemaCB = [
471                         'name' => 'schema',
472                         'callback' => [ $this, 'setupSchema' ]
473                 ];
474
475                 if ( $this->getVar( '_CreateDBAccount' ) ) {
476                         $this->parent->addInstallStep( $createDbAccount, 'database' );
477                 }
478                 $this->parent->addInstallStep( $commitCB, 'interwiki' );
479                 $this->parent->addInstallStep( $plpgCB, 'database' );
480                 $this->parent->addInstallStep( $schemaCB, 'database' );
481         }
482
483         function setupDatabase() {
484                 $status = $this->getPgConnection( 'create-db' );
485                 if ( !$status->isOK() ) {
486                         return $status;
487                 }
488                 $conn = $status->value;
489
490                 $dbName = $this->getVar( 'wgDBname' );
491
492                 $exists = $conn->selectField( '"pg_catalog"."pg_database"', '1',
493                         [ 'datname' => $dbName ], __METHOD__ );
494                 if ( !$exists ) {
495                         $safedb = $conn->addIdentifierQuotes( $dbName );
496                         $conn->query( "CREATE DATABASE $safedb", __METHOD__ );
497                 }
498
499                 return Status::newGood();
500         }
501
502         function setupSchema() {
503                 // Get a connection to the target database
504                 $status = $this->getPgConnection( 'create-schema' );
505                 if ( !$status->isOK() ) {
506                         return $status;
507                 }
508                 $conn = $status->value;
509
510                 // Create the schema if necessary
511                 $schema = $this->getVar( 'wgDBmwschema' );
512                 $safeschema = $conn->addIdentifierQuotes( $schema );
513                 $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
514                 if ( !$conn->schemaExists( $schema ) ) {
515                         try {
516                                 $conn->query( "CREATE SCHEMA $safeschema AUTHORIZATION $safeuser" );
517                         } catch ( DBQueryError $e ) {
518                                 return Status::newFatal( 'config-install-pg-schema-failed',
519                                         $this->getVar( '_InstallUser' ), $schema );
520                         }
521                 }
522
523                 // Select the new schema in the current connection
524                 $conn->determineCoreSchema( $schema );
525
526                 return Status::newGood();
527         }
528
529         function commitChanges() {
530                 $this->db->commit( __METHOD__ );
531
532                 return Status::newGood();
533         }
534
535         function setupUser() {
536                 if ( !$this->getVar( '_CreateDBAccount' ) ) {
537                         return Status::newGood();
538                 }
539
540                 $status = $this->getPgConnection( 'create-db' );
541                 if ( !$status->isOK() ) {
542                         return $status;
543                 }
544                 $conn = $status->value;
545
546                 $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
547                 $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
548
549                 // Check if the user already exists
550                 $userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) );
551                 if ( !$userExists ) {
552                         // Create the user
553                         try {
554                                 $sql = "CREATE ROLE $safeuser NOCREATEDB LOGIN PASSWORD $safepass";
555
556                                 // If the install user is not a superuser, we need to make the install
557                                 // user a member of the new user's group, so that the install user will
558                                 // be able to create a schema and other objects on behalf of the new user.
559                                 if ( !$this->isSuperUser() ) {
560                                         $sql .= ' ROLE' . $conn->addIdentifierQuotes( $this->getVar( '_InstallUser' ) );
561                                 }
562
563                                 $conn->query( $sql, __METHOD__ );
564                         } catch ( DBQueryError $e ) {
565                                 return Status::newFatal( 'config-install-user-create-failed',
566                                         $this->getVar( 'wgDBuser' ), $e->getMessage() );
567                         }
568                 }
569
570                 return Status::newGood();
571         }
572
573         function getLocalSettings() {
574                 $port = $this->getVar( 'wgDBport' );
575                 $schema = $this->getVar( 'wgDBmwschema' );
576
577                 return "# Postgres specific settings
578 \$wgDBport = \"{$port}\";
579 \$wgDBmwschema = \"{$schema}\";";
580         }
581
582         public function preUpgrade() {
583                 global $wgDBuser, $wgDBpassword;
584
585                 # Normal user and password are selected after this step, so for now
586                 # just copy these two
587                 $wgDBuser = $this->getVar( '_InstallUser' );
588                 $wgDBpassword = $this->getVar( '_InstallPassword' );
589         }
590
591         public function createTables() {
592                 $schema = $this->getVar( 'wgDBmwschema' );
593
594                 $status = $this->getConnection();
595                 if ( !$status->isOK() ) {
596                         return $status;
597                 }
598
599                 /** @var DatabasePostgres $conn */
600                 $conn = $status->value;
601
602                 if ( $conn->tableExists( 'archive' ) ) {
603                         $status->warning( 'config-install-tables-exist' );
604                         $this->enableLB();
605
606                         return $status;
607                 }
608
609                 $conn->begin( __METHOD__ );
610
611                 if ( !$conn->schemaExists( $schema ) ) {
612                         $status->fatal( 'config-install-pg-schema-not-exist' );
613
614                         return $status;
615                 }
616                 $error = $conn->sourceFile( $this->getSchemaPath( $conn ) );
617                 if ( $error !== true ) {
618                         $conn->reportQueryError( $error, 0, '', __METHOD__ );
619                         $conn->rollback( __METHOD__ );
620                         $status->fatal( 'config-install-tables-failed', $error );
621                 } else {
622                         $conn->commit( __METHOD__ );
623                 }
624                 // Resume normal operations
625                 if ( $status->isOK() ) {
626                         $this->enableLB();
627                 }
628
629                 return $status;
630         }
631
632         public function getGlobalDefaults() {
633                 // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
634                 // the use of a schema, so we need to set it here
635                 return array_merge( parent::getGlobalDefaults(), [
636                         'wgDBmwschema' => 'mediawiki',
637                 ] );
638         }
639
640         public function setupPLpgSQL() {
641                 // Connect as the install user, since it owns the database and so is
642                 // the user that needs to run "CREATE LANGAUGE"
643                 $status = $this->getPgConnection( 'create-schema' );
644                 if ( !$status->isOK() ) {
645                         return $status;
646                 }
647                 /**
648                  * @var $conn Database
649                  */
650                 $conn = $status->value;
651
652                 $exists = $conn->selectField( '"pg_catalog"."pg_language"', 1,
653                         [ 'lanname' => 'plpgsql' ], __METHOD__ );
654                 if ( $exists ) {
655                         // Already exists, nothing to do
656                         return Status::newGood();
657                 }
658
659                 // plpgsql is not installed, but if we have a pg_pltemplate table, we
660                 // should be able to create it
661                 $exists = $conn->selectField(
662                         [ '"pg_catalog"."pg_class"', '"pg_catalog"."pg_namespace"' ],
663                         1,
664                         [
665                                 'pg_namespace.oid=relnamespace',
666                                 'nspname' => 'pg_catalog',
667                                 'relname' => 'pg_pltemplate',
668                         ],
669                         __METHOD__ );
670                 if ( $exists ) {
671                         try {
672                                 $conn->query( 'CREATE LANGUAGE plpgsql' );
673                         } catch ( DBQueryError $e ) {
674                                 return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
675                         }
676                 } else {
677                         return Status::newFatal( 'config-pg-no-plpgsql', $this->getVar( 'wgDBname' ) );
678                 }
679
680                 return Status::newGood();
681         }
682 }