Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
PostgresInstaller
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 11
650
0.00% covered (danger)
0.00%
0 / 1
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isCompiled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConnectForm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSettingsForm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 openConnectionWithParams
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 openConnection
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 changeConnTypeFromSchemaToTables
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 openConnectionToAnyDB
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 getLocalSettings
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 preUpgrade
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getGlobalDefaults
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * PostgreSQL-specific installer.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Installer
8 */
9
10namespace MediaWiki\Installer;
11
12use InvalidArgumentException;
13use MediaWiki\MediaWikiServices;
14use Wikimedia\Rdbms\DatabaseFactory;
15use Wikimedia\Rdbms\DatabasePostgres;
16use Wikimedia\Rdbms\DBConnectionError;
17use Wikimedia\Rdbms\IMaintainableDatabase;
18
19/**
20 * Class for setting up the MediaWiki database using Postgres.
21 *
22 * @ingroup Installer
23 * @since 1.17
24 */
25class PostgresInstaller extends DatabaseInstaller {
26
27    /** @inheritDoc */
28    protected $globalNames = [
29        'wgDBserver',
30        'wgDBport',
31        'wgDBname',
32        'wgDBuser',
33        'wgDBpassword',
34        'wgDBssl',
35        'wgDBmwschema',
36    ];
37
38    /** @inheritDoc */
39    protected $internalDefaults = [
40        '_InstallUser' => 'postgres',
41    ];
42
43    /** @inheritDoc */
44    public static $minimumVersion = '10';
45    /** @inheritDoc */
46    protected static $notMinimumVersionMessage = 'config-postgres-old';
47
48    /** @inheritDoc */
49    public function getName() {
50        return 'postgres';
51    }
52
53    /** @inheritDoc */
54    public function isCompiled() {
55        return self::checkExtension( 'pgsql' );
56    }
57
58    public function getConnectForm( WebInstaller $webInstaller ): DatabaseConnectForm {
59        return new PostgresConnectForm( $webInstaller, $this );
60    }
61
62    public function getSettingsForm( WebInstaller $webInstaller ): DatabaseSettingsForm {
63        return new PostgresSettingsForm( $webInstaller, $this );
64    }
65
66    /**
67     * Open a PG connection with given parameters
68     * @param string $user User name
69     * @param string $password
70     * @param string $dbName Database name
71     * @param string $schema Database schema
72     * @return ConnectionStatus
73     */
74    protected function openConnectionWithParams( $user, $password, $dbName, $schema ) {
75        $status = new ConnectionStatus;
76        try {
77            $db = MediaWikiServices::getInstance()->getDatabaseFactory()->create( 'postgres', [
78                'host' => $this->getVar( 'wgDBserver' ),
79                'port' => $this->getVar( 'wgDBport' ),
80                'user' => $user,
81                'password' => $password,
82                'ssl' => $this->getVar( 'wgDBssl' ),
83                'dbname' => $dbName,
84                'schema' => $schema,
85            ] );
86            $status->setDB( $db );
87        } catch ( DBConnectionError $e ) {
88            $status->fatal( 'config-connection-error', $e->getMessage() );
89        }
90
91        return $status;
92    }
93
94    /**
95     * Get a connection of a specific PostgreSQL-specific type. Connections
96     * of a given type are cached.
97     *
98     * PostgreSQL lacks cross-database operations, so after the new database is
99     * created, you need to make a separate connection to connect to that
100     * database and add tables to it.
101     *
102     * New tables are owned by the user that creates them, and MediaWiki's
103     * PostgreSQL support has always assumed that the table owner will be
104     * $wgDBuser. So before we create new tables, we either need to either
105     * connect as the other user or to execute a SET ROLE command. Using a
106     * separate connection for this allows us to avoid accidental cross-module
107     * dependencies.
108     *
109     * @param string $type The type of connection to get:
110     *   - self::CONN_CREATE_DATABASE: A connection for creating DBs, suitable for pre-
111     *                                 installation.
112     *   - self::CONN_CREATE_SCHEMA:   A connection to the new DB, for creating schemas and
113     *                                 other similar objects in the new DB.
114     *   - self::CONN_CREATE_TABLES:   A connection with a role suitable for creating tables.
115     * @return ConnectionStatus On success, a connection object will be in the value member.
116     */
117    protected function openConnection( string $type ) {
118        switch ( $type ) {
119            case self::CONN_CREATE_DATABASE:
120                return $this->openConnectionToAnyDB(
121                    $this->getVar( '_InstallUser' ),
122                    $this->getVar( '_InstallPassword' ) );
123            case self::CONN_CREATE_SCHEMA:
124                return $this->openConnectionWithParams(
125                    $this->getVar( '_InstallUser' ),
126                    $this->getVar( '_InstallPassword' ),
127                    $this->getVar( 'wgDBname' ),
128                    $this->getVar( 'wgDBmwschema' ) );
129            case self::CONN_CREATE_TABLES:
130                $status = $this->openConnection( self::CONN_CREATE_SCHEMA );
131                if ( $status->isOK() ) {
132                    $status->merge( $this->changeConnTypeFromSchemaToTables( $status->getDB() ) );
133                }
134
135                return $status;
136            default:
137                throw new InvalidArgumentException( "Invalid connection type: \"$type\"" );
138        }
139    }
140
141    /** @inheritDoc */
142    protected function changeConnTypeFromSchemaToTables( IMaintainableDatabase $conn ) {
143        if ( !( $conn instanceof DatabasePostgres ) ) {
144            throw new InvalidArgumentException( 'Invalid connection type' );
145        }
146        $status = new ConnectionStatus( $conn );
147        $schema = $this->getVar( 'wgDBmwschema' );
148        if ( !$conn->schemaExists( $schema ) ) {
149            $status->fatal( 'config-install-pg-schema-not-exist' );
150            return $status;
151        }
152        $conn->determineCoreSchema( $schema );
153
154        $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
155        $conn->query( "SET ROLE $safeRole", __METHOD__ );
156        return $status;
157    }
158
159    public function openConnectionToAnyDB( string $user, ?string $password ): ConnectionStatus {
160        $dbs = [
161            'template1',
162            'postgres',
163        ];
164        if ( !in_array( $this->getVar( 'wgDBname' ), $dbs ) ) {
165            array_unshift( $dbs, $this->getVar( 'wgDBname' ) );
166        }
167        $conn = false;
168        $status = new ConnectionStatus;
169        foreach ( $dbs as $db ) {
170            try {
171                $p = [
172                    'host' => $this->getVar( 'wgDBserver' ),
173                    'port' => $this->getVar( 'wgDBport' ),
174                    'user' => $user,
175                    'password' => $password ?? '',
176                    'ssl' => $this->getVar( 'wgDBssl' ),
177                    'dbname' => $db
178                ];
179                $conn = ( new DatabaseFactory() )->create( 'postgres', $p );
180            } catch ( DBConnectionError $error ) {
181                $conn = false;
182                $status->fatal( 'config-pg-test-error', $db,
183                    $error->getMessage() );
184            }
185            if ( $conn !== false ) {
186                break;
187            }
188        }
189        if ( $conn !== false ) {
190            return new ConnectionStatus( $conn );
191        } else {
192            return $status;
193        }
194    }
195
196    /** @inheritDoc */
197    public function getLocalSettings() {
198        $port = $this->getVar( 'wgDBport' );
199        $useSsl = $this->getVar( 'wgDBssl' ) ? 'true' : 'false';
200        $schema = $this->getVar( 'wgDBmwschema' );
201
202        return "# Postgres specific settings
203\$wgDBport = \"{$port}\";
204\$wgDBssl = {$useSsl};
205\$wgDBmwschema = \"{$schema}\";";
206    }
207
208    public function preUpgrade() {
209        global $wgDBuser, $wgDBpassword;
210
211        # Normal user and password are selected after this step, so for now
212        # just copy these two
213        $wgDBuser = $this->getVar( '_InstallUser' );
214        $wgDBpassword = $this->getVar( '_InstallPassword' );
215    }
216
217    /** @inheritDoc */
218    public function getGlobalDefaults() {
219        // The default $wgDBmwschema is null, which breaks Postgres and other DBMSes that require
220        // the use of a schema, so we need to set it here
221        return array_merge( parent::getGlobalDefaults(), [
222            'wgDBmwschema' => 'mediawiki',
223        ] );
224    }
225}