Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 130 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
MysqlInstaller | |
0.00% |
0 / 130 |
|
0.00% |
0 / 15 |
2070 | |
0.00% |
0 / 1 |
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isCompiled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConnectForm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSettingsForm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
meetsMinimumRequirement | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
openConnection | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
preUpgrade | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
132 | |||
escapeLikeInternal | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getEngines | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getCharsets | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canCreateAccounts | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
156 | |||
likeToRegex | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getTableOptions | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getSchemaVars | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getLocalSettings | |
0.00% |
0 / 7 |
|
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 | |
25 | namespace MediaWiki\Installer; |
26 | |
27 | use Wikimedia\Rdbms\DatabaseFactory; |
28 | use Wikimedia\Rdbms\DatabaseMySQL; |
29 | use Wikimedia\Rdbms\DBConnectionError; |
30 | use Wikimedia\Rdbms\IDatabase; |
31 | |
32 | /** |
33 | * Class for setting up the MediaWiki database using MySQL. |
34 | * |
35 | * @ingroup Installer |
36 | * @since 1.17 |
37 | */ |
38 | class 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 | } |