Completed
Branch master (4de667)
by
unknown
26:16
created

DatabaseBase::serverIsReadOnly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * @defgroup Database Database
5
 *
6
 * This file deals with database interface functions
7
 * and query specifics/optimisations.
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 * @ingroup Database
26
 */
27
28
/**
29
 * Database abstraction object
30
 * @ingroup Database
31
 */
32
abstract class DatabaseBase implements IDatabase {
33
	/** Number of times to re-try an operation in case of deadlock */
34
	const DEADLOCK_TRIES = 4;
35
36
	/** Minimum time to wait before retry, in microseconds */
37
	const DEADLOCK_DELAY_MIN = 500000;
38
39
	/** Maximum time to wait before retry */
40
	const DEADLOCK_DELAY_MAX = 1500000;
41
42
	protected $mLastQuery = '';
43
	protected $mDoneWrites = false;
44
	protected $mPHPError = false;
45
46
	protected $mServer, $mUser, $mPassword, $mDBname;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
47
48
	/** @var BagOStuff APC cache */
49
	protected $srvCache;
50
51
	/** @var resource Database connection */
52
	protected $mConn = null;
53
	protected $mOpened = false;
54
55
	/** @var array[] List of (callable, method name) */
56
	protected $mTrxIdleCallbacks = [];
57
	/** @var array[] List of (callable, method name) */
58
	protected $mTrxPreCommitCallbacks = [];
59
	/** @var array[] List of (callable, method name) */
60
	protected $mTrxEndCallbacks = [];
61
	/** @var bool Whether to suppress triggering of post-commit callbacks */
62
	protected $suppressPostCommitCallbacks = false;
63
64
	protected $mTablePrefix;
65
	protected $mSchema;
66
	protected $mFlags;
67
	protected $mForeign;
68
	protected $mLBInfo = [];
69
	protected $mDefaultBigSelects = null;
70
	protected $mSchemaVars = false;
71
	/** @var array */
72
	protected $mSessionVars = [];
73
74
	protected $preparedArgs;
75
76
	protected $htmlErrors;
77
78
	protected $delimiter = ';';
79
80
	/**
81
	 * Either 1 if a transaction is active or 0 otherwise.
82
	 * The other Trx fields may not be meaningfull if this is 0.
83
	 *
84
	 * @var int
85
	 */
86
	protected $mTrxLevel = 0;
87
88
	/**
89
	 * Either a short hexidecimal string if a transaction is active or ""
90
	 *
91
	 * @var string
92
	 * @see DatabaseBase::mTrxLevel
93
	 */
94
	protected $mTrxShortId = '';
95
96
	/**
97
	 * The UNIX time that the transaction started. Callers can assume that if
98
	 * snapshot isolation is used, then the data is *at least* up to date to that
99
	 * point (possibly more up-to-date since the first SELECT defines the snapshot).
100
	 *
101
	 * @var float|null
102
	 * @see DatabaseBase::mTrxLevel
103
	 */
104
	private $mTrxTimestamp = null;
105
106
	/** @var float Lag estimate at the time of BEGIN */
107
	private $mTrxSlaveLag = null;
108
109
	/**
110
	 * Remembers the function name given for starting the most recent transaction via begin().
111
	 * Used to provide additional context for error reporting.
112
	 *
113
	 * @var string
114
	 * @see DatabaseBase::mTrxLevel
115
	 */
116
	private $mTrxFname = null;
117
118
	/**
119
	 * Record if possible write queries were done in the last transaction started
120
	 *
121
	 * @var bool
122
	 * @see DatabaseBase::mTrxLevel
123
	 */
124
	private $mTrxDoneWrites = false;
125
126
	/**
127
	 * Record if the current transaction was started implicitly due to DBO_TRX being set.
128
	 *
129
	 * @var bool
130
	 * @see DatabaseBase::mTrxLevel
131
	 */
132
	private $mTrxAutomatic = false;
133
134
	/**
135
	 * Array of levels of atomicity within transactions
136
	 *
137
	 * @var array
138
	 */
139
	private $mTrxAtomicLevels = [];
140
141
	/**
142
	 * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
143
	 *
144
	 * @var bool
145
	 */
146
	private $mTrxAutomaticAtomic = false;
147
148
	/**
149
	 * Track the write query callers of the current transaction
150
	 *
151
	 * @var string[]
152
	 */
153
	private $mTrxWriteCallers = [];
154
155
	/**
156
	 * Track the seconds spent in write queries for the current transaction
157
	 *
158
	 * @var float
159
	 */
160
	private $mTrxWriteDuration = 0.0;
161
162
	/** @var array Map of (name => 1) for locks obtained via lock() */
163
	private $mNamedLocksHeld = [];
164
165
	/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
166
	private $lazyMasterHandle;
167
168
	/**
169
	 * @since 1.21
170
	 * @var resource File handle for upgrade
171
	 */
172
	protected $fileHandle = null;
173
174
	/**
175
	 * @since 1.22
176
	 * @var string[] Process cache of VIEWs names in the database
177
	 */
178
	protected $allViews = null;
179
180
	/** @var TransactionProfiler */
181
	protected $trxProfiler;
182
183
	public function getServerInfo() {
184
		return $this->getServerVersion();
185
	}
186
187
	/**
188
	 * @return string Command delimiter used by this database engine
189
	 */
190
	public function getDelimiter() {
191
		return $this->delimiter;
192
	}
193
194
	/**
195
	 * Boolean, controls output of large amounts of debug information.
196
	 * @param bool|null $debug
197
	 *   - true to enable debugging
198
	 *   - false to disable debugging
199
	 *   - omitted or null to do nothing
200
	 *
201
	 * @return bool|null Previous value of the flag
202
	 */
203
	public function debug( $debug = null ) {
204
		return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
0 ignored issues
show
Bug introduced by
It seems like $debug defined by parameter $debug on line 203 can also be of type null; however, wfSetBit() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
205
	}
206
207
	public function bufferResults( $buffer = null ) {
208
		if ( is_null( $buffer ) ) {
209
			return !(bool)( $this->mFlags & DBO_NOBUFFER );
210
		} else {
211
			return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
212
		}
213
	}
214
215
	/**
216
	 * Turns on (false) or off (true) the automatic generation and sending
217
	 * of a "we're sorry, but there has been a database error" page on
218
	 * database errors. Default is on (false). When turned off, the
219
	 * code should use lastErrno() and lastError() to handle the
220
	 * situation as appropriate.
221
	 *
222
	 * Do not use this function outside of the Database classes.
223
	 *
224
	 * @param null|bool $ignoreErrors
225
	 * @return bool The previous value of the flag.
226
	 */
227
	protected function ignoreErrors( $ignoreErrors = null ) {
228
		return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
0 ignored issues
show
Bug introduced by
It seems like $ignoreErrors defined by parameter $ignoreErrors on line 227 can also be of type null; however, wfSetBit() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
229
	}
230
231
	public function trxLevel() {
232
		return $this->mTrxLevel;
233
	}
234
235
	public function trxTimestamp() {
236
		return $this->mTrxLevel ? $this->mTrxTimestamp : null;
237
	}
238
239
	public function tablePrefix( $prefix = null ) {
240
		return wfSetVar( $this->mTablePrefix, $prefix );
241
	}
242
243
	public function dbSchema( $schema = null ) {
244
		return wfSetVar( $this->mSchema, $schema );
245
	}
246
247
	/**
248
	 * Set the filehandle to copy write statements to.
249
	 *
250
	 * @param resource $fh File handle
251
	 */
252
	public function setFileHandle( $fh ) {
253
		$this->fileHandle = $fh;
254
	}
255
256
	public function getLBInfo( $name = null ) {
257
		if ( is_null( $name ) ) {
258
			return $this->mLBInfo;
259
		} else {
260
			if ( array_key_exists( $name, $this->mLBInfo ) ) {
261
				return $this->mLBInfo[$name];
262
			} else {
263
				return null;
264
			}
265
		}
266
	}
267
268
	public function setLBInfo( $name, $value = null ) {
269
		if ( is_null( $value ) ) {
270
			$this->mLBInfo = $name;
0 ignored issues
show
Documentation Bug introduced by
It seems like $name of type string is incompatible with the declared type array of property $mLBInfo.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
271
		} else {
272
			$this->mLBInfo[$name] = $value;
273
		}
274
	}
275
276
	/**
277
	 * Set a lazy-connecting DB handle to the master DB (for replication status purposes)
278
	 *
279
	 * @param IDatabase $conn
280
	 * @since 1.27
281
	 */
282
	public function setLazyMasterHandle( IDatabase $conn ) {
283
		$this->lazyMasterHandle = $conn;
284
	}
285
286
	/**
287
	 * @return IDatabase|null
288
	 * @see setLazyMasterHandle()
289
	 * @since 1.27
290
	 */
291
	public function getLazyMasterHandle() {
292
		return $this->lazyMasterHandle;
293
	}
294
295
	/**
296
	 * @return TransactionProfiler
297
	 */
298
	protected function getTransactionProfiler() {
299
		if ( !$this->trxProfiler ) {
300
			$this->trxProfiler = new TransactionProfiler();
301
		}
302
303
		return $this->trxProfiler;
304
	}
305
306
	/**
307
	 * @param TransactionProfiler $profiler
308
	 * @since 1.27
309
	 */
310
	public function setTransactionProfiler( TransactionProfiler $profiler ) {
311
		$this->trxProfiler = $profiler;
312
	}
313
314
	/**
315
	 * Returns true if this database supports (and uses) cascading deletes
316
	 *
317
	 * @return bool
318
	 */
319
	public function cascadingDeletes() {
320
		return false;
321
	}
322
323
	/**
324
	 * Returns true if this database supports (and uses) triggers (e.g. on the page table)
325
	 *
326
	 * @return bool
327
	 */
328
	public function cleanupTriggers() {
329
		return false;
330
	}
331
332
	/**
333
	 * Returns true if this database is strict about what can be put into an IP field.
334
	 * Specifically, it uses a NULL value instead of an empty string.
335
	 *
336
	 * @return bool
337
	 */
338
	public function strictIPs() {
339
		return false;
340
	}
341
342
	/**
343
	 * Returns true if this database uses timestamps rather than integers
344
	 *
345
	 * @return bool
346
	 */
347
	public function realTimestamps() {
348
		return false;
349
	}
350
351
	public function implicitGroupby() {
352
		return true;
353
	}
354
355
	public function implicitOrderby() {
356
		return true;
357
	}
358
359
	/**
360
	 * Returns true if this database can do a native search on IP columns
361
	 * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
362
	 *
363
	 * @return bool
364
	 */
365
	public function searchableIPs() {
366
		return false;
367
	}
368
369
	/**
370
	 * Returns true if this database can use functional indexes
371
	 *
372
	 * @return bool
373
	 */
374
	public function functionalIndexes() {
375
		return false;
376
	}
377
378
	public function lastQuery() {
379
		return $this->mLastQuery;
380
	}
381
382
	public function doneWrites() {
383
		return (bool)$this->mDoneWrites;
384
	}
385
386
	public function lastDoneWrites() {
387
		return $this->mDoneWrites ?: false;
388
	}
389
390
	public function writesPending() {
391
		return $this->mTrxLevel && $this->mTrxDoneWrites;
392
	}
393
394
	public function writesOrCallbacksPending() {
395
		return $this->mTrxLevel && (
396
			$this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
397
		);
398
	}
399
400
	public function pendingWriteQueryDuration() {
401
		return $this->mTrxLevel ? $this->mTrxWriteDuration : false;
402
	}
403
404
	public function pendingWriteCallers() {
405
		return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
406
	}
407
408
	public function isOpen() {
409
		return $this->mOpened;
410
	}
411
412
	public function setFlag( $flag ) {
413
		$this->mFlags |= $flag;
414
	}
415
416
	public function clearFlag( $flag ) {
417
		$this->mFlags &= ~$flag;
418
	}
419
420
	public function getFlag( $flag ) {
421
		return !!( $this->mFlags & $flag );
422
	}
423
424
	public function getProperty( $name ) {
425
		return $this->$name;
426
	}
427
428
	public function getWikiID() {
429
		if ( $this->mTablePrefix ) {
430
			return "{$this->mDBname}-{$this->mTablePrefix}";
431
		} else {
432
			return $this->mDBname;
433
		}
434
	}
435
436
	/**
437
	 * Return a path to the DBMS-specific SQL file if it exists,
438
	 * otherwise default SQL file
439
	 *
440
	 * @param string $filename
441
	 * @return string
442
	 */
443 View Code Duplication
	private function getSqlFilePath( $filename ) {
444
		global $IP;
445
		$dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename";
446
		if ( file_exists( $dbmsSpecificFilePath ) ) {
447
			return $dbmsSpecificFilePath;
448
		} else {
449
			return "$IP/maintenance/$filename";
450
		}
451
	}
452
453
	/**
454
	 * Return a path to the DBMS-specific schema file,
455
	 * otherwise default to tables.sql
456
	 *
457
	 * @return string
458
	 */
459
	public function getSchemaPath() {
460
		return $this->getSqlFilePath( 'tables.sql' );
461
	}
462
463
	/**
464
	 * Return a path to the DBMS-specific update key file,
465
	 * otherwise default to update-keys.sql
466
	 *
467
	 * @return string
468
	 */
469
	public function getUpdateKeysPath() {
470
		return $this->getSqlFilePath( 'update-keys.sql' );
471
	}
472
473
	/**
474
	 * Get information about an index into an object
475
	 * @param string $table Table name
476
	 * @param string $index Index name
477
	 * @param string $fname Calling function name
478
	 * @return mixed Database-specific index description class or false if the index does not exist
479
	 */
480
	abstract function indexInfo( $table, $index, $fname = __METHOD__ );
481
482
	/**
483
	 * Wrapper for addslashes()
484
	 *
485
	 * @param string $s String to be slashed.
486
	 * @return string Slashed string.
487
	 */
488
	abstract function strencode( $s );
489
490
	/**
491
	 * Constructor.
492
	 *
493
	 * FIXME: It is possible to construct a Database object with no associated
494
	 * connection object, by specifying no parameters to __construct(). This
495
	 * feature is deprecated and should be removed.
496
	 *
497
	 * DatabaseBase subclasses should not be constructed directly in external
498
	 * code. DatabaseBase::factory() should be used instead.
499
	 *
500
	 * @param array $params Parameters passed from DatabaseBase::factory()
501
	 */
502
	function __construct( array $params ) {
503
		global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
504
505
		$this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
506
507
		$server = $params['host'];
508
		$user = $params['user'];
509
		$password = $params['password'];
510
		$dbName = $params['dbname'];
511
		$flags = $params['flags'];
512
		$tablePrefix = $params['tablePrefix'];
513
		$schema = $params['schema'];
514
		$foreign = $params['foreign'];
515
516
		$this->mFlags = $flags;
517
		if ( $this->mFlags & DBO_DEFAULT ) {
518
			if ( $wgCommandLineMode ) {
519
				$this->mFlags &= ~DBO_TRX;
520
			} else {
521
				$this->mFlags |= DBO_TRX;
522
			}
523
		}
524
525
		$this->mSessionVars = $params['variables'];
526
527
		/** Get the default table prefix*/
528
		if ( $tablePrefix === 'get from global' ) {
529
			$this->mTablePrefix = $wgDBprefix;
530
		} else {
531
			$this->mTablePrefix = $tablePrefix;
532
		}
533
534
		/** Get the database schema*/
535
		if ( $schema === 'get from global' ) {
536
			$this->mSchema = $wgDBmwschema;
537
		} else {
538
			$this->mSchema = $schema;
539
		}
540
541
		$this->mForeign = $foreign;
542
543
		if ( isset( $params['trxProfiler'] ) ) {
544
			$this->trxProfiler = $params['trxProfiler']; // override
545
		}
546
547
		if ( $user ) {
548
			$this->open( $server, $user, $password, $dbName );
549
		}
550
	}
551
552
	/**
553
	 * Called by serialize. Throw an exception when DB connection is serialized.
554
	 * This causes problems on some database engines because the connection is
555
	 * not restored on unserialize.
556
	 */
557
	public function __sleep() {
558
		throw new MWException( 'Database serialization may cause problems, since ' .
559
			'the connection is not restored on wakeup.' );
560
	}
561
562
	/**
563
	 * Given a DB type, construct the name of the appropriate child class of
564
	 * DatabaseBase. This is designed to replace all of the manual stuff like:
565
	 *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
566
	 * as well as validate against the canonical list of DB types we have
567
	 *
568
	 * This factory function is mostly useful for when you need to connect to a
569
	 * database other than the MediaWiki default (such as for external auth,
570
	 * an extension, et cetera). Do not use this to connect to the MediaWiki
571
	 * database. Example uses in core:
572
	 * @see LoadBalancer::reallyOpenConnection()
573
	 * @see ForeignDBRepo::getMasterDB()
574
	 * @see WebInstallerDBConnect::execute()
575
	 *
576
	 * @since 1.18
577
	 *
578
	 * @param string $dbType A possible DB type
579
	 * @param array $p An array of options to pass to the constructor.
580
	 *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
581
	 * @throws MWException If the database driver or extension cannot be found
582
	 * @return DatabaseBase|null DatabaseBase subclass or null
583
	 */
584
	final public static function factory( $dbType, $p = [] ) {
585
		$canonicalDBTypes = [
586
			'mysql' => [ 'mysqli', 'mysql' ],
587
			'postgres' => [],
588
			'sqlite' => [],
589
			'oracle' => [],
590
			'mssql' => [],
591
		];
592
593
		$driver = false;
594
		$dbType = strtolower( $dbType );
595
		if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
596
			$possibleDrivers = $canonicalDBTypes[$dbType];
597
			if ( !empty( $p['driver'] ) ) {
598
				if ( in_array( $p['driver'], $possibleDrivers ) ) {
599
					$driver = $p['driver'];
600
				} else {
601
					throw new MWException( __METHOD__ .
602
						" cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
603
				}
604
			} else {
605
				foreach ( $possibleDrivers as $posDriver ) {
606
					if ( extension_loaded( $posDriver ) ) {
607
						$driver = $posDriver;
608
						break;
609
					}
610
				}
611
			}
612
		} else {
613
			$driver = $dbType;
614
		}
615
		if ( $driver === false ) {
616
			throw new MWException( __METHOD__ .
617
				" no viable database extension found for type '$dbType'" );
618
		}
619
620
		// Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
621
		// and everything else doesn't use a schema (e.g. null)
622
		// Although postgres and oracle support schemas, we don't use them (yet)
623
		// to maintain backwards compatibility
624
		$defaultSchemas = [
625
			'mssql' => 'get from global',
626
		];
627
628
		$class = 'Database' . ucfirst( $driver );
629
		if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
630
			// Resolve some defaults for b/c
631
			$p['host'] = isset( $p['host'] ) ? $p['host'] : false;
632
			$p['user'] = isset( $p['user'] ) ? $p['user'] : false;
633
			$p['password'] = isset( $p['password'] ) ? $p['password'] : false;
634
			$p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
635
			$p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
636
			$p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
637
			$p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
638
			if ( !isset( $p['schema'] ) ) {
639
				$p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
640
			}
641
			$p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
642
643
			return new $class( $p );
644
		} else {
645
			return null;
646
		}
647
	}
648
649
	protected function installErrorHandler() {
650
		$this->mPHPError = false;
651
		$this->htmlErrors = ini_set( 'html_errors', '0' );
652
		set_error_handler( [ $this, 'connectionErrorHandler' ] );
653
	}
654
655
	/**
656
	 * @return bool|string
657
	 */
658
	protected function restoreErrorHandler() {
659
		restore_error_handler();
660
		if ( $this->htmlErrors !== false ) {
661
			ini_set( 'html_errors', $this->htmlErrors );
662
		}
663
		if ( $this->mPHPError ) {
664
			$error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
665
			$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
666
667
			return $error;
668
		} else {
669
			return false;
670
		}
671
	}
672
673
	/**
674
	 * @param int $errno
675
	 * @param string $errstr
676
	 */
677
	public function connectionErrorHandler( $errno, $errstr ) {
678
		$this->mPHPError = $errstr;
0 ignored issues
show
Documentation Bug introduced by
The property $mPHPError was declared of type boolean, but $errstr is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
679
	}
680
681
	/**
682
	 * Create a log context to pass to wfLogDBError or other logging functions.
683
	 *
684
	 * @param array $extras Additional data to add to context
685
	 * @return array
686
	 */
687
	protected function getLogContext( array $extras = [] ) {
688
		return array_merge(
689
			[
690
				'db_server' => $this->mServer,
691
				'db_name' => $this->mDBname,
692
				'db_user' => $this->mUser,
693
			],
694
			$extras
695
		);
696
	}
697
698
	public function close() {
699
		if ( $this->mConn ) {
700
			if ( $this->trxLevel() ) {
701
				if ( !$this->mTrxAutomatic ) {
702
					wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " .
703
						" performing implicit commit before closing connection!" );
704
				}
705
706
				$this->commit( __METHOD__, 'flush' );
707
			}
708
709
			$closed = $this->closeConnection();
710
			$this->mConn = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type resource of property $mConn.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
711
		} elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
712
			throw new MWException( "Transaction callbacks still pending." );
713
		} else {
714
			$closed = true;
715
		}
716
		$this->mOpened = false;
717
718
		return $closed;
719
	}
720
721
	/**
722
	 * Make sure isOpen() returns true as a sanity check
723
	 *
724
	 * @throws DBUnexpectedError
725
	 */
726
	protected function assertOpen() {
727
		if ( !$this->isOpen() ) {
728
			throw new DBUnexpectedError( $this, "DB connection was already closed." );
729
		}
730
	}
731
732
	/**
733
	 * Closes underlying database connection
734
	 * @since 1.20
735
	 * @return bool Whether connection was closed successfully
736
	 */
737
	abstract protected function closeConnection();
738
739
	function reportConnectionError( $error = 'Unknown error' ) {
740
		$myError = $this->lastError();
741
		if ( $myError ) {
742
			$error = $myError;
743
		}
744
745
		# New method
746
		throw new DBConnectionError( $this, $error );
747
	}
748
749
	/**
750
	 * The DBMS-dependent part of query()
751
	 *
752
	 * @param string $sql SQL query.
753
	 * @return ResultWrapper|bool Result object to feed to fetchObject,
754
	 *   fetchRow, ...; or false on failure
755
	 */
756
	abstract protected function doQuery( $sql );
757
758
	/**
759
	 * Determine whether a query writes to the DB.
760
	 * Should return true if unsure.
761
	 *
762
	 * @param string $sql
763
	 * @return bool
764
	 */
765
	protected function isWriteQuery( $sql ) {
766
		return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
767
	}
768
769
	/**
770
	 * Determine whether a SQL statement is sensitive to isolation level.
771
	 * A SQL statement is considered transactable if its result could vary
772
	 * depending on the transaction isolation level. Operational commands
773
	 * such as 'SET' and 'SHOW' are not considered to be transactable.
774
	 *
775
	 * @param string $sql
776
	 * @return bool
777
	 */
778
	protected function isTransactableQuery( $sql ) {
779
		$verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
780
		return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ] );
781
	}
782
783
	public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
784
		global $wgUser;
785
786
		$this->mLastQuery = $sql;
787
788
		$isWriteQuery = $this->isWriteQuery( $sql );
789
		if ( $isWriteQuery ) {
790
			$reason = $this->getReadOnlyReason();
791
			if ( $reason !== false ) {
792
				throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
793
			}
794
			# Set a flag indicating that writes have been done
795
			$this->mDoneWrites = microtime( true );
0 ignored issues
show
Documentation Bug introduced by
The property $mDoneWrites was declared of type boolean, but microtime(true) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
796
		}
797
798
		# Add a comment for easy SHOW PROCESSLIST interpretation
799
		if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
800
			$userName = $wgUser->getName();
801
			if ( mb_strlen( $userName ) > 15 ) {
802
				$userName = mb_substr( $userName, 0, 15 ) . '...';
803
			}
804
			$userName = str_replace( '/', '', $userName );
805
		} else {
806
			$userName = '';
807
		}
808
809
		// Add trace comment to the begin of the sql string, right after the operator.
810
		// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
811
		$commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
812
813
		if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX ) && $this->isTransactableQuery( $sql ) ) {
814
			$this->begin( __METHOD__ . " ($fname)" );
815
			$this->mTrxAutomatic = true;
816
		}
817
818
		# Keep track of whether the transaction has write queries pending
819
		if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
820
			$this->mTrxDoneWrites = true;
821
			$this->getTransactionProfiler()->transactionWritingIn(
822
				$this->mServer, $this->mDBname, $this->mTrxShortId );
823
		}
824
825
		$isMaster = !is_null( $this->getLBInfo( 'master' ) );
826
		# generalizeSQL will probably cut down the query to reasonable
827
		# logging size most of the time. The substr is really just a sanity check.
828
		if ( $isMaster ) {
829
			$queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
830
			$totalProf = 'DatabaseBase::query-master';
831
		} else {
832
			$queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
833
			$totalProf = 'DatabaseBase::query';
834
		}
835
		# Include query transaction state
836
		$queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
837
838
		$profiler = Profiler::instance();
839
		if ( !$profiler instanceof ProfilerStub ) {
840
			$totalProfSection = $profiler->scopedProfileIn( $totalProf );
841
			$queryProfSection = $profiler->scopedProfileIn( $queryProf );
842
		}
843
844
		if ( $this->debug() ) {
845
			wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
846
		}
847
848
		$queryId = MWDebug::query( $sql, $fname, $isMaster );
849
850
		# Avoid fatals if close() was called
851
		$this->assertOpen();
852
853
		# Do the query and handle errors
854
		$startTime = microtime( true );
855
		$ret = $this->doQuery( $commentedSql );
856
		$queryRuntime = microtime( true ) - $startTime;
857
		# Log the query time and feed it into the DB trx profiler
858
		$this->getTransactionProfiler()->recordQueryCompletion(
859
			$queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
860
861
		MWDebug::queryTime( $queryId );
862
863
		# Try reconnecting if the connection was lost
864
		if ( false === $ret && $this->wasErrorReissuable() ) {
865
			# Transaction is gone; this can mean lost writes or REPEATABLE-READ snapshots
866
			$hadTrx = $this->mTrxLevel;
867
			# T127428: for non-write transactions, a disconnect and a COMMIT are similar:
868
			# neither changed data and in both cases any read snapshots are reset anyway.
869
			$isNoopCommit = ( !$this->writesOrCallbacksPending() && $sql === 'COMMIT' );
870
			# Update state tracking to reflect transaction loss
871
			$this->mTrxLevel = 0;
872
			$this->mTrxIdleCallbacks = []; // bug 65263
873
			$this->mTrxPreCommitCallbacks = []; // bug 65263
874
			wfDebug( "Connection lost, reconnecting...\n" );
875
			# Stash the last error values since ping() might clear them
876
			$lastError = $this->lastError();
877
			$lastErrno = $this->lastErrno();
878
			if ( $this->ping() ) {
879
				wfDebug( "Reconnected\n" );
880
				$server = $this->getServer();
881
				$msg = __METHOD__ . ": lost connection to $server; reconnected";
882
				wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
883
884
				if ( ( $hadTrx && !$isNoopCommit ) || $this->mNamedLocksHeld ) {
885
					# Leave $ret as false and let an error be reported.
886
					# Callers may catch the exception and continue to use the DB.
887
					$this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
888
				} else {
889
					# Should be safe to silently retry (no trx/callbacks/locks)
890
					$startTime = microtime( true );
891
					$ret = $this->doQuery( $commentedSql );
892
					$queryRuntime = microtime( true ) - $startTime;
893
					# Log the query time and feed it into the DB trx profiler
894
					$this->getTransactionProfiler()->recordQueryCompletion(
895
						$queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
896
				}
897
			} else {
898
				wfDebug( "Failed\n" );
899
			}
900
		}
901
902
		if ( false === $ret ) {
903
			$this->reportQueryError(
904
				$this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
905
		}
906
907
		$res = $this->resultObject( $ret );
908
909
		// Destroy profile sections in the opposite order to their creation
910
		ScopedCallback::consume( $queryProfSection );
911
		ScopedCallback::consume( $totalProfSection );
912
913
		if ( $isWriteQuery && $this->mTrxLevel ) {
914
			$this->mTrxWriteDuration += $queryRuntime;
915
			$this->mTrxWriteCallers[] = $fname;
916
		}
917
918
		return $res;
919
	}
920
921
	public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
922
		if ( $this->ignoreErrors() || $tempIgnore ) {
923
			wfDebug( "SQL ERROR (ignored): $error\n" );
924
		} else {
925
			$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
926
			wfLogDBError(
927
				"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
928
				$this->getLogContext( [
929
					'method' => __METHOD__,
930
					'errno' => $errno,
931
					'error' => $error,
932
					'sql1line' => $sql1line,
933
					'fname' => $fname,
934
				] )
935
			);
936
			wfDebug( "SQL ERROR: " . $error . "\n" );
937
			throw new DBQueryError( $this, $error, $errno, $sql, $fname );
938
		}
939
	}
940
941
	/**
942
	 * Intended to be compatible with the PEAR::DB wrapper functions.
943
	 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
944
	 *
945
	 * ? = scalar value, quoted as necessary
946
	 * ! = raw SQL bit (a function for instance)
947
	 * & = filename; reads the file and inserts as a blob
948
	 *     (we don't use this though...)
949
	 *
950
	 * @param string $sql
951
	 * @param string $func
952
	 *
953
	 * @return array
954
	 */
955
	protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
956
		/* MySQL doesn't support prepared statements (yet), so just
957
		 * pack up the query for reference. We'll manually replace
958
		 * the bits later.
959
		 */
960
		return [ 'query' => $sql, 'func' => $func ];
961
	}
962
963
	/**
964
	 * Free a prepared query, generated by prepare().
965
	 * @param string $prepared
966
	 */
967
	protected function freePrepared( $prepared ) {
968
		/* No-op by default */
969
	}
970
971
	/**
972
	 * Execute a prepared query with the various arguments
973
	 * @param string $prepared The prepared sql
974
	 * @param mixed $args Either an array here, or put scalars as varargs
975
	 *
976
	 * @return ResultWrapper
977
	 */
978
	public function execute( $prepared, $args = null ) {
979
		if ( !is_array( $args ) ) {
980
			# Pull the var args
981
			$args = func_get_args();
982
			array_shift( $args );
983
		}
984
985
		$sql = $this->fillPrepared( $prepared['query'], $args );
986
987
		return $this->query( $sql, $prepared['func'] );
988
	}
989
990
	/**
991
	 * For faking prepared SQL statements on DBs that don't support it directly.
992
	 *
993
	 * @param string $preparedQuery A 'preparable' SQL statement
994
	 * @param array $args Array of Arguments to fill it with
995
	 * @return string Executable SQL
996
	 */
997
	public function fillPrepared( $preparedQuery, $args ) {
998
		reset( $args );
999
		$this->preparedArgs =& $args;
1000
1001
		return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
1002
			[ &$this, 'fillPreparedArg' ], $preparedQuery );
1003
	}
1004
1005
	/**
1006
	 * preg_callback func for fillPrepared()
1007
	 * The arguments should be in $this->preparedArgs and must not be touched
1008
	 * while we're doing this.
1009
	 *
1010
	 * @param array $matches
1011
	 * @throws DBUnexpectedError
1012
	 * @return string
1013
	 */
1014
	protected function fillPreparedArg( $matches ) {
1015
		switch ( $matches[1] ) {
1016
			case '\\?':
1017
				return '?';
1018
			case '\\!':
1019
				return '!';
1020
			case '\\&':
1021
				return '&';
1022
		}
1023
1024
		list( /* $n */, $arg ) = each( $this->preparedArgs );
1025
1026
		switch ( $matches[1] ) {
1027
			case '?':
1028
				return $this->addQuotes( $arg );
1029
			case '!':
1030
				return $arg;
1031
			case '&':
1032
				# return $this->addQuotes( file_get_contents( $arg ) );
1033
				throw new DBUnexpectedError(
1034
					$this,
1035
					'& mode is not implemented. If it\'s really needed, uncomment the line above.'
1036
				);
1037
			default:
1038
				throw new DBUnexpectedError(
1039
					$this,
1040
					'Received invalid match. This should never happen!'
1041
				);
1042
		}
1043
	}
1044
1045
	public function freeResult( $res ) {
1046
	}
1047
1048
	public function selectField(
1049
		$table, $var, $cond = '', $fname = __METHOD__, $options = []
1050
	) {
1051
		if ( $var === '*' ) { // sanity
1052
			throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1053
		}
1054
1055
		if ( !is_array( $options ) ) {
1056
			$options = [ $options ];
1057
		}
1058
1059
		$options['LIMIT'] = 1;
1060
1061
		$res = $this->select( $table, $var, $cond, $fname, $options );
1062
		if ( $res === false || !$this->numRows( $res ) ) {
1063
			return false;
1064
		}
1065
1066
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->select($table, $v...cond, $fname, $options) on line 1061 can also be of type boolean; however, IDatabase::fetchRow() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1067
1068
		if ( $row !== false ) {
1069
			return reset( $row );
1070
		} else {
1071
			return false;
1072
		}
1073
	}
1074
1075
	public function selectFieldValues(
1076
		$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1077
	) {
1078
		if ( $var === '*' ) { // sanity
1079
			throw new DBUnexpectedError( $this, "Cannot use a * field" );
1080
		} elseif ( !is_string( $var ) ) { // sanity
1081
			throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1082
		}
1083
1084
		if ( !is_array( $options ) ) {
1085
			$options = [ $options ];
1086
		}
1087
1088
		$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1089
		if ( $res === false ) {
1090
			return false;
1091
		}
1092
1093
		$values = [];
1094
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1095
			$values[] = $row->$var;
1096
		}
1097
1098
		return $values;
1099
	}
1100
1101
	/**
1102
	 * Returns an optional USE INDEX clause to go after the table, and a
1103
	 * string to go at the end of the query.
1104
	 *
1105
	 * @param array $options Associative array of options to be turned into
1106
	 *   an SQL query, valid keys are listed in the function.
1107
	 * @return array
1108
	 * @see DatabaseBase::select()
1109
	 */
1110
	public function makeSelectOptions( $options ) {
1111
		$preLimitTail = $postLimitTail = '';
1112
		$startOpts = '';
1113
1114
		$noKeyOptions = [];
1115
1116
		foreach ( $options as $key => $option ) {
1117
			if ( is_numeric( $key ) ) {
1118
				$noKeyOptions[$option] = true;
1119
			}
1120
		}
1121
1122
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1123
1124
		$preLimitTail .= $this->makeOrderBy( $options );
1125
1126
		// if (isset($options['LIMIT'])) {
1127
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1128
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1129
		// 		: false);
1130
		// }
1131
1132
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1133
			$postLimitTail .= ' FOR UPDATE';
1134
		}
1135
1136
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1137
			$postLimitTail .= ' LOCK IN SHARE MODE';
1138
		}
1139
1140
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1141
			$startOpts .= 'DISTINCT';
1142
		}
1143
1144
		# Various MySQL extensions
1145
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1146
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1147
		}
1148
1149
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1150
			$startOpts .= ' HIGH_PRIORITY';
1151
		}
1152
1153
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1154
			$startOpts .= ' SQL_BIG_RESULT';
1155
		}
1156
1157
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1158
			$startOpts .= ' SQL_BUFFER_RESULT';
1159
		}
1160
1161
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1162
			$startOpts .= ' SQL_SMALL_RESULT';
1163
		}
1164
1165
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1166
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1167
		}
1168
1169
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1170
			$startOpts .= ' SQL_CACHE';
1171
		}
1172
1173
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1174
			$startOpts .= ' SQL_NO_CACHE';
1175
		}
1176
1177 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1178
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1179
		} else {
1180
			$useIndex = '';
1181
		}
1182
1183
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1184
	}
1185
1186
	/**
1187
	 * Returns an optional GROUP BY with an optional HAVING
1188
	 *
1189
	 * @param array $options Associative array of options
1190
	 * @return string
1191
	 * @see DatabaseBase::select()
1192
	 * @since 1.21
1193
	 */
1194
	public function makeGroupByWithHaving( $options ) {
1195
		$sql = '';
1196 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1197
			$gb = is_array( $options['GROUP BY'] )
1198
				? implode( ',', $options['GROUP BY'] )
1199
				: $options['GROUP BY'];
1200
			$sql .= ' GROUP BY ' . $gb;
1201
		}
1202 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1203
			$having = is_array( $options['HAVING'] )
1204
				? $this->makeList( $options['HAVING'], LIST_AND )
1205
				: $options['HAVING'];
1206
			$sql .= ' HAVING ' . $having;
1207
		}
1208
1209
		return $sql;
1210
	}
1211
1212
	/**
1213
	 * Returns an optional ORDER BY
1214
	 *
1215
	 * @param array $options Associative array of options
1216
	 * @return string
1217
	 * @see DatabaseBase::select()
1218
	 * @since 1.21
1219
	 */
1220
	public function makeOrderBy( $options ) {
1221 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1222
			$ob = is_array( $options['ORDER BY'] )
1223
				? implode( ',', $options['ORDER BY'] )
1224
				: $options['ORDER BY'];
1225
1226
			return ' ORDER BY ' . $ob;
1227
		}
1228
1229
		return '';
1230
	}
1231
1232
	// See IDatabase::select for the docs for this function
1233
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1234
		$options = [], $join_conds = [] ) {
1235
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1236
1237
		return $this->query( $sql, $fname );
1238
	}
1239
1240
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1241
		$options = [], $join_conds = []
1242
	) {
1243
		if ( is_array( $vars ) ) {
1244
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1245
		}
1246
1247
		$options = (array)$options;
1248
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1249
			? $options['USE INDEX']
1250
			: [];
1251
1252
		if ( is_array( $table ) ) {
1253
			$from = ' FROM ' .
1254
				$this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
1255
		} elseif ( $table != '' ) {
1256
			if ( $table[0] == ' ' ) {
1257
				$from = ' FROM ' . $table;
1258
			} else {
1259
				$from = ' FROM ' .
1260
					$this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
1261
			}
1262
		} else {
1263
			$from = '';
1264
		}
1265
1266
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
1267
			$this->makeSelectOptions( $options );
1268
1269
		if ( !empty( $conds ) ) {
1270
			if ( is_array( $conds ) ) {
1271
				$conds = $this->makeList( $conds, LIST_AND );
1272
			}
1273
			$sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1274
		} else {
1275
			$sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1276
		}
1277
1278
		if ( isset( $options['LIMIT'] ) ) {
1279
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1280
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1281
		}
1282
		$sql = "$sql $postLimitTail";
1283
1284
		if ( isset( $options['EXPLAIN'] ) ) {
1285
			$sql = 'EXPLAIN ' . $sql;
1286
		}
1287
1288
		return $sql;
1289
	}
1290
1291
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1292
		$options = [], $join_conds = []
1293
	) {
1294
		$options = (array)$options;
1295
		$options['LIMIT'] = 1;
1296
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1297
1298
		if ( $res === false ) {
1299
			return false;
1300
		}
1301
1302
		if ( !$this->numRows( $res ) ) {
1303
			return false;
1304
		}
1305
1306
		$obj = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->select($table, $v... $options, $join_conds) on line 1296 can also be of type boolean; however, IDatabase::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1307
1308
		return $obj;
1309
	}
1310
1311
	public function estimateRowCount(
1312
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1313
	) {
1314
		$rows = 0;
1315
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1316
1317 View Code Duplication
		if ( $res ) {
1318
			$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->select($table, ar...onds, $fname, $options) on line 1315 can also be of type boolean; however, IDatabase::fetchRow() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1319
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1320
		}
1321
1322
		return $rows;
1323
	}
1324
1325
	public function selectRowCount(
1326
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1327
	) {
1328
		$rows = 0;
1329
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1330
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1331
1332 View Code Duplication
		if ( $res ) {
1333
			$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query("SELECT COU...l}) tmp_count", $fname) on line 1330 can also be of type boolean; however, IDatabase::fetchRow() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1334
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1335
		}
1336
1337
		return $rows;
1338
	}
1339
1340
	/**
1341
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1342
	 * It's only slightly flawed. Don't use for anything important.
1343
	 *
1344
	 * @param string $sql A SQL Query
1345
	 *
1346
	 * @return string
1347
	 */
1348
	protected static function generalizeSQL( $sql ) {
1349
		# This does the same as the regexp below would do, but in such a way
1350
		# as to avoid crashing php on some large strings.
1351
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1352
1353
		$sql = str_replace( "\\\\", '', $sql );
1354
		$sql = str_replace( "\\'", '', $sql );
1355
		$sql = str_replace( "\\\"", '', $sql );
1356
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1357
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1358
1359
		# All newlines, tabs, etc replaced by single space
1360
		$sql = preg_replace( '/\s+/', ' ', $sql );
1361
1362
		# All numbers => N,
1363
		# except the ones surrounded by characters, e.g. l10n
1364
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1365
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1366
1367
		return $sql;
1368
	}
1369
1370
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1371
		$info = $this->fieldInfo( $table, $field );
1372
1373
		return (bool)$info;
1374
	}
1375
1376
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1377
		if ( !$this->tableExists( $table ) ) {
1378
			return null;
1379
		}
1380
1381
		$info = $this->indexInfo( $table, $index, $fname );
1382
		if ( is_null( $info ) ) {
1383
			return null;
1384
		} else {
1385
			return $info !== false;
1386
		}
1387
	}
1388
1389
	public function tableExists( $table, $fname = __METHOD__ ) {
1390
		$table = $this->tableName( $table );
1391
		$old = $this->ignoreErrors( true );
1392
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1393
		$this->ignoreErrors( $old );
1394
1395
		return (bool)$res;
1396
	}
1397
1398
	public function indexUnique( $table, $index ) {
1399
		$indexInfo = $this->indexInfo( $table, $index );
1400
1401
		if ( !$indexInfo ) {
1402
			return null;
1403
		}
1404
1405
		return !$indexInfo[0]->Non_unique;
1406
	}
1407
1408
	/**
1409
	 * Helper for DatabaseBase::insert().
1410
	 *
1411
	 * @param array $options
1412
	 * @return string
1413
	 */
1414
	protected function makeInsertOptions( $options ) {
1415
		return implode( ' ', $options );
1416
	}
1417
1418
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1419
		# No rows to insert, easy just return now
1420
		if ( !count( $a ) ) {
1421
			return true;
1422
		}
1423
1424
		$table = $this->tableName( $table );
1425
1426
		if ( !is_array( $options ) ) {
1427
			$options = [ $options ];
1428
		}
1429
1430
		$fh = null;
1431
		if ( isset( $options['fileHandle'] ) ) {
1432
			$fh = $options['fileHandle'];
1433
		}
1434
		$options = $this->makeInsertOptions( $options );
1435
1436
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1437
			$multi = true;
1438
			$keys = array_keys( $a[0] );
1439
		} else {
1440
			$multi = false;
1441
			$keys = array_keys( $a );
1442
		}
1443
1444
		$sql = 'INSERT ' . $options .
1445
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1446
1447
		if ( $multi ) {
1448
			$first = true;
1449 View Code Duplication
			foreach ( $a as $row ) {
1450
				if ( $first ) {
1451
					$first = false;
1452
				} else {
1453
					$sql .= ',';
1454
				}
1455
				$sql .= '(' . $this->makeList( $row ) . ')';
1456
			}
1457
		} else {
1458
			$sql .= '(' . $this->makeList( $a ) . ')';
1459
		}
1460
1461
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1462
			return false;
1463
		} elseif ( $fh !== null ) {
1464
			return true;
1465
		}
1466
1467
		return (bool)$this->query( $sql, $fname );
1468
	}
1469
1470
	/**
1471
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1472
	 *
1473
	 * @param array $options
1474
	 * @return array
1475
	 */
1476
	protected function makeUpdateOptionsArray( $options ) {
1477
		if ( !is_array( $options ) ) {
1478
			$options = [ $options ];
1479
		}
1480
1481
		$opts = [];
1482
1483
		if ( in_array( 'LOW_PRIORITY', $options ) ) {
1484
			$opts[] = $this->lowPriorityOption();
1485
		}
1486
1487
		if ( in_array( 'IGNORE', $options ) ) {
1488
			$opts[] = 'IGNORE';
1489
		}
1490
1491
		return $opts;
1492
	}
1493
1494
	/**
1495
	 * Make UPDATE options for the DatabaseBase::update function
1496
	 *
1497
	 * @param array $options The options passed to DatabaseBase::update
1498
	 * @return string
1499
	 */
1500
	protected function makeUpdateOptions( $options ) {
1501
		$opts = $this->makeUpdateOptionsArray( $options );
1502
1503
		return implode( ' ', $opts );
1504
	}
1505
1506
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1507
		$table = $this->tableName( $table );
1508
		$opts = $this->makeUpdateOptions( $options );
1509
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1510
1511 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
1512
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1513
		}
1514
1515
		return $this->query( $sql, $fname );
1516
	}
1517
1518
	public function makeList( $a, $mode = LIST_COMMA ) {
1519
		if ( !is_array( $a ) ) {
1520
			throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1521
		}
1522
1523
		$first = true;
1524
		$list = '';
1525
1526
		foreach ( $a as $field => $value ) {
1527
			if ( !$first ) {
1528
				if ( $mode == LIST_AND ) {
1529
					$list .= ' AND ';
1530
				} elseif ( $mode == LIST_OR ) {
1531
					$list .= ' OR ';
1532
				} else {
1533
					$list .= ',';
1534
				}
1535
			} else {
1536
				$first = false;
1537
			}
1538
1539
			if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1540
				$list .= "($value)";
1541
			} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1542
				$list .= "$value";
1543
			} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1544
				// Remove null from array to be handled separately if found
1545
				$includeNull = false;
1546
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1547
					$includeNull = true;
1548
					unset( $value[$nullKey] );
1549
				}
1550
				if ( count( $value ) == 0 && !$includeNull ) {
1551
					throw new MWException( __METHOD__ . ": empty input for field $field" );
1552
				} elseif ( count( $value ) == 0 ) {
1553
					// only check if $field is null
1554
					$list .= "$field IS NULL";
1555
				} else {
1556
					// IN clause contains at least one valid element
1557
					if ( $includeNull ) {
1558
						// Group subconditions to ensure correct precedence
1559
						$list .= '(';
1560
					}
1561
					if ( count( $value ) == 1 ) {
1562
						// Special-case single values, as IN isn't terribly efficient
1563
						// Don't necessarily assume the single key is 0; we don't
1564
						// enforce linear numeric ordering on other arrays here.
1565
						$value = array_values( $value )[0];
1566
						$list .= $field . " = " . $this->addQuotes( $value );
1567
					} else {
1568
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1569
					}
1570
					// if null present in array, append IS NULL
1571
					if ( $includeNull ) {
1572
						$list .= " OR $field IS NULL)";
1573
					}
1574
				}
1575
			} elseif ( $value === null ) {
1576 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR ) {
1577
					$list .= "$field IS ";
1578
				} elseif ( $mode == LIST_SET ) {
1579
					$list .= "$field = ";
1580
				}
1581
				$list .= 'NULL';
1582
			} else {
1583 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1584
					$list .= "$field = ";
1585
				}
1586
				$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1587
			}
1588
		}
1589
1590
		return $list;
1591
	}
1592
1593
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1594
		$conds = [];
1595
1596
		foreach ( $data as $base => $sub ) {
1597
			if ( count( $sub ) ) {
1598
				$conds[] = $this->makeList(
1599
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1600
					LIST_AND );
1601
			}
1602
		}
1603
1604
		if ( $conds ) {
1605
			return $this->makeList( $conds, LIST_OR );
1606
		} else {
1607
			// Nothing to search for...
1608
			return false;
1609
		}
1610
	}
1611
1612
	/**
1613
	 * Return aggregated value alias
1614
	 *
1615
	 * @param array $valuedata
1616
	 * @param string $valuename
1617
	 *
1618
	 * @return string
1619
	 */
1620
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1621
		return $valuename;
1622
	}
1623
1624
	public function bitNot( $field ) {
1625
		return "(~$field)";
1626
	}
1627
1628
	public function bitAnd( $fieldLeft, $fieldRight ) {
1629
		return "($fieldLeft & $fieldRight)";
1630
	}
1631
1632
	public function bitOr( $fieldLeft, $fieldRight ) {
1633
		return "($fieldLeft | $fieldRight)";
1634
	}
1635
1636
	public function buildConcat( $stringList ) {
1637
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1638
	}
1639
1640 View Code Duplication
	public function buildGroupConcatField(
1641
		$delim, $table, $field, $conds = '', $join_conds = []
1642
	) {
1643
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1644
1645
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1646
	}
1647
1648
	public function selectDB( $db ) {
1649
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1650
		# if your database engine supports a concept similar to MySQL's
1651
		# databases you may as well.
1652
		$this->mDBname = $db;
1653
1654
		return true;
1655
	}
1656
1657
	public function getDBname() {
1658
		return $this->mDBname;
1659
	}
1660
1661
	public function getServer() {
1662
		return $this->mServer;
1663
	}
1664
1665
	/**
1666
	 * Format a table name ready for use in constructing an SQL query
1667
	 *
1668
	 * This does two important things: it quotes the table names to clean them up,
1669
	 * and it adds a table prefix if only given a table name with no quotes.
1670
	 *
1671
	 * All functions of this object which require a table name call this function
1672
	 * themselves. Pass the canonical name to such functions. This is only needed
1673
	 * when calling query() directly.
1674
	 *
1675
	 * @note This function does not sanitize user input. It is not safe to use
1676
	 *   this function to escape user input.
1677
	 * @param string $name Database table name
1678
	 * @param string $format One of:
1679
	 *   quoted - Automatically pass the table name through addIdentifierQuotes()
1680
	 *            so that it can be used in a query.
1681
	 *   raw - Do not add identifier quotes to the table name
1682
	 * @return string Full database name
1683
	 */
1684
	public function tableName( $name, $format = 'quoted' ) {
1685
		global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1686
		# Skip the entire process when we have a string quoted on both ends.
1687
		# Note that we check the end so that we will still quote any use of
1688
		# use of `database`.table. But won't break things if someone wants
1689
		# to query a database table with a dot in the name.
1690
		if ( $this->isQuotedIdentifier( $name ) ) {
1691
			return $name;
1692
		}
1693
1694
		# Lets test for any bits of text that should never show up in a table
1695
		# name. Basically anything like JOIN or ON which are actually part of
1696
		# SQL queries, but may end up inside of the table value to combine
1697
		# sql. Such as how the API is doing.
1698
		# Note that we use a whitespace test rather than a \b test to avoid
1699
		# any remote case where a word like on may be inside of a table name
1700
		# surrounded by symbols which may be considered word breaks.
1701
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1702
			return $name;
1703
		}
1704
1705
		# Split database and table into proper variables.
1706
		# We reverse the explode so that database.table and table both output
1707
		# the correct table.
1708
		$dbDetails = explode( '.', $name, 3 );
1709
		if ( count( $dbDetails ) == 3 ) {
1710
			list( $database, $schema, $table ) = $dbDetails;
1711
			# We don't want any prefix added in this case
1712
			$prefix = '';
1713
		} elseif ( count( $dbDetails ) == 2 ) {
1714
			list( $database, $table ) = $dbDetails;
1715
			# We don't want any prefix added in this case
1716
			# In dbs that support it, $database may actually be the schema
1717
			# but that doesn't affect any of the functionality here
1718
			$prefix = '';
1719
			$schema = null;
1720
		} else {
1721
			list( $table ) = $dbDetails;
1722
			if ( $wgSharedDB !== null # We have a shared database
1723
				&& $this->mForeign == false # We're not working on a foreign database
1724
				&& !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1725
				&& in_array( $table, $wgSharedTables ) # A shared table is selected
1726
			) {
1727
				$database = $wgSharedDB;
1728
				$schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1729
				$prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1730
			} else {
1731
				$database = null;
1732
				$schema = $this->mSchema; # Default schema
1733
				$prefix = $this->mTablePrefix; # Default prefix
1734
			}
1735
		}
1736
1737
		# Quote $table and apply the prefix if not quoted.
1738
		# $tableName might be empty if this is called from Database::replaceVars()
1739
		$tableName = "{$prefix}{$table}";
1740
		if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1741
			$tableName = $this->addIdentifierQuotes( $tableName );
1742
		}
1743
1744
		# Quote $schema and merge it with the table name if needed
1745 View Code Duplication
		if ( strlen( $schema ) ) {
1746
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1747
				$schema = $this->addIdentifierQuotes( $schema );
1748
			}
1749
			$tableName = $schema . '.' . $tableName;
1750
		}
1751
1752
		# Quote $database and merge it with the table name if needed
1753 View Code Duplication
		if ( $database !== null ) {
1754
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1755
				$database = $this->addIdentifierQuotes( $database );
1756
			}
1757
			$tableName = $database . '.' . $tableName;
1758
		}
1759
1760
		return $tableName;
1761
	}
1762
1763
	/**
1764
	 * Fetch a number of table names into an array
1765
	 * This is handy when you need to construct SQL for joins
1766
	 *
1767
	 * Example:
1768
	 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1769
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1770
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1771
	 *
1772
	 * @return array
1773
	 */
1774 View Code Duplication
	public function tableNames() {
1775
		$inArray = func_get_args();
1776
		$retVal = [];
1777
1778
		foreach ( $inArray as $name ) {
1779
			$retVal[$name] = $this->tableName( $name );
1780
		}
1781
1782
		return $retVal;
1783
	}
1784
1785
	/**
1786
	 * Fetch a number of table names into an zero-indexed numerical array
1787
	 * This is handy when you need to construct SQL for joins
1788
	 *
1789
	 * Example:
1790
	 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1791
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1792
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1793
	 *
1794
	 * @return array
1795
	 */
1796 View Code Duplication
	public function tableNamesN() {
1797
		$inArray = func_get_args();
1798
		$retVal = [];
1799
1800
		foreach ( $inArray as $name ) {
1801
			$retVal[] = $this->tableName( $name );
1802
		}
1803
1804
		return $retVal;
1805
	}
1806
1807
	/**
1808
	 * Get an aliased table name
1809
	 * e.g. tableName AS newTableName
1810
	 *
1811
	 * @param string $name Table name, see tableName()
1812
	 * @param string|bool $alias Alias (optional)
1813
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1814
	 */
1815
	public function tableNameWithAlias( $name, $alias = false ) {
1816
		if ( !$alias || $alias == $name ) {
1817
			return $this->tableName( $name );
1818
		} else {
1819
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1815 can also be of type boolean; however, DatabaseBase::addIdentifierQuotes() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1820
		}
1821
	}
1822
1823
	/**
1824
	 * Gets an array of aliased table names
1825
	 *
1826
	 * @param array $tables Array( [alias] => table )
1827
	 * @return string[] See tableNameWithAlias()
1828
	 */
1829
	public function tableNamesWithAlias( $tables ) {
1830
		$retval = [];
1831
		foreach ( $tables as $alias => $table ) {
1832
			if ( is_numeric( $alias ) ) {
1833
				$alias = $table;
1834
			}
1835
			$retval[] = $this->tableNameWithAlias( $table, $alias );
1836
		}
1837
1838
		return $retval;
1839
	}
1840
1841
	/**
1842
	 * Get an aliased field name
1843
	 * e.g. fieldName AS newFieldName
1844
	 *
1845
	 * @param string $name Field name
1846
	 * @param string|bool $alias Alias (optional)
1847
	 * @return string SQL name for aliased field. Will not alias a field to its own name
1848
	 */
1849
	public function fieldNameWithAlias( $name, $alias = false ) {
1850
		if ( !$alias || (string)$alias === (string)$name ) {
1851
			return $name;
1852
		} else {
1853
			return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1849 can also be of type boolean; however, DatabaseBase::addIdentifierQuotes() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1854
		}
1855
	}
1856
1857
	/**
1858
	 * Gets an array of aliased field names
1859
	 *
1860
	 * @param array $fields Array( [alias] => field )
1861
	 * @return string[] See fieldNameWithAlias()
1862
	 */
1863
	public function fieldNamesWithAlias( $fields ) {
1864
		$retval = [];
1865
		foreach ( $fields as $alias => $field ) {
1866
			if ( is_numeric( $alias ) ) {
1867
				$alias = $field;
1868
			}
1869
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
1870
		}
1871
1872
		return $retval;
1873
	}
1874
1875
	/**
1876
	 * Get the aliased table name clause for a FROM clause
1877
	 * which might have a JOIN and/or USE INDEX clause
1878
	 *
1879
	 * @param array $tables ( [alias] => table )
1880
	 * @param array $use_index Same as for select()
1881
	 * @param array $join_conds Same as for select()
1882
	 * @return string
1883
	 */
1884
	protected function tableNamesWithUseIndexOrJOIN(
1885
		$tables, $use_index = [], $join_conds = []
1886
	) {
1887
		$ret = [];
1888
		$retJOIN = [];
1889
		$use_index = (array)$use_index;
1890
		$join_conds = (array)$join_conds;
1891
1892
		foreach ( $tables as $alias => $table ) {
1893
			if ( !is_string( $alias ) ) {
1894
				// No alias? Set it equal to the table name
1895
				$alias = $table;
1896
			}
1897
			// Is there a JOIN clause for this table?
1898
			if ( isset( $join_conds[$alias] ) ) {
1899
				list( $joinType, $conds ) = $join_conds[$alias];
1900
				$tableClause = $joinType;
1901
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1902
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1903
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1904
					if ( $use != '' ) {
1905
						$tableClause .= ' ' . $use;
1906
					}
1907
				}
1908
				$on = $this->makeList( (array)$conds, LIST_AND );
1909
				if ( $on != '' ) {
1910
					$tableClause .= ' ON (' . $on . ')';
1911
				}
1912
1913
				$retJOIN[] = $tableClause;
1914
			} elseif ( isset( $use_index[$alias] ) ) {
1915
				// Is there an INDEX clause for this table?
1916
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1917
				$tableClause .= ' ' . $this->useIndexClause(
1918
					implode( ',', (array)$use_index[$alias] )
1919
				);
1920
1921
				$ret[] = $tableClause;
1922
			} else {
1923
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1924
1925
				$ret[] = $tableClause;
1926
			}
1927
		}
1928
1929
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
1930
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
1931
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
1932
1933
		// Compile our final table clause
1934
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
1935
	}
1936
1937
	/**
1938
	 * Get the name of an index in a given table.
1939
	 *
1940
	 * @param string $index
1941
	 * @return string
1942
	 */
1943
	protected function indexName( $index ) {
1944
		// Backwards-compatibility hack
1945
		$renamed = [
1946
			'ar_usertext_timestamp' => 'usertext_timestamp',
1947
			'un_user_id' => 'user_id',
1948
			'un_user_ip' => 'user_ip',
1949
		];
1950
1951
		if ( isset( $renamed[$index] ) ) {
1952
			return $renamed[$index];
1953
		} else {
1954
			return $index;
1955
		}
1956
	}
1957
1958
	public function addQuotes( $s ) {
1959
		if ( $s instanceof Blob ) {
1960
			$s = $s->fetch();
1961
		}
1962
		if ( $s === null ) {
1963
			return 'NULL';
1964
		} else {
1965
			# This will also quote numeric values. This should be harmless,
1966
			# and protects against weird problems that occur when they really
1967
			# _are_ strings such as article titles and string->number->string
1968
			# conversion is not 1:1.
1969
			return "'" . $this->strencode( $s ) . "'";
1970
		}
1971
	}
1972
1973
	/**
1974
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
1975
	 * MySQL uses `backticks` while basically everything else uses double quotes.
1976
	 * Since MySQL is the odd one out here the double quotes are our generic
1977
	 * and we implement backticks in DatabaseMysql.
1978
	 *
1979
	 * @param string $s
1980
	 * @return string
1981
	 */
1982
	public function addIdentifierQuotes( $s ) {
1983
		return '"' . str_replace( '"', '""', $s ) . '"';
1984
	}
1985
1986
	/**
1987
	 * Returns if the given identifier looks quoted or not according to
1988
	 * the database convention for quoting identifiers .
1989
	 *
1990
	 * @note Do not use this to determine if untrusted input is safe.
1991
	 *   A malicious user can trick this function.
1992
	 * @param string $name
1993
	 * @return bool
1994
	 */
1995
	public function isQuotedIdentifier( $name ) {
1996
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
1997
	}
1998
1999
	/**
2000
	 * @param string $s
2001
	 * @return string
2002
	 */
2003
	protected function escapeLikeInternal( $s ) {
2004
		return addcslashes( $s, '\%_' );
2005
	}
2006
2007
	public function buildLike() {
2008
		$params = func_get_args();
2009
2010
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2011
			$params = $params[0];
2012
		}
2013
2014
		$s = '';
2015
2016
		foreach ( $params as $value ) {
2017
			if ( $value instanceof LikeMatch ) {
2018
				$s .= $value->toString();
2019
			} else {
2020
				$s .= $this->escapeLikeInternal( $value );
2021
			}
2022
		}
2023
2024
		return " LIKE {$this->addQuotes( $s )} ";
2025
	}
2026
2027
	public function anyChar() {
2028
		return new LikeMatch( '_' );
2029
	}
2030
2031
	public function anyString() {
2032
		return new LikeMatch( '%' );
2033
	}
2034
2035
	public function nextSequenceValue( $seqName ) {
2036
		return null;
2037
	}
2038
2039
	/**
2040
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2041
	 * is only needed because a) MySQL must be as efficient as possible due to
2042
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2043
	 * which index to pick. Anyway, other databases might have different
2044
	 * indexes on a given table. So don't bother overriding this unless you're
2045
	 * MySQL.
2046
	 * @param string $index
2047
	 * @return string
2048
	 */
2049
	public function useIndexClause( $index ) {
2050
		return '';
2051
	}
2052
2053
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2054
		$quotedTable = $this->tableName( $table );
2055
2056
		if ( count( $rows ) == 0 ) {
2057
			return;
2058
		}
2059
2060
		# Single row case
2061
		if ( !is_array( reset( $rows ) ) ) {
2062
			$rows = [ $rows ];
2063
		}
2064
2065
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2066
		foreach ( $rows as $row ) {
2067
			# Delete rows which collide
2068
			if ( $uniqueIndexes ) {
2069
				$sql = "DELETE FROM $quotedTable WHERE ";
2070
				$first = true;
2071
				foreach ( $uniqueIndexes as $index ) {
2072
					if ( $first ) {
2073
						$first = false;
2074
						$sql .= '( ';
2075
					} else {
2076
						$sql .= ' ) OR ( ';
2077
					}
2078
					if ( is_array( $index ) ) {
2079
						$first2 = true;
2080
						foreach ( $index as $col ) {
2081
							if ( $first2 ) {
2082
								$first2 = false;
2083
							} else {
2084
								$sql .= ' AND ';
2085
							}
2086
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2087
						}
2088
					} else {
2089
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2090
					}
2091
				}
2092
				$sql .= ' )';
2093
				$this->query( $sql, $fname );
2094
			}
2095
2096
			# Now insert the row
2097
			$this->insert( $table, $row, $fname );
2098
		}
2099
	}
2100
2101
	/**
2102
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2103
	 * statement.
2104
	 *
2105
	 * @param string $table Table name
2106
	 * @param array|string $rows Row(s) to insert
2107
	 * @param string $fname Caller function name
2108
	 *
2109
	 * @return ResultWrapper
2110
	 */
2111
	protected function nativeReplace( $table, $rows, $fname ) {
2112
		$table = $this->tableName( $table );
2113
2114
		# Single row case
2115
		if ( !is_array( reset( $rows ) ) ) {
2116
			$rows = [ $rows ];
2117
		}
2118
2119
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2120
		$first = true;
2121
2122 View Code Duplication
		foreach ( $rows as $row ) {
2123
			if ( $first ) {
2124
				$first = false;
2125
			} else {
2126
				$sql .= ',';
2127
			}
2128
2129
			$sql .= '(' . $this->makeList( $row ) . ')';
2130
		}
2131
2132
		return $this->query( $sql, $fname );
2133
	}
2134
2135
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2136
		$fname = __METHOD__
2137
	) {
2138
		if ( !count( $rows ) ) {
2139
			return true; // nothing to do
2140
		}
2141
2142
		if ( !is_array( reset( $rows ) ) ) {
2143
			$rows = [ $rows ];
2144
		}
2145
2146
		if ( count( $uniqueIndexes ) ) {
2147
			$clauses = []; // list WHERE clauses that each identify a single row
2148
			foreach ( $rows as $row ) {
2149
				foreach ( $uniqueIndexes as $index ) {
2150
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2151
					$rowKey = []; // unique key to this row
2152
					foreach ( $index as $column ) {
2153
						$rowKey[$column] = $row[$column];
2154
					}
2155
					$clauses[] = $this->makeList( $rowKey, LIST_AND );
2156
				}
2157
			}
2158
			$where = [ $this->makeList( $clauses, LIST_OR ) ];
2159
		} else {
2160
			$where = false;
2161
		}
2162
2163
		$useTrx = !$this->mTrxLevel;
2164
		if ( $useTrx ) {
2165
			$this->begin( $fname );
2166
		}
2167
		try {
2168
			# Update any existing conflicting row(s)
2169
			if ( $where !== false ) {
2170
				$ok = $this->update( $table, $set, $where, $fname );
2171
			} else {
2172
				$ok = true;
2173
			}
2174
			# Now insert any non-conflicting row(s)
2175
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2176
		} catch ( Exception $e ) {
2177
			if ( $useTrx ) {
2178
				$this->rollback( $fname );
2179
			}
2180
			throw $e;
2181
		}
2182
		if ( $useTrx ) {
2183
			$this->commit( $fname );
2184
		}
2185
2186
		return $ok;
2187
	}
2188
2189 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2190
		$fname = __METHOD__
2191
	) {
2192
		if ( !$conds ) {
2193
			throw new DBUnexpectedError( $this,
2194
				'DatabaseBase::deleteJoin() called with empty $conds' );
2195
		}
2196
2197
		$delTable = $this->tableName( $delTable );
2198
		$joinTable = $this->tableName( $joinTable );
2199
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2200
		if ( $conds != '*' ) {
2201
			$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2202
		}
2203
		$sql .= ')';
2204
2205
		$this->query( $sql, $fname );
2206
	}
2207
2208
	/**
2209
	 * Returns the size of a text field, or -1 for "unlimited"
2210
	 *
2211
	 * @param string $table
2212
	 * @param string $field
2213
	 * @return int
2214
	 */
2215
	public function textFieldSize( $table, $field ) {
2216
		$table = $this->tableName( $table );
2217
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2218
		$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2219
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, 'DatabaseBase::textFieldSize') on line 2218 can also be of type boolean; however, IDatabase::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2220
2221
		$m = [];
2222
2223
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2224
			$size = $m[1];
2225
		} else {
2226
			$size = -1;
2227
		}
2228
2229
		return $size;
2230
	}
2231
2232
	/**
2233
	 * A string to insert into queries to show that they're low-priority, like
2234
	 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2235
	 * string and nothing bad should happen.
2236
	 *
2237
	 * @return string Returns the text of the low priority option if it is
2238
	 *   supported, or a blank string otherwise
2239
	 */
2240
	public function lowPriorityOption() {
2241
		return '';
2242
	}
2243
2244
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2245
		if ( !$conds ) {
2246
			throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2247
		}
2248
2249
		$table = $this->tableName( $table );
2250
		$sql = "DELETE FROM $table";
2251
2252 View Code Duplication
		if ( $conds != '*' ) {
2253
			if ( is_array( $conds ) ) {
2254
				$conds = $this->makeList( $conds, LIST_AND );
2255
			}
2256
			$sql .= ' WHERE ' . $conds;
2257
		}
2258
2259
		return $this->query( $sql, $fname );
2260
	}
2261
2262
	public function insertSelect( $destTable, $srcTable, $varMap, $conds,
2263
		$fname = __METHOD__,
2264
		$insertOptions = [], $selectOptions = []
2265
	) {
2266
		$destTable = $this->tableName( $destTable );
2267
2268
		if ( !is_array( $insertOptions ) ) {
2269
			$insertOptions = [ $insertOptions ];
2270
		}
2271
2272
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2273
2274
		if ( !is_array( $selectOptions ) ) {
2275
			$selectOptions = [ $selectOptions ];
2276
		}
2277
2278
		list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
2279
2280 View Code Duplication
		if ( is_array( $srcTable ) ) {
2281
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2282
		} else {
2283
			$srcTable = $this->tableName( $srcTable );
2284
		}
2285
2286
		$sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2287
			" SELECT $startOpts " . implode( ',', $varMap ) .
2288
			" FROM $srcTable $useIndex ";
2289
2290 View Code Duplication
		if ( $conds != '*' ) {
2291
			if ( is_array( $conds ) ) {
2292
				$conds = $this->makeList( $conds, LIST_AND );
2293
			}
2294
			$sql .= " WHERE $conds";
2295
		}
2296
2297
		$sql .= " $tailOpts";
2298
2299
		return $this->query( $sql, $fname );
2300
	}
2301
2302
	/**
2303
	 * Construct a LIMIT query with optional offset. This is used for query
2304
	 * pages. The SQL should be adjusted so that only the first $limit rows
2305
	 * are returned. If $offset is provided as well, then the first $offset
2306
	 * rows should be discarded, and the next $limit rows should be returned.
2307
	 * If the result of the query is not ordered, then the rows to be returned
2308
	 * are theoretically arbitrary.
2309
	 *
2310
	 * $sql is expected to be a SELECT, if that makes a difference.
2311
	 *
2312
	 * The version provided by default works in MySQL and SQLite. It will very
2313
	 * likely need to be overridden for most other DBMSes.
2314
	 *
2315
	 * @param string $sql SQL query we will append the limit too
2316
	 * @param int $limit The SQL limit
2317
	 * @param int|bool $offset The SQL offset (default false)
2318
	 * @throws DBUnexpectedError
2319
	 * @return string
2320
	 */
2321
	public function limitResult( $sql, $limit, $offset = false ) {
2322
		if ( !is_numeric( $limit ) ) {
2323
			throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2324
		}
2325
2326
		return "$sql LIMIT "
2327
			. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2328
			. "{$limit} ";
2329
	}
2330
2331
	public function unionSupportsOrderAndLimit() {
2332
		return true; // True for almost every DB supported
2333
	}
2334
2335
	public function unionQueries( $sqls, $all ) {
2336
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2337
2338
		return '(' . implode( $glue, $sqls ) . ')';
2339
	}
2340
2341
	public function conditional( $cond, $trueVal, $falseVal ) {
2342
		if ( is_array( $cond ) ) {
2343
			$cond = $this->makeList( $cond, LIST_AND );
2344
		}
2345
2346
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2347
	}
2348
2349
	public function strreplace( $orig, $old, $new ) {
2350
		return "REPLACE({$orig}, {$old}, {$new})";
2351
	}
2352
2353
	public function getServerUptime() {
2354
		return 0;
2355
	}
2356
2357
	public function wasDeadlock() {
2358
		return false;
2359
	}
2360
2361
	public function wasLockTimeout() {
2362
		return false;
2363
	}
2364
2365
	public function wasErrorReissuable() {
2366
		return false;
2367
	}
2368
2369
	public function wasReadOnlyError() {
2370
		return false;
2371
	}
2372
2373
	/**
2374
	 * Determines if the given query error was a connection drop
2375
	 * STUB
2376
	 *
2377
	 * @param integer|string $errno
2378
	 * @return bool
2379
	 */
2380
	public function wasConnectionError( $errno ) {
2381
		return false;
2382
	}
2383
2384
	/**
2385
	 * Perform a deadlock-prone transaction.
2386
	 *
2387
	 * This function invokes a callback function to perform a set of write
2388
	 * queries. If a deadlock occurs during the processing, the transaction
2389
	 * will be rolled back and the callback function will be called again.
2390
	 *
2391
	 * Avoid using this method outside of Job or Maintenance classes.
2392
	 *
2393
	 * Usage:
2394
	 *   $dbw->deadlockLoop( callback, ... );
2395
	 *
2396
	 * Extra arguments are passed through to the specified callback function.
2397
	 * This method requires that no transactions are already active to avoid
2398
	 * causing premature commits or exceptions.
2399
	 *
2400
	 * Returns whatever the callback function returned on its successful,
2401
	 * iteration, or false on error, for example if the retry limit was
2402
	 * reached.
2403
	 *
2404
	 * @return mixed
2405
	 * @throws DBUnexpectedError
2406
	 * @throws Exception
2407
	 */
2408
	public function deadlockLoop() {
2409
		$args = func_get_args();
2410
		$function = array_shift( $args );
2411
		$tries = self::DEADLOCK_TRIES;
2412
2413
		$this->begin( __METHOD__ );
2414
2415
		$retVal = null;
2416
		/** @var Exception $e */
2417
		$e = null;
2418
		do {
2419
			try {
2420
				$retVal = call_user_func_array( $function, $args );
2421
				break;
2422
			} catch ( DBQueryError $e ) {
2423
				if ( $this->wasDeadlock() ) {
2424
					// Retry after a randomized delay
2425
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2426
				} else {
2427
					// Throw the error back up
2428
					throw $e;
2429
				}
2430
			}
2431
		} while ( --$tries > 0 );
2432
2433
		if ( $tries <= 0 ) {
2434
			// Too many deadlocks; give up
2435
			$this->rollback( __METHOD__ );
2436
			throw $e;
2437
		} else {
2438
			$this->commit( __METHOD__ );
2439
2440
			return $retVal;
2441
		}
2442
	}
2443
2444
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2445
		# Real waits are implemented in the subclass.
2446
		return 0;
2447
	}
2448
2449
	public function getSlavePos() {
2450
		# Stub
2451
		return false;
2452
	}
2453
2454
	public function getMasterPos() {
2455
		# Stub
2456
		return false;
2457
	}
2458
2459
	public function serverIsReadOnly() {
2460
		return false;
2461
	}
2462
2463
	final public function onTransactionResolution( callable $callback ) {
2464
		if ( !$this->mTrxLevel ) {
2465
			throw new DBUnexpectedError( $this, "No transaction is active." );
2466
		}
2467
		$this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
2468
	}
2469
2470
	final public function onTransactionIdle( callable $callback ) {
2471
		$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2472
		if ( !$this->mTrxLevel ) {
2473
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2474
		}
2475
	}
2476
2477
	final public function onTransactionPreCommitOrIdle( callable $callback ) {
2478
		if ( $this->mTrxLevel ) {
2479
			$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2480
		} else {
2481
			// If no transaction is active, then make one for this callback
2482
			$this->begin( __METHOD__ );
2483
			try {
2484
				call_user_func( $callback );
2485
				$this->commit( __METHOD__ );
2486
			} catch ( Exception $e ) {
2487
				$this->rollback( __METHOD__ );
2488
				throw $e;
2489
			}
2490
		}
2491
	}
2492
2493
	/**
2494
	 * Whether to disable running of post-commit callbacks
2495
	 *
2496
	 * This method should not be used outside of Database/LoadBalancer
2497
	 *
2498
	 * @param bool $suppress
2499
	 * @since 1.28
2500
	 */
2501
	final public function setPostCommitCallbackSupression( $suppress ) {
2502
		$this->suppressPostCommitCallbacks = $suppress;
2503
	}
2504
2505
	/**
2506
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2507
	 *
2508
	 * This method should not be used outside of Database/LoadBalancer
2509
	 *
2510
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2511
	 * @since 1.20
2512
	 */
2513
	public function runOnTransactionIdleCallbacks( $trigger ) {
2514
		if ( $this->suppressPostCommitCallbacks ) {
2515
			return;
2516
		}
2517
2518
		$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2519
2520
		$e = $ePrior = null; // last exception
2521
		do { // callbacks may add callbacks :)
2522
			$callbacks = array_merge(
2523
				$this->mTrxIdleCallbacks,
2524
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2525
			);
2526
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2527
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2528
			foreach ( $callbacks as $callback ) {
2529
				try {
2530
					list( $phpCallback ) = $callback;
2531
					$this->clearFlag( DBO_TRX ); // make each query its own transaction
2532
					call_user_func_array( $phpCallback, [ $trigger ] );
2533
					if ( $autoTrx ) {
2534
						$this->setFlag( DBO_TRX ); // restore automatic begin()
2535
					} else {
2536
						$this->clearFlag( DBO_TRX ); // restore auto-commit
2537
					}
2538
				} catch ( Exception $e ) {
2539
					if ( $ePrior ) {
2540
						MWExceptionHandler::logException( $ePrior );
2541
					}
2542
					$ePrior = $e;
2543
					// Some callbacks may use startAtomic/endAtomic, so make sure
2544
					// their transactions are ended so other callbacks don't fail
2545
					if ( $this->trxLevel() ) {
2546
						$this->rollback( __METHOD__ );
2547
					}
2548
				}
2549
			}
2550
		} while ( count( $this->mTrxIdleCallbacks ) );
2551
2552
		if ( $e instanceof Exception ) {
2553
			throw $e; // re-throw any last exception
2554
		}
2555
	}
2556
2557
	/**
2558
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2559
	 *
2560
	 * This method should not be used outside of Database/LoadBalancer
2561
	 *
2562
	 * @since 1.22
2563
	 */
2564
	public function runOnTransactionPreCommitCallbacks() {
2565
		$e = $ePrior = null; // last exception
2566
		do { // callbacks may add callbacks :)
2567
			$callbacks = $this->mTrxPreCommitCallbacks;
2568
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2569
			foreach ( $callbacks as $callback ) {
2570
				try {
2571
					list( $phpCallback ) = $callback;
2572
					call_user_func( $phpCallback );
2573
				} catch ( Exception $e ) {
2574
					if ( $ePrior ) {
2575
						MWExceptionHandler::logException( $ePrior );
2576
					}
2577
					$ePrior = $e;
2578
				}
2579
			}
2580
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2581
2582
		if ( $e instanceof Exception ) {
2583
			throw $e; // re-throw any last exception
2584
		}
2585
	}
2586
2587
	final public function startAtomic( $fname = __METHOD__ ) {
2588
		if ( !$this->mTrxLevel ) {
2589
			$this->begin( $fname );
2590
			$this->mTrxAutomatic = true;
2591
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2592
			// in all changes being in one transaction to keep requests transactional.
2593
			if ( !$this->getFlag( DBO_TRX ) ) {
2594
				$this->mTrxAutomaticAtomic = true;
2595
			}
2596
		}
2597
2598
		$this->mTrxAtomicLevels[] = $fname;
2599
	}
2600
2601
	final public function endAtomic( $fname = __METHOD__ ) {
2602
		if ( !$this->mTrxLevel ) {
2603
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2604
		}
2605
		if ( !$this->mTrxAtomicLevels ||
2606
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2607
		) {
2608
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2609
		}
2610
2611
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2612
			$this->commit( $fname, 'flush' );
2613
		}
2614
	}
2615
2616
	final public function doAtomicSection( $fname, callable $callback ) {
2617
		$this->startAtomic( $fname );
2618
		try {
2619
			call_user_func_array( $callback, [ $this, $fname ] );
2620
		} catch ( Exception $e ) {
2621
			$this->rollback( $fname );
2622
			throw $e;
2623
		}
2624
		$this->endAtomic( $fname );
2625
	}
2626
2627
	final public function begin( $fname = __METHOD__ ) {
2628
		if ( $this->mTrxLevel ) { // implicit commit
2629
			if ( $this->mTrxAtomicLevels ) {
2630
				// If the current transaction was an automatic atomic one, then we definitely have
2631
				// a problem. Same if there is any unclosed atomic level.
2632
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2633
				throw new DBUnexpectedError(
2634
					$this,
2635
					"Got explicit BEGIN from $fname while atomic section(s) $levels are open."
2636
				);
2637
			} elseif ( !$this->mTrxAutomatic ) {
2638
				// We want to warn about inadvertently nested begin/commit pairs, but not about
2639
				// auto-committing implicit transactions that were started by query() via DBO_TRX
2640
				throw new DBUnexpectedError(
2641
					$this,
2642
					"$fname: Transaction already in progress (from {$this->mTrxFname}), " .
2643
						" performing implicit commit!"
2644
				);
2645
			} else {
2646
				// The transaction was automatic and has done write operations
2647
				if ( $this->mTrxDoneWrites ) {
2648
					wfLogDBError( "$fname: Automatic transaction with writes in progress" .
2649
						" (from {$this->mTrxFname}), performing implicit commit!\n"
2650
					);
2651
				}
2652
			}
2653
2654
			$this->runOnTransactionPreCommitCallbacks();
2655
			$writeTime = $this->pendingWriteQueryDuration();
2656
			$this->doCommit( $fname );
2657 View Code Duplication
			if ( $this->mTrxDoneWrites ) {
2658
				$this->mDoneWrites = microtime( true );
0 ignored issues
show
Documentation Bug introduced by
The property $mDoneWrites was declared of type boolean, but microtime(true) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2659
				$this->getTransactionProfiler()->transactionWritingOut(
2660
					$this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
0 ignored issues
show
Security Bug introduced by
It seems like $writeTime defined by $this->pendingWriteQueryDuration() on line 2655 can also be of type false; however, TransactionProfiler::transactionWritingOut() does only seem to accept double, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2661
			}
2662
2663
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2664
		}
2665
2666
		// Avoid fatals if close() was called
2667
		$this->assertOpen();
2668
2669
		$this->doBegin( $fname );
2670
		$this->mTrxTimestamp = microtime( true );
2671
		$this->mTrxFname = $fname;
2672
		$this->mTrxDoneWrites = false;
2673
		$this->mTrxAutomatic = false;
2674
		$this->mTrxAutomaticAtomic = false;
2675
		$this->mTrxAtomicLevels = [];
2676
		$this->mTrxShortId = wfRandomString( 12 );
2677
		$this->mTrxWriteDuration = 0.0;
2678
		$this->mTrxWriteCallers = [];
2679
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2680
		// Get an estimate of the slave lag before then, treating estimate staleness
2681
		// as lag itself just to be safe
2682
		$status = $this->getApproximateLagStatus();
2683
		$this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
0 ignored issues
show
Documentation Bug introduced by
It seems like $status['lag'] + (microt...ue) - $status['since']) can also be of type integer. However, the property $mTrxSlaveLag is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2684
	}
2685
2686
	/**
2687
	 * Issues the BEGIN command to the database server.
2688
	 *
2689
	 * @see DatabaseBase::begin()
2690
	 * @param string $fname
2691
	 */
2692
	protected function doBegin( $fname ) {
2693
		$this->query( 'BEGIN', $fname );
2694
		$this->mTrxLevel = 1;
2695
	}
2696
2697
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2698
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2699
			// There are still atomic sections open. This cannot be ignored
2700
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2701
			throw new DBUnexpectedError(
2702
				$this,
2703
				"Got COMMIT while atomic sections $levels are still open"
2704
			);
2705
		}
2706
2707
		if ( $flush === 'flush' ) {
2708
			if ( !$this->mTrxLevel ) {
2709
				return; // nothing to do
2710
			} elseif ( !$this->mTrxAutomatic ) {
2711
				throw new DBUnexpectedError(
2712
					$this,
2713
					"$fname: Flushing an explicit transaction, getting out of sync!"
2714
				);
2715
			}
2716
		} else {
2717
			if ( !$this->mTrxLevel ) {
2718
				wfWarn( "$fname: No transaction to commit, something got out of sync!" );
2719
				return; // nothing to do
2720
			} elseif ( $this->mTrxAutomatic ) {
2721
				wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" );
2722
			}
2723
		}
2724
2725
		// Avoid fatals if close() was called
2726
		$this->assertOpen();
2727
2728
		$this->runOnTransactionPreCommitCallbacks();
2729
		$writeTime = $this->pendingWriteQueryDuration();
2730
		$this->doCommit( $fname );
2731 View Code Duplication
		if ( $this->mTrxDoneWrites ) {
2732
			$this->mDoneWrites = microtime( true );
0 ignored issues
show
Documentation Bug introduced by
The property $mDoneWrites was declared of type boolean, but microtime(true) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2733
			$this->getTransactionProfiler()->transactionWritingOut(
2734
				$this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
0 ignored issues
show
Security Bug introduced by
It seems like $writeTime defined by $this->pendingWriteQueryDuration() on line 2729 can also be of type false; however, TransactionProfiler::transactionWritingOut() does only seem to accept double, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2735
		}
2736
2737
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2738
	}
2739
2740
	/**
2741
	 * Issues the COMMIT command to the database server.
2742
	 *
2743
	 * @see DatabaseBase::commit()
2744
	 * @param string $fname
2745
	 */
2746
	protected function doCommit( $fname ) {
2747
		if ( $this->mTrxLevel ) {
2748
			$this->query( 'COMMIT', $fname );
2749
			$this->mTrxLevel = 0;
2750
		}
2751
	}
2752
2753
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
2754
		if ( $flush !== 'flush' ) {
2755
			if ( !$this->mTrxLevel ) {
2756
				wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
2757
				return; // nothing to do
2758
			}
2759
		} else {
2760
			if ( !$this->mTrxLevel ) {
2761
				return; // nothing to do
2762
			}
2763
		}
2764
2765
		// Avoid fatals if close() was called
2766
		$this->assertOpen();
2767
2768
		$this->doRollback( $fname );
2769
		$this->mTrxAtomicLevels = [];
2770
		if ( $this->mTrxDoneWrites ) {
2771
			$this->getTransactionProfiler()->transactionWritingOut(
2772
				$this->mServer, $this->mDBname, $this->mTrxShortId );
2773
		}
2774
2775
		$this->mTrxIdleCallbacks = []; // clear
2776
		$this->mTrxPreCommitCallbacks = []; // clear
2777
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2778
	}
2779
2780
	/**
2781
	 * Issues the ROLLBACK command to the database server.
2782
	 *
2783
	 * @see DatabaseBase::rollback()
2784
	 * @param string $fname
2785
	 */
2786
	protected function doRollback( $fname ) {
2787
		if ( $this->mTrxLevel ) {
2788
			$this->query( 'ROLLBACK', $fname, true );
2789
			$this->mTrxLevel = 0;
2790
		}
2791
	}
2792
2793
	/**
2794
	 * Creates a new table with structure copied from existing table
2795
	 * Note that unlike most database abstraction functions, this function does not
2796
	 * automatically append database prefix, because it works at a lower
2797
	 * abstraction level.
2798
	 * The table names passed to this function shall not be quoted (this
2799
	 * function calls addIdentifierQuotes when needed).
2800
	 *
2801
	 * @param string $oldName Name of table whose structure should be copied
2802
	 * @param string $newName Name of table to be created
2803
	 * @param bool $temporary Whether the new table should be temporary
2804
	 * @param string $fname Calling function name
2805
	 * @throws MWException
2806
	 * @return bool True if operation was successful
2807
	 */
2808
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
2809
		$fname = __METHOD__
2810
	) {
2811
		throw new MWException(
2812
			'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
2813
	}
2814
2815
	function listTables( $prefix = null, $fname = __METHOD__ ) {
2816
		throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
2817
	}
2818
2819
	/**
2820
	 * Reset the views process cache set by listViews()
2821
	 * @since 1.22
2822
	 */
2823
	final public function clearViewsCache() {
2824
		$this->allViews = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,string> of property $allViews.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2825
	}
2826
2827
	/**
2828
	 * Lists all the VIEWs in the database
2829
	 *
2830
	 * For caching purposes the list of all views should be stored in
2831
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
2832
	 *
2833
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
2834
	 * @param string $fname Name of calling function
2835
	 * @throws MWException
2836
	 * @return array
2837
	 * @since 1.22
2838
	 */
2839
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
2840
		throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
2841
	}
2842
2843
	/**
2844
	 * Differentiates between a TABLE and a VIEW
2845
	 *
2846
	 * @param string $name Name of the database-structure to test.
2847
	 * @throws MWException
2848
	 * @return bool
2849
	 * @since 1.22
2850
	 */
2851
	public function isView( $name ) {
2852
		throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
2853
	}
2854
2855
	public function timestamp( $ts = 0 ) {
2856
		return wfTimestamp( TS_MW, $ts );
2857
	}
2858
2859
	public function timestampOrNull( $ts = null ) {
2860
		if ( is_null( $ts ) ) {
2861
			return null;
2862
		} else {
2863
			return $this->timestamp( $ts );
2864
		}
2865
	}
2866
2867
	/**
2868
	 * Take the result from a query, and wrap it in a ResultWrapper if
2869
	 * necessary. Boolean values are passed through as is, to indicate success
2870
	 * of write queries or failure.
2871
	 *
2872
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
2873
	 * resource, and it was necessary to call this function to convert it to
2874
	 * a wrapper. Nowadays, raw database objects are never exposed to external
2875
	 * callers, so this is unnecessary in external code.
2876
	 *
2877
	 * @param bool|ResultWrapper|resource|object $result
2878
	 * @return bool|ResultWrapper
2879
	 */
2880
	protected function resultObject( $result ) {
2881
		if ( !$result ) {
2882
			return false;
2883
		} elseif ( $result instanceof ResultWrapper ) {
2884
			return $result;
2885
		} elseif ( $result === true ) {
2886
			// Successful write query
2887
			return $result;
2888
		} else {
2889
			return new ResultWrapper( $this, $result );
2890
		}
2891
	}
2892
2893
	public function ping() {
2894
		# Stub. Not essential to override.
2895
		return true;
2896
	}
2897
2898
	public function getSessionLagStatus() {
2899
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
2900
	}
2901
2902
	/**
2903
	 * Get the slave lag when the current transaction started
2904
	 *
2905
	 * This is useful when transactions might use snapshot isolation
2906
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
2907
	 * is this lag plus transaction duration. If they don't, it is still
2908
	 * safe to be pessimistic. This returns null if there is no transaction.
2909
	 *
2910
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
2911
	 * @since 1.27
2912
	 */
2913
	public function getTransactionLagStatus() {
2914
		return $this->mTrxLevel
2915
			? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
2916
			: null;
2917
	}
2918
2919
	/**
2920
	 * Get a slave lag estimate for this server
2921
	 *
2922
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
2923
	 * @since 1.27
2924
	 */
2925
	public function getApproximateLagStatus() {
2926
		return [
2927
			'lag'   => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
2928
			'since' => microtime( true )
2929
		];
2930
	}
2931
2932
	/**
2933
	 * Merge the result of getSessionLagStatus() for several DBs
2934
	 * using the most pessimistic values to estimate the lag of
2935
	 * any data derived from them in combination
2936
	 *
2937
	 * This is information is useful for caching modules
2938
	 *
2939
	 * @see WANObjectCache::set()
2940
	 * @see WANObjectCache::getWithSetCallback()
2941
	 *
2942
	 * @param IDatabase $db1
2943
	 * @param IDatabase ...
2944
	 * @return array Map of values:
2945
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
2946
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
2947
	 *   - pending: whether any of the DBs have uncommitted changes
2948
	 * @since 1.27
2949
	 */
2950
	public static function getCacheSetOptions( IDatabase $db1 ) {
2951
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
2952
		foreach ( func_get_args() as $db ) {
2953
			/** @var IDatabase $db */
2954
			$status = $db->getSessionLagStatus();
2955
			if ( $status['lag'] === false ) {
2956
				$res['lag'] = false;
2957
			} elseif ( $res['lag'] !== false ) {
2958
				$res['lag'] = max( $res['lag'], $status['lag'] );
2959
			}
2960
			$res['since'] = min( $res['since'], $status['since'] );
2961
			$res['pending'] = $res['pending'] ?: $db->writesPending();
2962
		}
2963
2964
		return $res;
2965
	}
2966
2967
	public function getLag() {
2968
		return 0;
2969
	}
2970
2971
	function maxListLen() {
2972
		return 0;
2973
	}
2974
2975
	public function encodeBlob( $b ) {
2976
		return $b;
2977
	}
2978
2979
	public function decodeBlob( $b ) {
2980
		if ( $b instanceof Blob ) {
2981
			$b = $b->fetch();
2982
		}
2983
		return $b;
2984
	}
2985
2986
	public function setSessionOptions( array $options ) {
2987
	}
2988
2989
	/**
2990
	 * Read and execute SQL commands from a file.
2991
	 *
2992
	 * Returns true on success, error string or exception on failure (depending
2993
	 * on object's error ignore settings).
2994
	 *
2995
	 * @param string $filename File name to open
2996
	 * @param bool|callable $lineCallback Optional function called before reading each line
2997
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
2998
	 * @param bool|string $fname Calling function name or false if name should be
2999
	 *   generated dynamically using $filename
3000
	 * @param bool|callable $inputCallback Optional function called for each
3001
	 *   complete line sent
3002
	 * @throws Exception|MWException
3003
	 * @return bool|string
3004
	 */
3005
	public function sourceFile(
3006
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3007
	) {
3008
		MediaWiki\suppressWarnings();
3009
		$fp = fopen( $filename, 'r' );
3010
		MediaWiki\restoreWarnings();
3011
3012
		if ( false === $fp ) {
3013
			throw new MWException( "Could not open \"{$filename}\".\n" );
3014
		}
3015
3016
		if ( !$fname ) {
3017
			$fname = __METHOD__ . "( $filename )";
3018
		}
3019
3020
		try {
3021
			$error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
0 ignored issues
show
Bug introduced by
It seems like $fname defined by parameter $fname on line 3006 can also be of type boolean; however, DatabaseBase::sourceStream() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
3022
		} catch ( Exception $e ) {
3023
			fclose( $fp );
3024
			throw $e;
3025
		}
3026
3027
		fclose( $fp );
3028
3029
		return $error;
3030
	}
3031
3032
	/**
3033
	 * Get the full path of a patch file. Originally based on archive()
3034
	 * from updaters.inc. Keep in mind this always returns a patch, as
3035
	 * it fails back to MySQL if no DB-specific patch can be found
3036
	 *
3037
	 * @param string $patch The name of the patch, like patch-something.sql
3038
	 * @return string Full path to patch file
3039
	 */
3040 View Code Duplication
	public function patchPath( $patch ) {
3041
		global $IP;
3042
3043
		$dbType = $this->getType();
3044
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
3045
			return "$IP/maintenance/$dbType/archives/$patch";
3046
		} else {
3047
			return "$IP/maintenance/archives/$patch";
3048
		}
3049
	}
3050
3051
	public function setSchemaVars( $vars ) {
3052
		$this->mSchemaVars = $vars;
0 ignored issues
show
Documentation Bug introduced by
It seems like $vars can also be of type array. However, the property $mSchemaVars is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
3053
	}
3054
3055
	/**
3056
	 * Read and execute commands from an open file handle.
3057
	 *
3058
	 * Returns true on success, error string or exception on failure (depending
3059
	 * on object's error ignore settings).
3060
	 *
3061
	 * @param resource $fp File handle
3062
	 * @param bool|callable $lineCallback Optional function called before reading each query
3063
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3064
	 * @param string $fname Calling function name
3065
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3066
	 * @return bool|string
3067
	 */
3068
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3069
		$fname = __METHOD__, $inputCallback = false
3070
	) {
3071
		$cmd = '';
3072
3073
		while ( !feof( $fp ) ) {
3074
			if ( $lineCallback ) {
3075
				call_user_func( $lineCallback );
3076
			}
3077
3078
			$line = trim( fgets( $fp ) );
3079
3080
			if ( $line == '' ) {
3081
				continue;
3082
			}
3083
3084
			if ( '-' == $line[0] && '-' == $line[1] ) {
3085
				continue;
3086
			}
3087
3088
			if ( $cmd != '' ) {
3089
				$cmd .= ' ';
3090
			}
3091
3092
			$done = $this->streamStatementEnd( $cmd, $line );
3093
3094
			$cmd .= "$line\n";
3095
3096
			if ( $done || feof( $fp ) ) {
3097
				$cmd = $this->replaceVars( $cmd );
3098
3099
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3100
					$res = $this->query( $cmd, $fname );
3101
3102
					if ( $resultCallback ) {
3103
						call_user_func( $resultCallback, $res, $this );
3104
					}
3105
3106
					if ( false === $res ) {
3107
						$err = $this->lastError();
3108
3109
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3110
					}
3111
				}
3112
				$cmd = '';
3113
			}
3114
		}
3115
3116
		return true;
3117
	}
3118
3119
	/**
3120
	 * Called by sourceStream() to check if we've reached a statement end
3121
	 *
3122
	 * @param string $sql SQL assembled so far
3123
	 * @param string $newLine New line about to be added to $sql
3124
	 * @return bool Whether $newLine contains end of the statement
3125
	 */
3126
	public function streamStatementEnd( &$sql, &$newLine ) {
3127
		if ( $this->delimiter ) {
3128
			$prev = $newLine;
3129
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3130
			if ( $newLine != $prev ) {
3131
				return true;
3132
			}
3133
		}
3134
3135
		return false;
3136
	}
3137
3138
	/**
3139
	 * Database independent variable replacement. Replaces a set of variables
3140
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3141
	 *
3142
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3143
	 *
3144
	 * - '{$var}' should be used for text and is passed through the database's
3145
	 *   addQuotes method.
3146
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3147
	 *   It is passed through the database's addIdentifierQuotes method which
3148
	 *   can be overridden if the database uses something other than backticks.
3149
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3150
	 *   database's tableName method.
3151
	 * - / *i* / passes the name that follows through the database's indexName method.
3152
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3153
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3154
	 *
3155
	 * @param string $ins SQL statement to replace variables in
3156
	 * @return string The new SQL statement with variables replaced
3157
	 */
3158
	protected function replaceVars( $ins ) {
3159
		$vars = $this->getSchemaVars();
3160
		return preg_replace_callback(
3161
			'!
3162
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3163
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3164
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3165
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3166
			!x',
3167
			function ( $m ) use ( $vars ) {
3168
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3169
				// check for both nonexistent keys *and* the empty string.
3170
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3171
					if ( $m[1] === 'i' ) {
3172
						return $this->indexName( $m[2] );
3173
					} else {
3174
						return $this->tableName( $m[2] );
3175
					}
3176 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3177
					return $this->addQuotes( $vars[$m[3]] );
3178
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3179
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3180 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3181
					return $vars[$m[5]];
3182
				} else {
3183
					return $m[0];
3184
				}
3185
			},
3186
			$ins
3187
		);
3188
	}
3189
3190
	/**
3191
	 * Get schema variables. If none have been set via setSchemaVars(), then
3192
	 * use some defaults from the current object.
3193
	 *
3194
	 * @return array
3195
	 */
3196
	protected function getSchemaVars() {
3197
		if ( $this->mSchemaVars ) {
3198
			return $this->mSchemaVars;
3199
		} else {
3200
			return $this->getDefaultSchemaVars();
3201
		}
3202
	}
3203
3204
	/**
3205
	 * Get schema variables to use if none have been set via setSchemaVars().
3206
	 *
3207
	 * Override this in derived classes to provide variables for tables.sql
3208
	 * and SQL patch files.
3209
	 *
3210
	 * @return array
3211
	 */
3212
	protected function getDefaultSchemaVars() {
3213
		return [];
3214
	}
3215
3216
	public function lockIsFree( $lockName, $method ) {
3217
		return true;
3218
	}
3219
3220
	public function lock( $lockName, $method, $timeout = 5 ) {
3221
		$this->mNamedLocksHeld[$lockName] = 1;
3222
3223
		return true;
3224
	}
3225
3226
	public function unlock( $lockName, $method ) {
3227
		unset( $this->mNamedLocksHeld[$lockName] );
3228
3229
		return true;
3230
	}
3231
3232
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3233
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3234
			return null;
3235
		}
3236
3237
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3238
			$this->commit( __METHOD__, 'flush' );
3239
			$this->unlock( $lockKey, $fname );
3240
		} );
3241
3242
		$this->commit( __METHOD__, 'flush' );
3243
3244
		return $unlocker;
3245
	}
3246
3247
	public function namedLocksEnqueue() {
3248
		return false;
3249
	}
3250
3251
	/**
3252
	 * Lock specific tables
3253
	 *
3254
	 * @param array $read Array of tables to lock for read access
3255
	 * @param array $write Array of tables to lock for write access
3256
	 * @param string $method Name of caller
3257
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3258
	 * @return bool
3259
	 */
3260
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3261
		return true;
3262
	}
3263
3264
	/**
3265
	 * Unlock specific tables
3266
	 *
3267
	 * @param string $method The caller
3268
	 * @return bool
3269
	 */
3270
	public function unlockTables( $method ) {
3271
		return true;
3272
	}
3273
3274
	/**
3275
	 * Delete a table
3276
	 * @param string $tableName
3277
	 * @param string $fName
3278
	 * @return bool|ResultWrapper
3279
	 * @since 1.18
3280
	 */
3281 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3282
		if ( !$this->tableExists( $tableName, $fName ) ) {
3283
			return false;
3284
		}
3285
		$sql = "DROP TABLE " . $this->tableName( $tableName );
3286
		if ( $this->cascadingDeletes() ) {
3287
			$sql .= " CASCADE";
3288
		}
3289
3290
		return $this->query( $sql, $fName );
3291
	}
3292
3293
	/**
3294
	 * Get search engine class. All subclasses of this need to implement this
3295
	 * if they wish to use searching.
3296
	 *
3297
	 * @return string
3298
	 */
3299
	public function getSearchEngine() {
3300
		return 'SearchEngineDummy';
3301
	}
3302
3303
	public function getInfinity() {
3304
		return 'infinity';
3305
	}
3306
3307
	public function encodeExpiry( $expiry ) {
3308
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3309
			? $this->getInfinity()
3310
			: $this->timestamp( $expiry );
3311
	}
3312
3313
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3314
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3315
			? 'infinity'
3316
			: wfTimestamp( $format, $expiry );
3317
	}
3318
3319
	public function setBigSelects( $value = true ) {
3320
		// no-op
3321
	}
3322
3323
	public function isReadOnly() {
3324
		return ( $this->getReadOnlyReason() !== false );
3325
	}
3326
3327
	/**
3328
	 * @return string|bool Reason this DB is read-only or false if it is not
3329
	 */
3330
	protected function getReadOnlyReason() {
3331
		$reason = $this->getLBInfo( 'readOnlyReason' );
3332
3333
		return is_string( $reason ) ? $reason : false;
3334
	}
3335
3336
	/**
3337
	 * @since 1.19
3338
	 * @return string
3339
	 */
3340
	public function __toString() {
3341
		return (string)$this->mConn;
3342
	}
3343
3344
	/**
3345
	 * Run a few simple sanity checks
3346
	 */
3347
	public function __destruct() {
3348
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3349
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3350
		}
3351
		$danglingCallbacks = array_merge(
3352
			$this->mTrxIdleCallbacks,
3353
			$this->mTrxPreCommitCallbacks,
3354
			$this->mTrxEndCallbacks
3355
		);
3356
		if ( $danglingCallbacks ) {
3357
			$callers = [];
3358
			foreach ( $danglingCallbacks as $callbackInfo ) {
3359
				$callers[] = $callbackInfo[1];
3360
			}
3361
			$callers = implode( ', ', $callers );
3362
			trigger_error( "DB transaction callbacks still pending (from $callers)." );
3363
		}
3364
	}
3365
}
3366
3367
/**
3368
 * @since 1.27
3369
 */
3370
abstract class Database extends DatabaseBase {
3371
	// B/C until nothing type hints for DatabaseBase
3372
	// @TODO: finish renaming DatabaseBase => Database
3373
}
3374