Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
MysqlInstaller
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 15
2070
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
 meetsMinimumRequirement
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 openConnection
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 preUpgrade
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
132
 escapeLikeInternal
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getEngines
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getCharsets
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canCreateAccounts
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
156
 likeToRegex
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getTableOptions
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getSchemaVars
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getLocalSettings
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * MySQL-specific installer.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @ingroup Installer
23 */
24
25namespace MediaWiki\Installer;
26
27use Wikimedia\Rdbms\DatabaseFactory;
28use Wikimedia\Rdbms\DatabaseMySQL;
29use Wikimedia\Rdbms\DBConnectionError;
30use Wikimedia\Rdbms\IDatabase;
31
32/**
33 * Class for setting up the MediaWiki database using MySQL.
34 *
35 * @ingroup Installer
36 * @since 1.17
37 */
38class MysqlInstaller extends DatabaseInstaller {
39
40    /** @inheritDoc */
41    protected $globalNames = [
42        'wgDBserver',
43        'wgDBname',
44        'wgDBuser',
45        'wgDBpassword',
46        'wgDBssl',
47        'wgDBprefix',
48        'wgDBTableOptions',
49    ];
50
51    /** @inheritDoc */
52    protected $internalDefaults = [
53        '_MysqlEngine' => 'InnoDB',
54        '_MysqlCharset' => 'binary',
55        '_InstallUser' => 'root',
56    ];
57
58    /** @var string[] */
59    public $supportedEngines = [ 'InnoDB' ];
60
61    private const MIN_VERSIONS = [
62        'MySQL' => '5.7.0',
63        'MariaDB' => '10.3',
64    ];
65    /** @inheritDoc */
66    public static $minimumVersion;
67    /** @inheritDoc */
68    protected static $notMinimumVersionMessage;
69
70    /** @var string[] */
71    public $webUserPrivs = [
72        'DELETE',
73        'INSERT',
74        'SELECT',
75        'UPDATE',
76        'CREATE TEMPORARY TABLES',
77    ];
78
79    /**
80     * @return string
81     */
82    public function getName() {
83        return 'mysql';
84    }
85
86    /**
87     * @return bool
88     */
89    public function isCompiled() {
90        return self::checkExtension( 'mysqli' );
91    }
92
93    public function getConnectForm( WebInstaller $webInstaller ): DatabaseConnectForm {
94        return new MysqlConnectForm( $webInstaller, $this );
95    }
96
97    public function getSettingsForm( WebInstaller $webInstaller ): DatabaseSettingsForm {
98        return new MysqlSettingsForm( $webInstaller, $this );
99    }
100
101    public static function meetsMinimumRequirement( IDatabase $conn ) {
102        $type = str_contains( $conn->getSoftwareLink(), 'MariaDB' ) ? 'MariaDB' : 'MySQL';
103        self::$minimumVersion = self::MIN_VERSIONS[$type];
104        // Used messages: config-mysql-old, config-mariadb-old
105        self::$notMinimumVersionMessage = 'config-' . strtolower( $type ) . '-old';
106        return parent::meetsMinimumRequirement( $conn );
107    }
108
109    /**
110     * @param string $type
111     * @return ConnectionStatus
112     */
113    protected function openConnection( string $type ) {
114        $status = new ConnectionStatus;
115        $dbName = $type === DatabaseInstaller::CONN_CREATE_DATABASE
116            ? null : $this->getVar( 'wgDBname' );
117        try {
118            /** @var DatabaseMySQL $db */
119            $db = ( new DatabaseFactory() )->create( 'mysql', [
120                'host' => $this->getVar( 'wgDBserver' ),
121                'user' => $this->getVar( '_InstallUser' ),
122                'password' => $this->getVar( '_InstallPassword' ),
123                'ssl' => $this->getVar( 'wgDBssl' ),
124                'dbname' => $dbName,
125                'flags' => 0,
126                'tablePrefix' => $this->getVar( 'wgDBprefix' ) ] );
127            $status->setDB( $db );
128        } catch ( DBConnectionError $e ) {
129            $status->fatal( 'config-connection-error', $e->getMessage() );
130        }
131
132        return $status;
133    }
134
135    public function preUpgrade() {
136        global $wgDBuser, $wgDBpassword;
137
138        $status = $this->getConnection( self::CONN_CREATE_TABLES );
139        if ( !$status->isOK() ) {
140            $this->parent->showStatusMessage( $status );
141
142            return;
143        }
144        $conn = $status->getDB();
145        # Determine existing default character set
146        if ( $conn->tableExists( "revision", __METHOD__ ) ) {
147            $revision = $this->escapeLikeInternal( $this->getVar( 'wgDBprefix' ) . 'revision', '\\' );
148            $res = $conn->query( "SHOW TABLE STATUS LIKE '$revision'", __METHOD__ );
149            $row = $res->fetchObject();
150            if ( !$row ) {
151                $this->parent->showMessage( 'config-show-table-status' );
152                $existingSchema = false;
153                $existingEngine = false;
154            } else {
155                if ( preg_match( '/^latin1/', $row->Collation ) ) {
156                    $existingSchema = 'latin1';
157                } elseif ( preg_match( '/^utf8/', $row->Collation ) ) {
158                    $existingSchema = 'utf8';
159                } elseif ( preg_match( '/^binary/', $row->Collation ) ) {
160                    $existingSchema = 'binary';
161                } else {
162                    $existingSchema = false;
163                    $this->parent->showMessage( 'config-unknown-collation' );
164                }
165                $existingEngine = $row->Engine ?? $row->Type;
166            }
167        } else {
168            $existingSchema = false;
169            $existingEngine = false;
170        }
171
172        if ( $existingSchema && $existingSchema != $this->getVar( '_MysqlCharset' ) ) {
173            $this->setVar( '_MysqlCharset', $existingSchema );
174        }
175        if ( $existingEngine && $existingEngine != $this->getVar( '_MysqlEngine' ) ) {
176            $this->setVar( '_MysqlEngine', $existingEngine );
177        }
178
179        # Normal user and password are selected after this step, so for now
180        # just copy these two
181        $wgDBuser = $this->getVar( '_InstallUser' );
182        $wgDBpassword = $this->getVar( '_InstallPassword' );
183    }
184
185    /**
186     * @param string $s
187     * @param string $escapeChar
188     * @return string
189     */
190    protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
191        return str_replace( [ $escapeChar, '%', '_' ],
192            [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
193            $s );
194    }
195
196    /**
197     * Get a list of storage engines that are available and supported
198     *
199     * @return array
200     */
201    public function getEngines() {
202        $status = $this->getConnection( self::CONN_CREATE_DATABASE );
203        $conn = $status->getDB();
204
205        $engines = [];
206        $res = $conn->query( 'SHOW ENGINES', __METHOD__ );
207        foreach ( $res as $row ) {
208            if ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) {
209                $engines[] = $row->Engine;
210            }
211        }
212        $engines = array_intersect( $this->supportedEngines, $engines );
213
214        return $engines;
215    }
216
217    /**
218     * Get a list of character sets that are available and supported
219     *
220     * @return array
221     */
222    public function getCharsets() {
223        return [ 'binary', 'utf8' ];
224    }
225
226    /**
227     * Return true if the install user can create accounts
228     *
229     * @return bool
230     */
231    public function canCreateAccounts() {
232        $status = $this->getConnection( self::CONN_CREATE_DATABASE );
233        if ( !$status->isOK() ) {
234            return false;
235        }
236        $conn = $status->getDB();
237
238        // Get current account name
239        $currentName = $conn->selectField( '', 'CURRENT_USER()', '', __METHOD__ );
240        $parts = explode( '@', $currentName );
241        if ( count( $parts ) != 2 ) {
242            return false;
243        }
244        $quotedUser = $conn->addQuotes( $parts[0] ) .
245            '@' . $conn->addQuotes( $parts[1] );
246
247        // The user needs to have INSERT on mysql.* to be able to CREATE USER
248        // The grantee will be double-quoted in this query, as required
249        $res = $conn->select( 'INFORMATION_SCHEMA.USER_PRIVILEGES', '*',
250            [ 'GRANTEE' => $quotedUser ], __METHOD__ );
251        $insertMysql = false;
252        $grantOptions = array_fill_keys( $this->webUserPrivs, true );
253        foreach ( $res as $row ) {
254            if ( $row->PRIVILEGE_TYPE == 'INSERT' ) {
255                $insertMysql = true;
256            }
257            if ( $row->IS_GRANTABLE ) {
258                unset( $grantOptions[$row->PRIVILEGE_TYPE] );
259            }
260        }
261
262        // Check for DB-specific privs for mysql.*
263        if ( !$insertMysql ) {
264            $row = $conn->selectRow( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
265                [
266                    'GRANTEE' => $quotedUser,
267                    'TABLE_SCHEMA' => 'mysql',
268                    'PRIVILEGE_TYPE' => 'INSERT',
269                ], __METHOD__ );
270            if ( $row ) {
271                $insertMysql = true;
272            }
273        }
274
275        if ( !$insertMysql ) {
276            return false;
277        }
278
279        // Check for DB-level grant options
280        $res = $conn->select( 'INFORMATION_SCHEMA.SCHEMA_PRIVILEGES', '*',
281            [
282                'GRANTEE' => $quotedUser,
283                'IS_GRANTABLE' => 1,
284            ], __METHOD__ );
285        foreach ( $res as $row ) {
286            $regex = $this->likeToRegex( $row->TABLE_SCHEMA );
287            if ( preg_match( $regex, $this->getVar( 'wgDBname' ) ) ) {
288                unset( $grantOptions[$row->PRIVILEGE_TYPE] );
289            }
290        }
291        if ( count( $grantOptions ) ) {
292            // Can't grant everything
293            return false;
294        }
295
296        return true;
297    }
298
299    /**
300     * Convert a wildcard (as used in LIKE) to a regex
301     * Slashes are escaped, slash terminators included
302     * @param string $wildcard
303     * @return string
304     */
305    protected function likeToRegex( $wildcard ) {
306        $r = preg_quote( $wildcard, '/' );
307        $r = strtr( $r, [
308            '%' => '.*',
309            '_' => '.'
310        ] );
311        return "/$r/s";
312    }
313
314    /**
315     * Return any table options to be applied to all tables that don't
316     * override them.
317     *
318     * @return string
319     */
320    protected function getTableOptions() {
321        $options = [];
322        if ( $this->getVar( '_MysqlEngine' ) !== null ) {
323            $options[] = "ENGINE=" . $this->getVar( '_MysqlEngine' );
324        }
325        if ( $this->getVar( '_MysqlCharset' ) !== null ) {
326            $options[] = 'DEFAULT CHARSET=' . $this->getVar( '_MysqlCharset' );
327        }
328
329        return implode( ', ', $options );
330    }
331
332    /**
333     * Get variables to substitute into tables.sql and the SQL patch files.
334     *
335     * @return array
336     */
337    public function getSchemaVars() {
338        return [
339            'wgDBTableOptions' => $this->getTableOptions(),
340        ];
341    }
342
343    public function getLocalSettings() {
344        $prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
345        $useSsl = $this->getVar( 'wgDBssl' ) ? 'true' : 'false';
346        $tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() );
347
348        return "# MySQL specific settings
349\$wgDBprefix = \"{$prefix}\";
350\$wgDBssl = {$useSsl};
351
352# MySQL table options to use during installation or update
353\$wgDBTableOptions = \"{$tblOpts}\";";
354    }
355}