Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 117 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
| MWLBConfig | |
0.00% |
0 / 117 |
|
0.00% |
0 / 8 |
1406 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| applyConfig | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
156 | |||
| getConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| initServerInfo | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
132 | |||
| assertValidServerConfigs | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
90 | |||
| reportIfPrefixSet | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| reportMismatchedDBs | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| reportMismatchedPrefixes | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Generator of database load balancing objects. |
| 4 | * |
| 5 | * @license GPL-2.0-or-later |
| 6 | * @file |
| 7 | * @ingroup Database |
| 8 | */ |
| 9 | |
| 10 | namespace MediaWiki\DB; |
| 11 | |
| 12 | use MediaWiki\Config\ServiceOptions; |
| 13 | use MediaWiki\Deferred\LinksUpdate\LinksTable; |
| 14 | use MediaWiki\Exception\MWExceptionRenderer; |
| 15 | use MediaWiki\MainConfigNames; |
| 16 | use MediaWiki\Rest\EntryPoint; |
| 17 | use UnexpectedValueException; |
| 18 | use Wikimedia\Rdbms\DatabaseDomain; |
| 19 | use Wikimedia\Rdbms\LBFactoryMulti; |
| 20 | use Wikimedia\Rdbms\LBFactorySimple; |
| 21 | |
| 22 | /** |
| 23 | * MediaWiki-specific class for generating configuration of load balancers |
| 24 | * |
| 25 | * @internal For use by core ServiceWiring only. |
| 26 | * @ingroup Database |
| 27 | */ |
| 28 | class MWLBConfig { |
| 29 | private const array DB_TYPES_WITH_SCHEMAS = [ 'postgres' ]; |
| 30 | |
| 31 | public const CORE_VIRTUAL_DOMAINS = [ |
| 32 | 'virtual-botpasswords', |
| 33 | 'virtual-interwiki', |
| 34 | 'virtual-interwiki-interlanguage', |
| 35 | LinksTable::VIRTUAL_DOMAIN, |
| 36 | ]; |
| 37 | |
| 38 | /** |
| 39 | * @internal For use by ServiceWiring |
| 40 | */ |
| 41 | public const APPLY_DEFAULT_CONFIG_OPTIONS = [ |
| 42 | MainConfigNames::DBcompress, |
| 43 | MainConfigNames::DBmwschema, |
| 44 | MainConfigNames::DBname, |
| 45 | MainConfigNames::DBpassword, |
| 46 | MainConfigNames::DBport, |
| 47 | MainConfigNames::DBprefix, |
| 48 | MainConfigNames::DBserver, |
| 49 | MainConfigNames::DBservers, |
| 50 | MainConfigNames::DBssl, |
| 51 | MainConfigNames::DBStrictWarnings, |
| 52 | MainConfigNames::DBtype, |
| 53 | MainConfigNames::DBuser, |
| 54 | MainConfigNames::DebugDumpSql, |
| 55 | MainConfigNames::DebugLogFile, |
| 56 | MainConfigNames::DebugToolbar, |
| 57 | MainConfigNames::ExternalServers, |
| 58 | MainConfigNames::SQLiteDataDir, |
| 59 | MainConfigNames::SQLMode, |
| 60 | MainConfigNames::VirtualDomainsMapping, |
| 61 | ]; |
| 62 | private ServiceOptions $options; |
| 63 | |
| 64 | /** @var string[] */ |
| 65 | private array $virtualDomains; |
| 66 | |
| 67 | private array $lbConf; |
| 68 | |
| 69 | /** |
| 70 | * @param ServiceOptions $options |
| 71 | * @param string[] $virtualDomains |
| 72 | * @param array $lbConf |
| 73 | */ |
| 74 | public function __construct( |
| 75 | ServiceOptions $options, |
| 76 | array $virtualDomains, |
| 77 | array $lbConf |
| 78 | ) { |
| 79 | $this->options = $options; |
| 80 | $this->virtualDomains = $virtualDomains; |
| 81 | $this->lbConf = $this->applyConfig( $lbConf ); |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * @param array $lbConf Config for LBFactory::__construct() |
| 86 | * @return array |
| 87 | * @internal For use with service wiring |
| 88 | */ |
| 89 | private function applyConfig( array $lbConf ): array { |
| 90 | $this->options->assertRequiredOptions( self::APPLY_DEFAULT_CONFIG_OPTIONS ); |
| 91 | |
| 92 | $lbConf += [ |
| 93 | 'localDomain' => new DatabaseDomain( |
| 94 | $this->options->get( MainConfigNames::DBname ), |
| 95 | $this->options->get( MainConfigNames::DBmwschema ), |
| 96 | $this->options->get( MainConfigNames::DBprefix ) |
| 97 | ), |
| 98 | ]; |
| 99 | |
| 100 | $serversCheck = []; |
| 101 | // When making changes here, remember to also specify MediaWiki-specific options |
| 102 | // for Database classes in the relevant Installer subclass. |
| 103 | // Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams. |
| 104 | if ( $lbConf['class'] === LBFactorySimple::class ) { |
| 105 | if ( isset( $lbConf['servers'] ) ) { |
| 106 | // Server array is already explicitly configured |
| 107 | } elseif ( is_array( $this->options->get( MainConfigNames::DBservers ) ) ) { |
| 108 | $lbConf['servers'] = []; |
| 109 | foreach ( $this->options->get( MainConfigNames::DBservers ) as $i => $server ) { |
| 110 | $lbConf['servers'][$i] = $this->initServerInfo( $server, $this->options ); |
| 111 | } |
| 112 | } else { |
| 113 | $server = $this->initServerInfo( |
| 114 | [ |
| 115 | 'host' => $this->options->get( MainConfigNames::DBserver ), |
| 116 | 'user' => $this->options->get( MainConfigNames::DBuser ), |
| 117 | 'password' => $this->options->get( MainConfigNames::DBpassword ), |
| 118 | 'dbname' => $this->options->get( MainConfigNames::DBname ), |
| 119 | 'type' => $this->options->get( MainConfigNames::DBtype ), |
| 120 | 'load' => 1 |
| 121 | ], |
| 122 | $this->options |
| 123 | ); |
| 124 | |
| 125 | if ( $this->options->get( MainConfigNames::DBssl ) ) { |
| 126 | $server['ssl'] = true; |
| 127 | } |
| 128 | $server['flags'] |= $this->options->get( MainConfigNames::DBcompress ) ? DBO_COMPRESS : 0; |
| 129 | if ( $this->options->get( MainConfigNames::DBStrictWarnings ) ) { |
| 130 | $server['strictWarnings'] = true; |
| 131 | } |
| 132 | |
| 133 | $lbConf['servers'] = [ $server ]; |
| 134 | } |
| 135 | if ( !isset( $lbConf['externalClusters'] ) ) { |
| 136 | $lbConf['externalClusters'] = $this->options->get( MainConfigNames::ExternalServers ); |
| 137 | } |
| 138 | |
| 139 | $serversCheck = $lbConf['servers']; |
| 140 | } elseif ( $lbConf['class'] === LBFactoryMulti::class ) { |
| 141 | if ( isset( $lbConf['serverTemplate'] ) ) { |
| 142 | if ( in_array( $lbConf['serverTemplate']['type'], self::DB_TYPES_WITH_SCHEMAS, true ) ) { |
| 143 | $lbConf['serverTemplate']['schema'] = $this->options->get( MainConfigNames::DBmwschema ); |
| 144 | } |
| 145 | $lbConf['serverTemplate']['sqlMode'] = $this->options->get( MainConfigNames::SQLMode ); |
| 146 | $serversCheck = [ $lbConf['serverTemplate'] ]; |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | $this->assertValidServerConfigs( |
| 151 | $serversCheck, |
| 152 | $this->options->get( MainConfigNames::DBname ), |
| 153 | $this->options->get( MainConfigNames::DBprefix ) |
| 154 | ); |
| 155 | |
| 156 | $lbConf['virtualDomains'] = array_merge( $this->virtualDomains, self::CORE_VIRTUAL_DOMAINS ); |
| 157 | $lbConf['virtualDomainsMapping'] = $this->options->get( MainConfigNames::VirtualDomainsMapping ); |
| 158 | |
| 159 | return $lbConf; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * @return array the LBFactory config |
| 164 | */ |
| 165 | public function getConfig() { |
| 166 | return $this->lbConf; |
| 167 | } |
| 168 | |
| 169 | /** |
| 170 | * @param array $server |
| 171 | * @param ServiceOptions $options |
| 172 | * @return array |
| 173 | */ |
| 174 | private function initServerInfo( array $server, ServiceOptions $options ): array { |
| 175 | if ( $server['type'] === 'sqlite' ) { |
| 176 | $httpMethod = $_SERVER['REQUEST_METHOD'] ?? null; |
| 177 | // T93097: hint for how file-based databases (e.g. sqlite) should go about locking. |
| 178 | // See https://www.sqlite.org/lang_transaction.html |
| 179 | // See https://www.sqlite.org/lockingv3.html#shared_lock |
| 180 | $isHttpRead = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] ); |
| 181 | if ( MW_ENTRY_POINT === 'rest' && !$isHttpRead ) { |
| 182 | // Hack to support some re-entrant invocations using sqlite |
| 183 | // See: T259685, T91820 |
| 184 | $request = EntryPoint::getMainRequest(); |
| 185 | if ( $request->hasHeader( 'Promise-Non-Write-API-Action' ) ) { |
| 186 | $isHttpRead = true; |
| 187 | } |
| 188 | } |
| 189 | $server += [ |
| 190 | 'dbDirectory' => $options->get( MainConfigNames::SQLiteDataDir ), |
| 191 | 'trxMode' => $isHttpRead ? 'DEFERRED' : 'IMMEDIATE' |
| 192 | ]; |
| 193 | } elseif ( $server['type'] === 'postgres' ) { |
| 194 | $server += [ 'port' => $options->get( MainConfigNames::DBport ) ]; |
| 195 | } |
| 196 | |
| 197 | if ( in_array( $server['type'], self::DB_TYPES_WITH_SCHEMAS, true ) ) { |
| 198 | $server += [ 'schema' => $options->get( MainConfigNames::DBmwschema ) ]; |
| 199 | } |
| 200 | |
| 201 | $flags = $server['flags'] ?? DBO_DEFAULT; |
| 202 | if ( $options->get( MainConfigNames::DebugDumpSql ) |
| 203 | || $options->get( MainConfigNames::DebugLogFile ) |
| 204 | || $options->get( MainConfigNames::DebugToolbar ) |
| 205 | ) { |
| 206 | $flags |= DBO_DEBUG; |
| 207 | } |
| 208 | $server['flags'] = $flags; |
| 209 | |
| 210 | $server += [ |
| 211 | 'tablePrefix' => $options->get( MainConfigNames::DBprefix ), |
| 212 | 'sqlMode' => $options->get( MainConfigNames::SQLMode ), |
| 213 | ]; |
| 214 | |
| 215 | return $server; |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * @param array $servers |
| 220 | * @param string $ldDB Local domain database name |
| 221 | * @param string $ldTP Local domain prefix |
| 222 | */ |
| 223 | private function assertValidServerConfigs( array $servers, string $ldDB, string $ldTP ): void { |
| 224 | foreach ( $servers as $server ) { |
| 225 | $type = $server['type'] ?? null; |
| 226 | $srvDB = $server['dbname'] ?? null; // server DB |
| 227 | $srvTP = $server['tablePrefix'] ?? ''; // server table prefix |
| 228 | |
| 229 | if ( $type === 'mysql' ) { |
| 230 | // A DB name is not needed to connect to mysql; 'dbname' is useless. |
| 231 | // This field only defines the DB to use for unspecified DB domains. |
| 232 | if ( $srvDB !== null && $srvDB !== $ldDB ) { |
| 233 | $this->reportMismatchedDBs( $srvDB, $ldDB ); |
| 234 | } |
| 235 | } elseif ( $type === 'postgres' ) { |
| 236 | if ( $srvTP !== '' ) { |
| 237 | $this->reportIfPrefixSet( $srvTP, $type ); |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | if ( $srvTP !== '' && $srvTP !== $ldTP ) { |
| 242 | $this->reportMismatchedPrefixes( $srvTP, $ldTP ); |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | /** |
| 248 | * @param string $prefix Table prefix |
| 249 | * @param string $dbType Database type |
| 250 | * @return never |
| 251 | */ |
| 252 | private function reportIfPrefixSet( string $prefix, string $dbType ): never { |
| 253 | $e = new UnexpectedValueException( |
| 254 | "\$wgDBprefix is set to '$prefix' but the database type is '$dbType'. " . |
| 255 | "MediaWiki does not support using a table prefix with this RDBMS type." |
| 256 | ); |
| 257 | MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW ); |
| 258 | exit; |
| 259 | } |
| 260 | |
| 261 | /** |
| 262 | * @param string $srvDB Server config database |
| 263 | * @param string $ldDB Local DB domain database |
| 264 | * @return never |
| 265 | */ |
| 266 | private function reportMismatchedDBs( string $srvDB, string $ldDB ): never { |
| 267 | $e = new UnexpectedValueException( |
| 268 | "\$wgDBservers has dbname='$srvDB' but \$wgDBname='$ldDB'. " . |
| 269 | "Set \$wgDBname to the database used by this wiki project. " . |
| 270 | "There is rarely a need to set 'dbname' in \$wgDBservers. " . |
| 271 | "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " . |
| 272 | "use of Database::getDomainId(), and other features are not reliable when " . |
| 273 | "\$wgDBservers does not match the local wiki database/prefix." |
| 274 | ); |
| 275 | MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW ); |
| 276 | exit; |
| 277 | } |
| 278 | |
| 279 | /** |
| 280 | * @param string $srvTP Server config table prefix |
| 281 | * @param string $ldTP Local DB domain database |
| 282 | * @return never |
| 283 | */ |
| 284 | private function reportMismatchedPrefixes( string $srvTP, string $ldTP ): never { |
| 285 | $e = new UnexpectedValueException( |
| 286 | "\$wgDBservers has tablePrefix='$srvTP' but \$wgDBprefix='$ldTP'. " . |
| 287 | "Set \$wgDBprefix to the table prefix used by this wiki project. " . |
| 288 | "There is rarely a need to set 'tablePrefix' in \$wgDBservers. " . |
| 289 | "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " . |
| 290 | "use of Database::getDomainId(), and other features are not reliable when " . |
| 291 | "\$wgDBservers does not match the local wiki database/prefix." |
| 292 | ); |
| 293 | MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW ); |
| 294 | exit; |
| 295 | } |
| 296 | } |