Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
MWLBConfig
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 8
1406
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 applyConfig
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
156
 getConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initServerInfo
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
132
 assertValidServerConfigs
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
90
 reportIfPrefixSet
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 reportMismatchedDBs
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 reportMismatchedPrefixes
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
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
10namespace MediaWiki\DB;
11
12use MediaWiki\Config\ServiceOptions;
13use MediaWiki\Deferred\LinksUpdate\LinksTable;
14use MediaWiki\Exception\MWExceptionRenderer;
15use MediaWiki\MainConfigNames;
16use MediaWiki\Rest\EntryPoint;
17use UnexpectedValueException;
18use Wikimedia\Rdbms\DatabaseDomain;
19use Wikimedia\Rdbms\LBFactoryMulti;
20use 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 */
28class 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}