1<?php
2/**
3 * Generator of database load balancing objects.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Database
22 */
23
24use MediaWiki\Config\ServiceOptions;
25use MediaWiki\Logger\LoggerFactory;
26use Wikimedia\Rdbms\DatabaseDomain;
27use Wikimedia\Rdbms\ILBFactory;
28
29/**
30 * MediaWiki-specific class for generating database load balancers
31 * @ingroup Database
32 */
33abstract class MWLBFactory {
34
35	/** @var array Cache of already-logged deprecation messages */
36	private static $loggedDeprecations = [];
37
38	/**
39	 * @var array
40	 * @since 1.34
41	 */
42	public const APPLY_DEFAULT_CONFIG_OPTIONS = [
43		'DBcompress',
44		'DBDefaultGroup',
45		'DBmwschema',
46		'DBname',
47		'DBpassword',
48		'DBport',
49		'DBprefix',
50		'DBserver',
51		'DBservers',
52		'DBssl',
53		'DBtype',
54		'DBuser',
55		'DebugDumpSql',
56		'DebugLogFile',
57		'DebugToolbar',
58		'ExternalServers',
59		'SQLiteDataDir',
60		'SQLMode',
61	];
62
63	/**
64	 * @param array $lbConf Config for LBFactory::__construct()
65	 * @param ServiceOptions $options
66	 * @param ConfiguredReadOnlyMode $readOnlyMode
67	 * @param BagOStuff $srvCache
68	 * @param BagOStuff $mainStash
69	 * @param WANObjectCache $wanCache
70	 * @return array
71	 * @internal For use with service wiring
72	 */
73	public static function applyDefaultConfig(
74		array $lbConf,
75		ServiceOptions $options,
76		ConfiguredReadOnlyMode $readOnlyMode,
77		BagOStuff $srvCache,
78		BagOStuff $mainStash,
79		WANObjectCache $wanCache
80	) {
81		$options->assertRequiredOptions( self::APPLY_DEFAULT_CONFIG_OPTIONS );
82
83		global $wgCommandLineMode;
84
85		$typesWithSchema = self::getDbTypesWithSchemas();
86
87		$lbConf += [
88			'localDomain' => new DatabaseDomain(
89				$options->get( 'DBname' ),
90				$options->get( 'DBmwschema' ),
91				$options->get( 'DBprefix' )
92			),
93			'profiler' => function ( $section ) {
94				return Profiler::instance()->scopedProfileIn( $section );
95			},
96			'trxProfiler' => Profiler::instance()->getTransactionProfiler(),
97			'replLogger' => LoggerFactory::getInstance( 'DBReplication' ),
98			'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ),
99			'connLogger' => LoggerFactory::getInstance( 'DBConnection' ),
100			'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ),
101			'errorLogger' => [ MWExceptionHandler::class, 'logException' ],
102			'deprecationLogger' => [ static::class, 'logDeprecation' ],
103			'cliMode' => $wgCommandLineMode,
104			'hostname' => wfHostname(),
105			'readOnlyReason' => $readOnlyMode->getReason(),
106			'defaultGroup' => $options->get( 'DBDefaultGroup' ),
107		];
108
109		$serversCheck = [];
110		// When making changes here, remember to also specify MediaWiki-specific options
111		// for Database classes in the relevant Installer subclass.
112		// Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams.
113		if ( $lbConf['class'] === Wikimedia\Rdbms\LBFactorySimple::class ) {
114			if ( isset( $lbConf['servers'] ) ) {
115				// Server array is already explicitly configured
116			} elseif ( is_array( $options->get( 'DBservers' ) ) ) {
117				$lbConf['servers'] = [];
118				foreach ( $options->get( 'DBservers' ) as $i => $server ) {
119					$lbConf['servers'][$i] = self::initServerInfo( $server, $options );
120				}
121			} else {
122				$server = self::initServerInfo(
123					[
124						'host' => $options->get( 'DBserver' ),
125						'user' => $options->get( 'DBuser' ),
126						'password' => $options->get( 'DBpassword' ),
127						'dbname' => $options->get( 'DBname' ),
128						'type' => $options->get( 'DBtype' ),
129						'load' => 1
130					],
131					$options
132				);
133
134				$server['flags'] |= $options->get( 'DBssl' ) ? DBO_SSL : 0;
135				$server['flags'] |= $options->get( 'DBcompress' ) ? DBO_COMPRESS : 0;
136
137				$lbConf['servers'] = [ $server ];
138			}
139			if ( !isset( $lbConf['externalClusters'] ) ) {
140				$lbConf['externalClusters'] = $options->get( 'ExternalServers' );
141			}
142
143			$serversCheck = $lbConf['servers'];
144		} elseif ( $lbConf['class'] === Wikimedia\Rdbms\LBFactoryMulti::class ) {
145			if ( isset( $lbConf['serverTemplate'] ) ) {
146				if ( in_array( $lbConf['serverTemplate']['type'], $typesWithSchema, true ) ) {
147					$lbConf['serverTemplate']['schema'] = $options->get( 'DBmwschema' );
148				}
149				$lbConf['serverTemplate']['sqlMode'] = $options->get( 'SQLMode' );
150				$serversCheck = [ $lbConf['serverTemplate'] ];
151			}
152		}
153
154		self::assertValidServerConfigs(
155			$serversCheck,
156			$options->get( 'DBname' ),
157			$options->get( 'DBprefix' )
158		);
159
160		$lbConf['srvCache'] = $srvCache;
161		$lbConf['memStash'] = $mainStash;
162		$lbConf['wanCache'] = $wanCache;
163
164		return $lbConf;
165	}
166
167	/**
168	 * @return array
169	 */
170	private static function getDbTypesWithSchemas() {
171		return [ 'postgres' ];
172	}
173
174	/**
175	 * @param array $server
176	 * @param ServiceOptions $options
177	 * @return array
178	 */
179	private static function initServerInfo( array $server, ServiceOptions $options ) {
180		if ( $server['type'] === 'sqlite' ) {
181			$httpMethod = $_SERVER['REQUEST_METHOD'] ?? null;
182			// T93097: hint for how file-based databases (e.g. sqlite) should go about locking.
183			// See https://www.sqlite.org/lang_transaction.html
184			// See https://www.sqlite.org/lockingv3.html#shared_lock
185			$isHttpRead = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
186			if ( MW_ENTRY_POINT === 'rest' && !$isHttpRead ) {
187				// Hack to support some re-entrant invocations using sqlite
188				// See: T259685, T91820
189				$request = \MediaWiki\Rest\EntryPoint::getMainRequest();
190				if ( $request->hasHeader( 'Promise-Non-Write-API-Action' ) ) {
191					$isHttpRead = true;
192				}
193			}
194			$server += [
195				'dbDirectory' => $options->get( 'SQLiteDataDir' ),
196				'trxMode' => $isHttpRead ? 'DEFERRED' : 'IMMEDIATE'
197			];
198		} elseif ( $server['type'] === 'postgres' ) {
199			$server += [
200				'port' => $options->get( 'DBport' ),
201				// Work around the reserved word usage in MediaWiki schema
202				'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ]
203			];
204		}
205
206		if ( in_array( $server['type'], self::getDbTypesWithSchemas(), true ) ) {
207			$server += [ 'schema' => $options->get( 'DBmwschema' ) ];
208		}
209
210		$flags = $server['flags'] ?? DBO_DEFAULT;
211		if ( $options->get( 'DebugDumpSql' )
212			|| $options->get( 'DebugLogFile' )
213			|| $options->get( 'DebugToolbar' )
214		) {
215			$flags |= DBO_DEBUG;
216		}
217		$server['flags'] = $flags;
218
219		$server += [
220			'tablePrefix' => $options->get( 'DBprefix' ),
221			'sqlMode' => $options->get( 'SQLMode' ),
222		];
223
224		return $server;
225	}
226
227	/**
228	 * @param array $servers
229	 * @param string $ldDB Local domain database name
230	 * @param string $ldTP Local domain prefix
231	 */
232	private static function assertValidServerConfigs( array $servers, $ldDB, $ldTP ) {
233		foreach ( $servers as $server ) {
234			$type = $server['type'] ?? null;
235			$srvDB = $server['dbname'] ?? null; // server DB
236			$srvTP = $server['tablePrefix'] ?? ''; // server table prefix
237
238			if ( $type === 'mysql' ) {
239				// A DB name is not needed to connect to mysql; 'dbname' is useless.
240				// This field only defines the DB to use for unspecified DB domains.
241				if ( $srvDB !== null && $srvDB !== $ldDB ) {
242					self::reportMismatchedDBs( $srvDB, $ldDB );
243				}
244			} elseif ( $type === 'postgres' ) {
245				if ( $srvTP !== '' ) {
246					self::reportIfPrefixSet( $srvTP, $type );
247				}
248			}
249
250			if ( $srvTP !== '' && $srvTP !== $ldTP ) {
251				self::reportMismatchedPrefixes( $srvTP, $ldTP );
252			}
253		}
254	}
255
256	/**
257	 * @param string $prefix Table prefix
258	 * @param string $dbType Database type
259	 */
260	private static function reportIfPrefixSet( $prefix, $dbType ) {
261		$e = new UnexpectedValueException(
262			"\$wgDBprefix is set to '$prefix' but the database type is '$dbType'. " .
263			"MediaWiki does not support using a table prefix with this RDBMS type."
264		);
265		MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY );
266		exit;
267	}
268
269	/**
270	 * @param string $srvDB Server config database
271	 * @param string $ldDB Local DB domain database
272	 */
273	private static function reportMismatchedDBs( $srvDB, $ldDB ) {
274		$e = new UnexpectedValueException(
275			"\$wgDBservers has dbname='$srvDB' but \$wgDBname='$ldDB'. " .
276			"Set \$wgDBname to the database used by this wiki project. " .
277			"There is rarely a need to set 'dbname' in \$wgDBservers. " .
278			"Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " .
279			"use of Database::getDomainId(), and other features are not reliable when " .
280			"\$wgDBservers does not match the local wiki database/prefix."
281		);
282		MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY );
283		exit;
284	}
285
286	/**
287	 * @param string $srvTP Server config table prefix
288	 * @param string $ldTP Local DB domain database
289	 */
290	private static function reportMismatchedPrefixes( $srvTP, $ldTP ) {
291		$e = new UnexpectedValueException(
292			"\$wgDBservers has tablePrefix='$srvTP' but \$wgDBprefix='$ldTP'. " .
293			"Set \$wgDBprefix to the table prefix used by this wiki project. " .
294			"There is rarely a need to set 'tablePrefix' in \$wgDBservers. " .
295			"Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " .
296			"use of Database::getDomainId(), and other features are not reliable when " .
297			"\$wgDBservers does not match the local wiki database/prefix."
298		);
299		MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY );
300		exit;
301	}
302
303	/**
304	 * Decide which LBFactory class to use.
305	 *
306	 * @internal For use by ServiceWiring
307	 * @param array $config (e.g. $wgLBFactoryConf)
308	 * @return string Class name
309	 */
310	public static function getLBFactoryClass( array $config ) {
311		$compat = [
312			// For LocalSettings.php compat after removing underscores (since 1.23).
313			'LBFactory_Single' => Wikimedia\Rdbms\LBFactorySingle::class,
314			'LBFactory_Simple' => Wikimedia\Rdbms\LBFactorySimple::class,
315			'LBFactory_Multi' => Wikimedia\Rdbms\LBFactoryMulti::class,
316			// For LocalSettings.php compat after moving classes to namespaces (since 1.29).
317			'LBFactorySingle' => Wikimedia\Rdbms\LBFactorySingle::class,
318			'LBFactorySimple' => Wikimedia\Rdbms\LBFactorySimple::class,
319			'LBFactoryMulti' => Wikimedia\Rdbms\LBFactoryMulti::class
320		];
321
322		$class = $config['class'];
323		return $compat[$class] ?? $class;
324	}
325
326	/**
327	 * @param ILBFactory $lbFactory
328	 */
329	public static function setDomainAliases( ILBFactory $lbFactory ) {
330		$domain = DatabaseDomain::newFromId( $lbFactory->getLocalDomainID() );
331		// For compatibility with hyphenated $wgDBname values on older wikis, handle callers
332		// that assume corresponding database domain IDs and wiki IDs have identical values
333		$rawLocalDomain = strlen( $domain->getTablePrefix() )
334			? "{$domain->getDatabase()}-{$domain->getTablePrefix()}"
335			: (string)$domain->getDatabase();
336
337		$lbFactory->setDomainAliases( [ $rawLocalDomain => $domain ] );
338	}
339
340	/**
341	 * Log a database deprecation warning
342	 * @param string $msg Deprecation message
343	 * @internal For use with service wiring
344	 */
345	public static function logDeprecation( $msg ) {
346		if ( isset( self::$loggedDeprecations[$msg] ) ) {
347			return;
348		}
349		self::$loggedDeprecations[$msg] = true;
350		MWDebug::sendRawDeprecated( $msg, true, wfGetCaller() );
351	}
352}
353