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