Completed
Branch master (bbf110)
by
unknown
25:51
created

DatabaseBase::implicitGroupby()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
		$priorWritesPending = $this->writesOrCallbacksPending();
787
		$this->mLastQuery = $sql;
788
789
		$isWriteQuery = $this->isWriteQuery( $sql );
790
		if ( $isWriteQuery ) {
791
			$reason = $this->getReadOnlyReason();
792
			if ( $reason !== false ) {
793
				throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
794
			}
795
			# Set a flag indicating that writes have been done
796
			$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...
797
		}
798
799
		# Add a comment for easy SHOW PROCESSLIST interpretation
800
		if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
801
			$userName = $wgUser->getName();
802
			if ( mb_strlen( $userName ) > 15 ) {
803
				$userName = mb_substr( $userName, 0, 15 ) . '...';
804
			}
805
			$userName = str_replace( '/', '', $userName );
806
		} else {
807
			$userName = '';
808
		}
809
810
		// Add trace comment to the begin of the sql string, right after the operator.
811
		// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
812
		$commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
813
814
		# Start implicit transactions that wrap the request if DBO_TRX is enabled
815
		if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
816
			&& $this->isTransactableQuery( $sql )
817
		) {
818
			$this->begin( __METHOD__ . " ($fname)" );
819
			$this->mTrxAutomatic = true;
820
		}
821
822
		# Keep track of whether the transaction has write queries pending
823
		if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
824
			$this->mTrxDoneWrites = true;
825
			$this->getTransactionProfiler()->transactionWritingIn(
826
				$this->mServer, $this->mDBname, $this->mTrxShortId );
827
		}
828
829
		$isMaster = !is_null( $this->getLBInfo( 'master' ) );
830
		# generalizeSQL will probably cut down the query to reasonable
831
		# logging size most of the time. The substr is really just a sanity check.
832
		if ( $isMaster ) {
833
			$queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
834
			$totalProf = 'DatabaseBase::query-master';
835
		} else {
836
			$queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
837
			$totalProf = 'DatabaseBase::query';
838
		}
839
		# Include query transaction state
840
		$queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
841
842
		$profiler = Profiler::instance();
843
		if ( !$profiler instanceof ProfilerStub ) {
844
			$totalProfSection = $profiler->scopedProfileIn( $totalProf );
845
			$queryProfSection = $profiler->scopedProfileIn( $queryProf );
846
		}
847
848
		if ( $this->debug() ) {
849
			wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
850
		}
851
852
		$queryId = MWDebug::query( $sql, $fname, $isMaster );
853
854
		# Avoid fatals if close() was called
855
		$this->assertOpen();
856
857
		# Do the query and handle errors
858
		$startTime = microtime( true );
859
		$ret = $this->doQuery( $commentedSql );
860
		$queryRuntime = microtime( true ) - $startTime;
861
		# Log the query time and feed it into the DB trx profiler
862
		$this->getTransactionProfiler()->recordQueryCompletion(
863
			$queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
864
865
		MWDebug::queryTime( $queryId );
866
867
		# Try reconnecting if the connection was lost
868
		if ( false === $ret && $this->wasErrorReissuable() ) {
869
			$recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
870
			# Stash the last error values before anything might clear them
871
			$lastError = $this->lastError();
872
			$lastErrno = $this->lastErrno();
873
			# Update state tracking to reflect transaction loss due to disconnection
874
			$this->handleTransactionLoss();
875
			wfDebug( "Connection lost, reconnecting...\n" );
876
			if ( $this->reconnect() ) {
877
				wfDebug( "Reconnected\n" );
878
				$msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
879
				wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
880
881
				if ( !$recoverable ) {
882
					# Callers may catch the exception and continue to use the DB
883
					$this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
884
				} else {
885
					# Should be safe to silently retry the query
886
					$startTime = microtime( true );
887
					$ret = $this->doQuery( $commentedSql );
888
					$queryRuntime = microtime( true ) - $startTime;
889
					# Log the query time and feed it into the DB trx profiler
890
					$this->getTransactionProfiler()->recordQueryCompletion(
891
						$queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
892
				}
893
			} else {
894
				wfDebug( "Failed\n" );
895
			}
896
		}
897
898
		if ( false === $ret ) {
899
			# Deadlocks cause the entire transaction to abort, not just the statement.
900
			# http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
901
			# https://www.postgresql.org/docs/9.1/static/explicit-locking.html
902
			if ( $this->wasDeadlock() ) {
903
				if ( $this->explicitTrxActive() || $priorWritesPending ) {
904
					$tempIgnore = false; // not recoverable
905
				}
906
				# Update state tracking to reflect transaction loss
907
				$this->handleTransactionLoss();
908
			}
909
910
			$this->reportQueryError(
911
				$this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
912
		}
913
914
		$res = $this->resultObject( $ret );
915
916
		// Destroy profile sections in the opposite order to their creation
917
		ScopedCallback::consume( $queryProfSection );
918
		ScopedCallback::consume( $totalProfSection );
919
920
		if ( $isWriteQuery && $this->mTrxLevel ) {
921
			$this->mTrxWriteDuration += $queryRuntime;
922
			$this->mTrxWriteCallers[] = $fname;
923
		}
924
925
		return $res;
926
	}
927
928
	private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
929
		# Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
930
		# Dropped connections also mean that named locks are automatically released.
931
		# Only allow error suppression in autocommit mode or when the lost transaction
932
		# didn't matter anyway (aside from DBO_TRX snapshot loss).
933
		if ( $this->mNamedLocksHeld ) {
934
			return false; // possible critical section violation
935
		} elseif ( $sql === 'COMMIT' ) {
936
			return !$priorWritesPending; // nothing written anyway? (T127428)
937
		} elseif ( $sql === 'ROLLBACK' ) {
938
			return true; // transaction lost...which is also what was requested :)
939
		} elseif ( $this->explicitTrxActive() ) {
940
			return false; // don't drop atomocity
941
		} elseif ( $priorWritesPending ) {
942
			return false; // prior writes lost from implicit transaction
943
		}
944
945
		return true;
946
	}
947
948
	private function handleTransactionLoss() {
949
		$this->mTrxLevel = 0;
950
		$this->mTrxIdleCallbacks = []; // bug 65263
951
		$this->mTrxPreCommitCallbacks = []; // bug 65263
952
		try {
953
			// Handle callbacks in mTrxEndCallbacks
954
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
955
			return null;
956
		} catch ( Exception $e ) {
957
			// Already logged; move on...
958
			return $e;
959
		}
960
	}
961
962
	public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
963
		if ( $this->ignoreErrors() || $tempIgnore ) {
964
			wfDebug( "SQL ERROR (ignored): $error\n" );
965
		} else {
966
			$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
967
			wfLogDBError(
968
				"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
969
				$this->getLogContext( [
970
					'method' => __METHOD__,
971
					'errno' => $errno,
972
					'error' => $error,
973
					'sql1line' => $sql1line,
974
					'fname' => $fname,
975
				] )
976
			);
977
			wfDebug( "SQL ERROR: " . $error . "\n" );
978
			throw new DBQueryError( $this, $error, $errno, $sql, $fname );
979
		}
980
	}
981
982
	/**
983
	 * Intended to be compatible with the PEAR::DB wrapper functions.
984
	 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
985
	 *
986
	 * ? = scalar value, quoted as necessary
987
	 * ! = raw SQL bit (a function for instance)
988
	 * & = filename; reads the file and inserts as a blob
989
	 *     (we don't use this though...)
990
	 *
991
	 * @param string $sql
992
	 * @param string $func
993
	 *
994
	 * @return array
995
	 */
996
	protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
997
		/* MySQL doesn't support prepared statements (yet), so just
998
		 * pack up the query for reference. We'll manually replace
999
		 * the bits later.
1000
		 */
1001
		return [ 'query' => $sql, 'func' => $func ];
1002
	}
1003
1004
	/**
1005
	 * Free a prepared query, generated by prepare().
1006
	 * @param string $prepared
1007
	 */
1008
	protected function freePrepared( $prepared ) {
1009
		/* No-op by default */
1010
	}
1011
1012
	/**
1013
	 * Execute a prepared query with the various arguments
1014
	 * @param string $prepared The prepared sql
1015
	 * @param mixed $args Either an array here, or put scalars as varargs
1016
	 *
1017
	 * @return ResultWrapper
1018
	 */
1019
	public function execute( $prepared, $args = null ) {
1020
		if ( !is_array( $args ) ) {
1021
			# Pull the var args
1022
			$args = func_get_args();
1023
			array_shift( $args );
1024
		}
1025
1026
		$sql = $this->fillPrepared( $prepared['query'], $args );
1027
1028
		return $this->query( $sql, $prepared['func'] );
1029
	}
1030
1031
	/**
1032
	 * For faking prepared SQL statements on DBs that don't support it directly.
1033
	 *
1034
	 * @param string $preparedQuery A 'preparable' SQL statement
1035
	 * @param array $args Array of Arguments to fill it with
1036
	 * @return string Executable SQL
1037
	 */
1038
	public function fillPrepared( $preparedQuery, $args ) {
1039
		reset( $args );
1040
		$this->preparedArgs =& $args;
1041
1042
		return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
1043
			[ &$this, 'fillPreparedArg' ], $preparedQuery );
1044
	}
1045
1046
	/**
1047
	 * preg_callback func for fillPrepared()
1048
	 * The arguments should be in $this->preparedArgs and must not be touched
1049
	 * while we're doing this.
1050
	 *
1051
	 * @param array $matches
1052
	 * @throws DBUnexpectedError
1053
	 * @return string
1054
	 */
1055
	protected function fillPreparedArg( $matches ) {
1056
		switch ( $matches[1] ) {
1057
			case '\\?':
1058
				return '?';
1059
			case '\\!':
1060
				return '!';
1061
			case '\\&':
1062
				return '&';
1063
		}
1064
1065
		list( /* $n */, $arg ) = each( $this->preparedArgs );
1066
1067
		switch ( $matches[1] ) {
1068
			case '?':
1069
				return $this->addQuotes( $arg );
1070
			case '!':
1071
				return $arg;
1072
			case '&':
1073
				# return $this->addQuotes( file_get_contents( $arg ) );
1074
				throw new DBUnexpectedError(
1075
					$this,
1076
					'& mode is not implemented. If it\'s really needed, uncomment the line above.'
1077
				);
1078
			default:
1079
				throw new DBUnexpectedError(
1080
					$this,
1081
					'Received invalid match. This should never happen!'
1082
				);
1083
		}
1084
	}
1085
1086
	public function freeResult( $res ) {
1087
	}
1088
1089
	public function selectField(
1090
		$table, $var, $cond = '', $fname = __METHOD__, $options = []
1091
	) {
1092
		if ( $var === '*' ) { // sanity
1093
			throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1094
		}
1095
1096
		if ( !is_array( $options ) ) {
1097
			$options = [ $options ];
1098
		}
1099
1100
		$options['LIMIT'] = 1;
1101
1102
		$res = $this->select( $table, $var, $cond, $fname, $options );
1103
		if ( $res === false || !$this->numRows( $res ) ) {
1104
			return false;
1105
		}
1106
1107
		$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 1102 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...
1108
1109
		if ( $row !== false ) {
1110
			return reset( $row );
1111
		} else {
1112
			return false;
1113
		}
1114
	}
1115
1116
	public function selectFieldValues(
1117
		$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1118
	) {
1119
		if ( $var === '*' ) { // sanity
1120
			throw new DBUnexpectedError( $this, "Cannot use a * field" );
1121
		} elseif ( !is_string( $var ) ) { // sanity
1122
			throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1123
		}
1124
1125
		if ( !is_array( $options ) ) {
1126
			$options = [ $options ];
1127
		}
1128
1129
		$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1130
		if ( $res === false ) {
1131
			return false;
1132
		}
1133
1134
		$values = [];
1135
		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...
1136
			$values[] = $row->$var;
1137
		}
1138
1139
		return $values;
1140
	}
1141
1142
	/**
1143
	 * Returns an optional USE INDEX clause to go after the table, and a
1144
	 * string to go at the end of the query.
1145
	 *
1146
	 * @param array $options Associative array of options to be turned into
1147
	 *   an SQL query, valid keys are listed in the function.
1148
	 * @return array
1149
	 * @see DatabaseBase::select()
1150
	 */
1151
	public function makeSelectOptions( $options ) {
1152
		$preLimitTail = $postLimitTail = '';
1153
		$startOpts = '';
1154
1155
		$noKeyOptions = [];
1156
1157
		foreach ( $options as $key => $option ) {
1158
			if ( is_numeric( $key ) ) {
1159
				$noKeyOptions[$option] = true;
1160
			}
1161
		}
1162
1163
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1164
1165
		$preLimitTail .= $this->makeOrderBy( $options );
1166
1167
		// if (isset($options['LIMIT'])) {
1168
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1169
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1170
		// 		: false);
1171
		// }
1172
1173
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1174
			$postLimitTail .= ' FOR UPDATE';
1175
		}
1176
1177
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1178
			$postLimitTail .= ' LOCK IN SHARE MODE';
1179
		}
1180
1181
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1182
			$startOpts .= 'DISTINCT';
1183
		}
1184
1185
		# Various MySQL extensions
1186
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1187
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1188
		}
1189
1190
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1191
			$startOpts .= ' HIGH_PRIORITY';
1192
		}
1193
1194
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1195
			$startOpts .= ' SQL_BIG_RESULT';
1196
		}
1197
1198
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1199
			$startOpts .= ' SQL_BUFFER_RESULT';
1200
		}
1201
1202
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1203
			$startOpts .= ' SQL_SMALL_RESULT';
1204
		}
1205
1206
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1207
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1208
		}
1209
1210
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1211
			$startOpts .= ' SQL_CACHE';
1212
		}
1213
1214
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1215
			$startOpts .= ' SQL_NO_CACHE';
1216
		}
1217
1218 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1219
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1220
		} else {
1221
			$useIndex = '';
1222
		}
1223
1224
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1225
	}
1226
1227
	/**
1228
	 * Returns an optional GROUP BY with an optional HAVING
1229
	 *
1230
	 * @param array $options Associative array of options
1231
	 * @return string
1232
	 * @see DatabaseBase::select()
1233
	 * @since 1.21
1234
	 */
1235
	public function makeGroupByWithHaving( $options ) {
1236
		$sql = '';
1237 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1238
			$gb = is_array( $options['GROUP BY'] )
1239
				? implode( ',', $options['GROUP BY'] )
1240
				: $options['GROUP BY'];
1241
			$sql .= ' GROUP BY ' . $gb;
1242
		}
1243 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1244
			$having = is_array( $options['HAVING'] )
1245
				? $this->makeList( $options['HAVING'], LIST_AND )
1246
				: $options['HAVING'];
1247
			$sql .= ' HAVING ' . $having;
1248
		}
1249
1250
		return $sql;
1251
	}
1252
1253
	/**
1254
	 * Returns an optional ORDER BY
1255
	 *
1256
	 * @param array $options Associative array of options
1257
	 * @return string
1258
	 * @see DatabaseBase::select()
1259
	 * @since 1.21
1260
	 */
1261
	public function makeOrderBy( $options ) {
1262 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1263
			$ob = is_array( $options['ORDER BY'] )
1264
				? implode( ',', $options['ORDER BY'] )
1265
				: $options['ORDER BY'];
1266
1267
			return ' ORDER BY ' . $ob;
1268
		}
1269
1270
		return '';
1271
	}
1272
1273
	// See IDatabase::select for the docs for this function
1274
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1275
		$options = [], $join_conds = [] ) {
1276
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1277
1278
		return $this->query( $sql, $fname );
1279
	}
1280
1281
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1282
		$options = [], $join_conds = []
1283
	) {
1284
		if ( is_array( $vars ) ) {
1285
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1286
		}
1287
1288
		$options = (array)$options;
1289
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1290
			? $options['USE INDEX']
1291
			: [];
1292
1293
		if ( is_array( $table ) ) {
1294
			$from = ' FROM ' .
1295
				$this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
1296
		} elseif ( $table != '' ) {
1297
			if ( $table[0] == ' ' ) {
1298
				$from = ' FROM ' . $table;
1299
			} else {
1300
				$from = ' FROM ' .
1301
					$this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
1302
			}
1303
		} else {
1304
			$from = '';
1305
		}
1306
1307
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
1308
			$this->makeSelectOptions( $options );
1309
1310
		if ( !empty( $conds ) ) {
1311
			if ( is_array( $conds ) ) {
1312
				$conds = $this->makeList( $conds, LIST_AND );
1313
			}
1314
			$sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1315
		} else {
1316
			$sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1317
		}
1318
1319
		if ( isset( $options['LIMIT'] ) ) {
1320
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1321
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1322
		}
1323
		$sql = "$sql $postLimitTail";
1324
1325
		if ( isset( $options['EXPLAIN'] ) ) {
1326
			$sql = 'EXPLAIN ' . $sql;
1327
		}
1328
1329
		return $sql;
1330
	}
1331
1332
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1333
		$options = [], $join_conds = []
1334
	) {
1335
		$options = (array)$options;
1336
		$options['LIMIT'] = 1;
1337
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1338
1339
		if ( $res === false ) {
1340
			return false;
1341
		}
1342
1343
		if ( !$this->numRows( $res ) ) {
1344
			return false;
1345
		}
1346
1347
		$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 1337 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...
1348
1349
		return $obj;
1350
	}
1351
1352
	public function estimateRowCount(
1353
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1354
	) {
1355
		$rows = 0;
1356
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1357
1358 View Code Duplication
		if ( $res ) {
1359
			$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 1356 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...
1360
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1361
		}
1362
1363
		return $rows;
1364
	}
1365
1366
	public function selectRowCount(
1367
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1368
	) {
1369
		$rows = 0;
1370
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1371
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1372
1373 View Code Duplication
		if ( $res ) {
1374
			$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 1371 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...
1375
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1376
		}
1377
1378
		return $rows;
1379
	}
1380
1381
	/**
1382
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1383
	 * It's only slightly flawed. Don't use for anything important.
1384
	 *
1385
	 * @param string $sql A SQL Query
1386
	 *
1387
	 * @return string
1388
	 */
1389
	protected static function generalizeSQL( $sql ) {
1390
		# This does the same as the regexp below would do, but in such a way
1391
		# as to avoid crashing php on some large strings.
1392
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1393
1394
		$sql = str_replace( "\\\\", '', $sql );
1395
		$sql = str_replace( "\\'", '', $sql );
1396
		$sql = str_replace( "\\\"", '', $sql );
1397
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1398
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1399
1400
		# All newlines, tabs, etc replaced by single space
1401
		$sql = preg_replace( '/\s+/', ' ', $sql );
1402
1403
		# All numbers => N,
1404
		# except the ones surrounded by characters, e.g. l10n
1405
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1406
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1407
1408
		return $sql;
1409
	}
1410
1411
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1412
		$info = $this->fieldInfo( $table, $field );
1413
1414
		return (bool)$info;
1415
	}
1416
1417
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1418
		if ( !$this->tableExists( $table ) ) {
1419
			return null;
1420
		}
1421
1422
		$info = $this->indexInfo( $table, $index, $fname );
1423
		if ( is_null( $info ) ) {
1424
			return null;
1425
		} else {
1426
			return $info !== false;
1427
		}
1428
	}
1429
1430
	public function tableExists( $table, $fname = __METHOD__ ) {
1431
		$table = $this->tableName( $table );
1432
		$old = $this->ignoreErrors( true );
1433
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1434
		$this->ignoreErrors( $old );
1435
1436
		return (bool)$res;
1437
	}
1438
1439
	public function indexUnique( $table, $index ) {
1440
		$indexInfo = $this->indexInfo( $table, $index );
1441
1442
		if ( !$indexInfo ) {
1443
			return null;
1444
		}
1445
1446
		return !$indexInfo[0]->Non_unique;
1447
	}
1448
1449
	/**
1450
	 * Helper for DatabaseBase::insert().
1451
	 *
1452
	 * @param array $options
1453
	 * @return string
1454
	 */
1455
	protected function makeInsertOptions( $options ) {
1456
		return implode( ' ', $options );
1457
	}
1458
1459
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1460
		# No rows to insert, easy just return now
1461
		if ( !count( $a ) ) {
1462
			return true;
1463
		}
1464
1465
		$table = $this->tableName( $table );
1466
1467
		if ( !is_array( $options ) ) {
1468
			$options = [ $options ];
1469
		}
1470
1471
		$fh = null;
1472
		if ( isset( $options['fileHandle'] ) ) {
1473
			$fh = $options['fileHandle'];
1474
		}
1475
		$options = $this->makeInsertOptions( $options );
1476
1477
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1478
			$multi = true;
1479
			$keys = array_keys( $a[0] );
1480
		} else {
1481
			$multi = false;
1482
			$keys = array_keys( $a );
1483
		}
1484
1485
		$sql = 'INSERT ' . $options .
1486
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1487
1488
		if ( $multi ) {
1489
			$first = true;
1490 View Code Duplication
			foreach ( $a as $row ) {
1491
				if ( $first ) {
1492
					$first = false;
1493
				} else {
1494
					$sql .= ',';
1495
				}
1496
				$sql .= '(' . $this->makeList( $row ) . ')';
1497
			}
1498
		} else {
1499
			$sql .= '(' . $this->makeList( $a ) . ')';
1500
		}
1501
1502
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1503
			return false;
1504
		} elseif ( $fh !== null ) {
1505
			return true;
1506
		}
1507
1508
		return (bool)$this->query( $sql, $fname );
1509
	}
1510
1511
	/**
1512
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1513
	 *
1514
	 * @param array $options
1515
	 * @return array
1516
	 */
1517
	protected function makeUpdateOptionsArray( $options ) {
1518
		if ( !is_array( $options ) ) {
1519
			$options = [ $options ];
1520
		}
1521
1522
		$opts = [];
1523
1524
		if ( in_array( 'LOW_PRIORITY', $options ) ) {
1525
			$opts[] = $this->lowPriorityOption();
1526
		}
1527
1528
		if ( in_array( 'IGNORE', $options ) ) {
1529
			$opts[] = 'IGNORE';
1530
		}
1531
1532
		return $opts;
1533
	}
1534
1535
	/**
1536
	 * Make UPDATE options for the DatabaseBase::update function
1537
	 *
1538
	 * @param array $options The options passed to DatabaseBase::update
1539
	 * @return string
1540
	 */
1541
	protected function makeUpdateOptions( $options ) {
1542
		$opts = $this->makeUpdateOptionsArray( $options );
1543
1544
		return implode( ' ', $opts );
1545
	}
1546
1547
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1548
		$table = $this->tableName( $table );
1549
		$opts = $this->makeUpdateOptions( $options );
1550
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1551
1552 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
1553
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1554
		}
1555
1556
		return $this->query( $sql, $fname );
1557
	}
1558
1559
	public function makeList( $a, $mode = LIST_COMMA ) {
1560
		if ( !is_array( $a ) ) {
1561
			throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1562
		}
1563
1564
		$first = true;
1565
		$list = '';
1566
1567
		foreach ( $a as $field => $value ) {
1568
			if ( !$first ) {
1569
				if ( $mode == LIST_AND ) {
1570
					$list .= ' AND ';
1571
				} elseif ( $mode == LIST_OR ) {
1572
					$list .= ' OR ';
1573
				} else {
1574
					$list .= ',';
1575
				}
1576
			} else {
1577
				$first = false;
1578
			}
1579
1580
			if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1581
				$list .= "($value)";
1582
			} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1583
				$list .= "$value";
1584
			} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1585
				// Remove null from array to be handled separately if found
1586
				$includeNull = false;
1587
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1588
					$includeNull = true;
1589
					unset( $value[$nullKey] );
1590
				}
1591
				if ( count( $value ) == 0 && !$includeNull ) {
1592
					throw new MWException( __METHOD__ . ": empty input for field $field" );
1593
				} elseif ( count( $value ) == 0 ) {
1594
					// only check if $field is null
1595
					$list .= "$field IS NULL";
1596
				} else {
1597
					// IN clause contains at least one valid element
1598
					if ( $includeNull ) {
1599
						// Group subconditions to ensure correct precedence
1600
						$list .= '(';
1601
					}
1602
					if ( count( $value ) == 1 ) {
1603
						// Special-case single values, as IN isn't terribly efficient
1604
						// Don't necessarily assume the single key is 0; we don't
1605
						// enforce linear numeric ordering on other arrays here.
1606
						$value = array_values( $value )[0];
1607
						$list .= $field . " = " . $this->addQuotes( $value );
1608
					} else {
1609
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1610
					}
1611
					// if null present in array, append IS NULL
1612
					if ( $includeNull ) {
1613
						$list .= " OR $field IS NULL)";
1614
					}
1615
				}
1616
			} elseif ( $value === null ) {
1617 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR ) {
1618
					$list .= "$field IS ";
1619
				} elseif ( $mode == LIST_SET ) {
1620
					$list .= "$field = ";
1621
				}
1622
				$list .= 'NULL';
1623
			} else {
1624 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1625
					$list .= "$field = ";
1626
				}
1627
				$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1628
			}
1629
		}
1630
1631
		return $list;
1632
	}
1633
1634
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1635
		$conds = [];
1636
1637
		foreach ( $data as $base => $sub ) {
1638
			if ( count( $sub ) ) {
1639
				$conds[] = $this->makeList(
1640
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1641
					LIST_AND );
1642
			}
1643
		}
1644
1645
		if ( $conds ) {
1646
			return $this->makeList( $conds, LIST_OR );
1647
		} else {
1648
			// Nothing to search for...
1649
			return false;
1650
		}
1651
	}
1652
1653
	/**
1654
	 * Return aggregated value alias
1655
	 *
1656
	 * @param array $valuedata
1657
	 * @param string $valuename
1658
	 *
1659
	 * @return string
1660
	 */
1661
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1662
		return $valuename;
1663
	}
1664
1665
	public function bitNot( $field ) {
1666
		return "(~$field)";
1667
	}
1668
1669
	public function bitAnd( $fieldLeft, $fieldRight ) {
1670
		return "($fieldLeft & $fieldRight)";
1671
	}
1672
1673
	public function bitOr( $fieldLeft, $fieldRight ) {
1674
		return "($fieldLeft | $fieldRight)";
1675
	}
1676
1677
	public function buildConcat( $stringList ) {
1678
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1679
	}
1680
1681 View Code Duplication
	public function buildGroupConcatField(
1682
		$delim, $table, $field, $conds = '', $join_conds = []
1683
	) {
1684
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1685
1686
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1687
	}
1688
1689
	public function selectDB( $db ) {
1690
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1691
		# if your database engine supports a concept similar to MySQL's
1692
		# databases you may as well.
1693
		$this->mDBname = $db;
1694
1695
		return true;
1696
	}
1697
1698
	public function getDBname() {
1699
		return $this->mDBname;
1700
	}
1701
1702
	public function getServer() {
1703
		return $this->mServer;
1704
	}
1705
1706
	/**
1707
	 * Format a table name ready for use in constructing an SQL query
1708
	 *
1709
	 * This does two important things: it quotes the table names to clean them up,
1710
	 * and it adds a table prefix if only given a table name with no quotes.
1711
	 *
1712
	 * All functions of this object which require a table name call this function
1713
	 * themselves. Pass the canonical name to such functions. This is only needed
1714
	 * when calling query() directly.
1715
	 *
1716
	 * @note This function does not sanitize user input. It is not safe to use
1717
	 *   this function to escape user input.
1718
	 * @param string $name Database table name
1719
	 * @param string $format One of:
1720
	 *   quoted - Automatically pass the table name through addIdentifierQuotes()
1721
	 *            so that it can be used in a query.
1722
	 *   raw - Do not add identifier quotes to the table name
1723
	 * @return string Full database name
1724
	 */
1725
	public function tableName( $name, $format = 'quoted' ) {
1726
		global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1727
		# Skip the entire process when we have a string quoted on both ends.
1728
		# Note that we check the end so that we will still quote any use of
1729
		# use of `database`.table. But won't break things if someone wants
1730
		# to query a database table with a dot in the name.
1731
		if ( $this->isQuotedIdentifier( $name ) ) {
1732
			return $name;
1733
		}
1734
1735
		# Lets test for any bits of text that should never show up in a table
1736
		# name. Basically anything like JOIN or ON which are actually part of
1737
		# SQL queries, but may end up inside of the table value to combine
1738
		# sql. Such as how the API is doing.
1739
		# Note that we use a whitespace test rather than a \b test to avoid
1740
		# any remote case where a word like on may be inside of a table name
1741
		# surrounded by symbols which may be considered word breaks.
1742
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1743
			return $name;
1744
		}
1745
1746
		# Split database and table into proper variables.
1747
		# We reverse the explode so that database.table and table both output
1748
		# the correct table.
1749
		$dbDetails = explode( '.', $name, 3 );
1750
		if ( count( $dbDetails ) == 3 ) {
1751
			list( $database, $schema, $table ) = $dbDetails;
1752
			# We don't want any prefix added in this case
1753
			$prefix = '';
1754
		} elseif ( count( $dbDetails ) == 2 ) {
1755
			list( $database, $table ) = $dbDetails;
1756
			# We don't want any prefix added in this case
1757
			# In dbs that support it, $database may actually be the schema
1758
			# but that doesn't affect any of the functionality here
1759
			$prefix = '';
1760
			$schema = null;
1761
		} else {
1762
			list( $table ) = $dbDetails;
1763
			if ( $wgSharedDB !== null # We have a shared database
1764
				&& $this->mForeign == false # We're not working on a foreign database
1765
				&& !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1766
				&& in_array( $table, $wgSharedTables ) # A shared table is selected
1767
			) {
1768
				$database = $wgSharedDB;
1769
				$schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1770
				$prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1771
			} else {
1772
				$database = null;
1773
				$schema = $this->mSchema; # Default schema
1774
				$prefix = $this->mTablePrefix; # Default prefix
1775
			}
1776
		}
1777
1778
		# Quote $table and apply the prefix if not quoted.
1779
		# $tableName might be empty if this is called from Database::replaceVars()
1780
		$tableName = "{$prefix}{$table}";
1781
		if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1782
			$tableName = $this->addIdentifierQuotes( $tableName );
1783
		}
1784
1785
		# Quote $schema and merge it with the table name if needed
1786 View Code Duplication
		if ( strlen( $schema ) ) {
1787
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1788
				$schema = $this->addIdentifierQuotes( $schema );
1789
			}
1790
			$tableName = $schema . '.' . $tableName;
1791
		}
1792
1793
		# Quote $database and merge it with the table name if needed
1794 View Code Duplication
		if ( $database !== null ) {
1795
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1796
				$database = $this->addIdentifierQuotes( $database );
1797
			}
1798
			$tableName = $database . '.' . $tableName;
1799
		}
1800
1801
		return $tableName;
1802
	}
1803
1804
	/**
1805
	 * Fetch a number of table names into an array
1806
	 * This is handy when you need to construct SQL for joins
1807
	 *
1808
	 * Example:
1809
	 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1810
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1811
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1812
	 *
1813
	 * @return array
1814
	 */
1815 View Code Duplication
	public function tableNames() {
1816
		$inArray = func_get_args();
1817
		$retVal = [];
1818
1819
		foreach ( $inArray as $name ) {
1820
			$retVal[$name] = $this->tableName( $name );
1821
		}
1822
1823
		return $retVal;
1824
	}
1825
1826
	/**
1827
	 * Fetch a number of table names into an zero-indexed numerical array
1828
	 * This is handy when you need to construct SQL for joins
1829
	 *
1830
	 * Example:
1831
	 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1832
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1833
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1834
	 *
1835
	 * @return array
1836
	 */
1837 View Code Duplication
	public function tableNamesN() {
1838
		$inArray = func_get_args();
1839
		$retVal = [];
1840
1841
		foreach ( $inArray as $name ) {
1842
			$retVal[] = $this->tableName( $name );
1843
		}
1844
1845
		return $retVal;
1846
	}
1847
1848
	/**
1849
	 * Get an aliased table name
1850
	 * e.g. tableName AS newTableName
1851
	 *
1852
	 * @param string $name Table name, see tableName()
1853
	 * @param string|bool $alias Alias (optional)
1854
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1855
	 */
1856
	public function tableNameWithAlias( $name, $alias = false ) {
1857
		if ( !$alias || $alias == $name ) {
1858
			return $this->tableName( $name );
1859
		} else {
1860
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1856 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...
1861
		}
1862
	}
1863
1864
	/**
1865
	 * Gets an array of aliased table names
1866
	 *
1867
	 * @param array $tables Array( [alias] => table )
1868
	 * @return string[] See tableNameWithAlias()
1869
	 */
1870
	public function tableNamesWithAlias( $tables ) {
1871
		$retval = [];
1872
		foreach ( $tables as $alias => $table ) {
1873
			if ( is_numeric( $alias ) ) {
1874
				$alias = $table;
1875
			}
1876
			$retval[] = $this->tableNameWithAlias( $table, $alias );
1877
		}
1878
1879
		return $retval;
1880
	}
1881
1882
	/**
1883
	 * Get an aliased field name
1884
	 * e.g. fieldName AS newFieldName
1885
	 *
1886
	 * @param string $name Field name
1887
	 * @param string|bool $alias Alias (optional)
1888
	 * @return string SQL name for aliased field. Will not alias a field to its own name
1889
	 */
1890
	public function fieldNameWithAlias( $name, $alias = false ) {
1891
		if ( !$alias || (string)$alias === (string)$name ) {
1892
			return $name;
1893
		} else {
1894
			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 1890 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...
1895
		}
1896
	}
1897
1898
	/**
1899
	 * Gets an array of aliased field names
1900
	 *
1901
	 * @param array $fields Array( [alias] => field )
1902
	 * @return string[] See fieldNameWithAlias()
1903
	 */
1904
	public function fieldNamesWithAlias( $fields ) {
1905
		$retval = [];
1906
		foreach ( $fields as $alias => $field ) {
1907
			if ( is_numeric( $alias ) ) {
1908
				$alias = $field;
1909
			}
1910
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
1911
		}
1912
1913
		return $retval;
1914
	}
1915
1916
	/**
1917
	 * Get the aliased table name clause for a FROM clause
1918
	 * which might have a JOIN and/or USE INDEX clause
1919
	 *
1920
	 * @param array $tables ( [alias] => table )
1921
	 * @param array $use_index Same as for select()
1922
	 * @param array $join_conds Same as for select()
1923
	 * @return string
1924
	 */
1925
	protected function tableNamesWithUseIndexOrJOIN(
1926
		$tables, $use_index = [], $join_conds = []
1927
	) {
1928
		$ret = [];
1929
		$retJOIN = [];
1930
		$use_index = (array)$use_index;
1931
		$join_conds = (array)$join_conds;
1932
1933
		foreach ( $tables as $alias => $table ) {
1934
			if ( !is_string( $alias ) ) {
1935
				// No alias? Set it equal to the table name
1936
				$alias = $table;
1937
			}
1938
			// Is there a JOIN clause for this table?
1939
			if ( isset( $join_conds[$alias] ) ) {
1940
				list( $joinType, $conds ) = $join_conds[$alias];
1941
				$tableClause = $joinType;
1942
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1943
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1944
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1945
					if ( $use != '' ) {
1946
						$tableClause .= ' ' . $use;
1947
					}
1948
				}
1949
				$on = $this->makeList( (array)$conds, LIST_AND );
1950
				if ( $on != '' ) {
1951
					$tableClause .= ' ON (' . $on . ')';
1952
				}
1953
1954
				$retJOIN[] = $tableClause;
1955
			} elseif ( isset( $use_index[$alias] ) ) {
1956
				// Is there an INDEX clause for this table?
1957
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1958
				$tableClause .= ' ' . $this->useIndexClause(
1959
					implode( ',', (array)$use_index[$alias] )
1960
				);
1961
1962
				$ret[] = $tableClause;
1963
			} else {
1964
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1965
1966
				$ret[] = $tableClause;
1967
			}
1968
		}
1969
1970
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
1971
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
1972
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
1973
1974
		// Compile our final table clause
1975
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
1976
	}
1977
1978
	/**
1979
	 * Get the name of an index in a given table.
1980
	 *
1981
	 * @param string $index
1982
	 * @return string
1983
	 */
1984
	protected function indexName( $index ) {
1985
		// Backwards-compatibility hack
1986
		$renamed = [
1987
			'ar_usertext_timestamp' => 'usertext_timestamp',
1988
			'un_user_id' => 'user_id',
1989
			'un_user_ip' => 'user_ip',
1990
		];
1991
1992
		if ( isset( $renamed[$index] ) ) {
1993
			return $renamed[$index];
1994
		} else {
1995
			return $index;
1996
		}
1997
	}
1998
1999
	public function addQuotes( $s ) {
2000
		if ( $s instanceof Blob ) {
2001
			$s = $s->fetch();
2002
		}
2003
		if ( $s === null ) {
2004
			return 'NULL';
2005
		} else {
2006
			# This will also quote numeric values. This should be harmless,
2007
			# and protects against weird problems that occur when they really
2008
			# _are_ strings such as article titles and string->number->string
2009
			# conversion is not 1:1.
2010
			return "'" . $this->strencode( $s ) . "'";
2011
		}
2012
	}
2013
2014
	/**
2015
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
2016
	 * MySQL uses `backticks` while basically everything else uses double quotes.
2017
	 * Since MySQL is the odd one out here the double quotes are our generic
2018
	 * and we implement backticks in DatabaseMysql.
2019
	 *
2020
	 * @param string $s
2021
	 * @return string
2022
	 */
2023
	public function addIdentifierQuotes( $s ) {
2024
		return '"' . str_replace( '"', '""', $s ) . '"';
2025
	}
2026
2027
	/**
2028
	 * Returns if the given identifier looks quoted or not according to
2029
	 * the database convention for quoting identifiers .
2030
	 *
2031
	 * @note Do not use this to determine if untrusted input is safe.
2032
	 *   A malicious user can trick this function.
2033
	 * @param string $name
2034
	 * @return bool
2035
	 */
2036
	public function isQuotedIdentifier( $name ) {
2037
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2038
	}
2039
2040
	/**
2041
	 * @param string $s
2042
	 * @return string
2043
	 */
2044
	protected function escapeLikeInternal( $s ) {
2045
		return addcslashes( $s, '\%_' );
2046
	}
2047
2048
	public function buildLike() {
2049
		$params = func_get_args();
2050
2051
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2052
			$params = $params[0];
2053
		}
2054
2055
		$s = '';
2056
2057
		foreach ( $params as $value ) {
2058
			if ( $value instanceof LikeMatch ) {
2059
				$s .= $value->toString();
2060
			} else {
2061
				$s .= $this->escapeLikeInternal( $value );
2062
			}
2063
		}
2064
2065
		return " LIKE {$this->addQuotes( $s )} ";
2066
	}
2067
2068
	public function anyChar() {
2069
		return new LikeMatch( '_' );
2070
	}
2071
2072
	public function anyString() {
2073
		return new LikeMatch( '%' );
2074
	}
2075
2076
	public function nextSequenceValue( $seqName ) {
2077
		return null;
2078
	}
2079
2080
	/**
2081
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2082
	 * is only needed because a) MySQL must be as efficient as possible due to
2083
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2084
	 * which index to pick. Anyway, other databases might have different
2085
	 * indexes on a given table. So don't bother overriding this unless you're
2086
	 * MySQL.
2087
	 * @param string $index
2088
	 * @return string
2089
	 */
2090
	public function useIndexClause( $index ) {
2091
		return '';
2092
	}
2093
2094
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2095
		$quotedTable = $this->tableName( $table );
2096
2097
		if ( count( $rows ) == 0 ) {
2098
			return;
2099
		}
2100
2101
		# Single row case
2102
		if ( !is_array( reset( $rows ) ) ) {
2103
			$rows = [ $rows ];
2104
		}
2105
2106
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2107
		foreach ( $rows as $row ) {
2108
			# Delete rows which collide
2109
			if ( $uniqueIndexes ) {
2110
				$sql = "DELETE FROM $quotedTable WHERE ";
2111
				$first = true;
2112
				foreach ( $uniqueIndexes as $index ) {
2113
					if ( $first ) {
2114
						$first = false;
2115
						$sql .= '( ';
2116
					} else {
2117
						$sql .= ' ) OR ( ';
2118
					}
2119
					if ( is_array( $index ) ) {
2120
						$first2 = true;
2121
						foreach ( $index as $col ) {
2122
							if ( $first2 ) {
2123
								$first2 = false;
2124
							} else {
2125
								$sql .= ' AND ';
2126
							}
2127
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2128
						}
2129
					} else {
2130
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2131
					}
2132
				}
2133
				$sql .= ' )';
2134
				$this->query( $sql, $fname );
2135
			}
2136
2137
			# Now insert the row
2138
			$this->insert( $table, $row, $fname );
2139
		}
2140
	}
2141
2142
	/**
2143
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2144
	 * statement.
2145
	 *
2146
	 * @param string $table Table name
2147
	 * @param array|string $rows Row(s) to insert
2148
	 * @param string $fname Caller function name
2149
	 *
2150
	 * @return ResultWrapper
2151
	 */
2152
	protected function nativeReplace( $table, $rows, $fname ) {
2153
		$table = $this->tableName( $table );
2154
2155
		# Single row case
2156
		if ( !is_array( reset( $rows ) ) ) {
2157
			$rows = [ $rows ];
2158
		}
2159
2160
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2161
		$first = true;
2162
2163 View Code Duplication
		foreach ( $rows as $row ) {
2164
			if ( $first ) {
2165
				$first = false;
2166
			} else {
2167
				$sql .= ',';
2168
			}
2169
2170
			$sql .= '(' . $this->makeList( $row ) . ')';
2171
		}
2172
2173
		return $this->query( $sql, $fname );
2174
	}
2175
2176
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2177
		$fname = __METHOD__
2178
	) {
2179
		if ( !count( $rows ) ) {
2180
			return true; // nothing to do
2181
		}
2182
2183
		if ( !is_array( reset( $rows ) ) ) {
2184
			$rows = [ $rows ];
2185
		}
2186
2187
		if ( count( $uniqueIndexes ) ) {
2188
			$clauses = []; // list WHERE clauses that each identify a single row
2189
			foreach ( $rows as $row ) {
2190
				foreach ( $uniqueIndexes as $index ) {
2191
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2192
					$rowKey = []; // unique key to this row
2193
					foreach ( $index as $column ) {
2194
						$rowKey[$column] = $row[$column];
2195
					}
2196
					$clauses[] = $this->makeList( $rowKey, LIST_AND );
2197
				}
2198
			}
2199
			$where = [ $this->makeList( $clauses, LIST_OR ) ];
2200
		} else {
2201
			$where = false;
2202
		}
2203
2204
		$useTrx = !$this->mTrxLevel;
2205
		if ( $useTrx ) {
2206
			$this->begin( $fname );
2207
		}
2208
		try {
2209
			# Update any existing conflicting row(s)
2210
			if ( $where !== false ) {
2211
				$ok = $this->update( $table, $set, $where, $fname );
2212
			} else {
2213
				$ok = true;
2214
			}
2215
			# Now insert any non-conflicting row(s)
2216
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2217
		} catch ( Exception $e ) {
2218
			if ( $useTrx ) {
2219
				$this->rollback( $fname );
2220
			}
2221
			throw $e;
2222
		}
2223
		if ( $useTrx ) {
2224
			$this->commit( $fname );
2225
		}
2226
2227
		return $ok;
2228
	}
2229
2230 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2231
		$fname = __METHOD__
2232
	) {
2233
		if ( !$conds ) {
2234
			throw new DBUnexpectedError( $this,
2235
				'DatabaseBase::deleteJoin() called with empty $conds' );
2236
		}
2237
2238
		$delTable = $this->tableName( $delTable );
2239
		$joinTable = $this->tableName( $joinTable );
2240
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2241
		if ( $conds != '*' ) {
2242
			$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2243
		}
2244
		$sql .= ')';
2245
2246
		$this->query( $sql, $fname );
2247
	}
2248
2249
	/**
2250
	 * Returns the size of a text field, or -1 for "unlimited"
2251
	 *
2252
	 * @param string $table
2253
	 * @param string $field
2254
	 * @return int
2255
	 */
2256
	public function textFieldSize( $table, $field ) {
2257
		$table = $this->tableName( $table );
2258
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2259
		$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2260
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, 'DatabaseBase::textFieldSize') on line 2259 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...
2261
2262
		$m = [];
2263
2264
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2265
			$size = $m[1];
2266
		} else {
2267
			$size = -1;
2268
		}
2269
2270
		return $size;
2271
	}
2272
2273
	/**
2274
	 * A string to insert into queries to show that they're low-priority, like
2275
	 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2276
	 * string and nothing bad should happen.
2277
	 *
2278
	 * @return string Returns the text of the low priority option if it is
2279
	 *   supported, or a blank string otherwise
2280
	 */
2281
	public function lowPriorityOption() {
2282
		return '';
2283
	}
2284
2285
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2286
		if ( !$conds ) {
2287
			throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2288
		}
2289
2290
		$table = $this->tableName( $table );
2291
		$sql = "DELETE FROM $table";
2292
2293 View Code Duplication
		if ( $conds != '*' ) {
2294
			if ( is_array( $conds ) ) {
2295
				$conds = $this->makeList( $conds, LIST_AND );
2296
			}
2297
			$sql .= ' WHERE ' . $conds;
2298
		}
2299
2300
		return $this->query( $sql, $fname );
2301
	}
2302
2303
	public function insertSelect( $destTable, $srcTable, $varMap, $conds,
2304
		$fname = __METHOD__,
2305
		$insertOptions = [], $selectOptions = []
2306
	) {
2307
		$destTable = $this->tableName( $destTable );
2308
2309
		if ( !is_array( $insertOptions ) ) {
2310
			$insertOptions = [ $insertOptions ];
2311
		}
2312
2313
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2314
2315
		if ( !is_array( $selectOptions ) ) {
2316
			$selectOptions = [ $selectOptions ];
2317
		}
2318
2319
		list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
2320
2321 View Code Duplication
		if ( is_array( $srcTable ) ) {
2322
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2323
		} else {
2324
			$srcTable = $this->tableName( $srcTable );
2325
		}
2326
2327
		$sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2328
			" SELECT $startOpts " . implode( ',', $varMap ) .
2329
			" FROM $srcTable $useIndex ";
2330
2331 View Code Duplication
		if ( $conds != '*' ) {
2332
			if ( is_array( $conds ) ) {
2333
				$conds = $this->makeList( $conds, LIST_AND );
2334
			}
2335
			$sql .= " WHERE $conds";
2336
		}
2337
2338
		$sql .= " $tailOpts";
2339
2340
		return $this->query( $sql, $fname );
2341
	}
2342
2343
	/**
2344
	 * Construct a LIMIT query with optional offset. This is used for query
2345
	 * pages. The SQL should be adjusted so that only the first $limit rows
2346
	 * are returned. If $offset is provided as well, then the first $offset
2347
	 * rows should be discarded, and the next $limit rows should be returned.
2348
	 * If the result of the query is not ordered, then the rows to be returned
2349
	 * are theoretically arbitrary.
2350
	 *
2351
	 * $sql is expected to be a SELECT, if that makes a difference.
2352
	 *
2353
	 * The version provided by default works in MySQL and SQLite. It will very
2354
	 * likely need to be overridden for most other DBMSes.
2355
	 *
2356
	 * @param string $sql SQL query we will append the limit too
2357
	 * @param int $limit The SQL limit
2358
	 * @param int|bool $offset The SQL offset (default false)
2359
	 * @throws DBUnexpectedError
2360
	 * @return string
2361
	 */
2362
	public function limitResult( $sql, $limit, $offset = false ) {
2363
		if ( !is_numeric( $limit ) ) {
2364
			throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2365
		}
2366
2367
		return "$sql LIMIT "
2368
			. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2369
			. "{$limit} ";
2370
	}
2371
2372
	public function unionSupportsOrderAndLimit() {
2373
		return true; // True for almost every DB supported
2374
	}
2375
2376
	public function unionQueries( $sqls, $all ) {
2377
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2378
2379
		return '(' . implode( $glue, $sqls ) . ')';
2380
	}
2381
2382
	public function conditional( $cond, $trueVal, $falseVal ) {
2383
		if ( is_array( $cond ) ) {
2384
			$cond = $this->makeList( $cond, LIST_AND );
2385
		}
2386
2387
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2388
	}
2389
2390
	public function strreplace( $orig, $old, $new ) {
2391
		return "REPLACE({$orig}, {$old}, {$new})";
2392
	}
2393
2394
	public function getServerUptime() {
2395
		return 0;
2396
	}
2397
2398
	public function wasDeadlock() {
2399
		return false;
2400
	}
2401
2402
	public function wasLockTimeout() {
2403
		return false;
2404
	}
2405
2406
	public function wasErrorReissuable() {
2407
		return false;
2408
	}
2409
2410
	public function wasReadOnlyError() {
2411
		return false;
2412
	}
2413
2414
	/**
2415
	 * Determines if the given query error was a connection drop
2416
	 * STUB
2417
	 *
2418
	 * @param integer|string $errno
2419
	 * @return bool
2420
	 */
2421
	public function wasConnectionError( $errno ) {
2422
		return false;
2423
	}
2424
2425
	/**
2426
	 * Perform a deadlock-prone transaction.
2427
	 *
2428
	 * This function invokes a callback function to perform a set of write
2429
	 * queries. If a deadlock occurs during the processing, the transaction
2430
	 * will be rolled back and the callback function will be called again.
2431
	 *
2432
	 * Avoid using this method outside of Job or Maintenance classes.
2433
	 *
2434
	 * Usage:
2435
	 *   $dbw->deadlockLoop( callback, ... );
2436
	 *
2437
	 * Extra arguments are passed through to the specified callback function.
2438
	 * This method requires that no transactions are already active to avoid
2439
	 * causing premature commits or exceptions.
2440
	 *
2441
	 * Returns whatever the callback function returned on its successful,
2442
	 * iteration, or false on error, for example if the retry limit was
2443
	 * reached.
2444
	 *
2445
	 * @return mixed
2446
	 * @throws DBUnexpectedError
2447
	 * @throws Exception
2448
	 */
2449
	public function deadlockLoop() {
2450
		$args = func_get_args();
2451
		$function = array_shift( $args );
2452
		$tries = self::DEADLOCK_TRIES;
2453
2454
		$this->begin( __METHOD__ );
2455
2456
		$retVal = null;
2457
		/** @var Exception $e */
2458
		$e = null;
2459
		do {
2460
			try {
2461
				$retVal = call_user_func_array( $function, $args );
2462
				break;
2463
			} catch ( DBQueryError $e ) {
2464
				if ( $this->wasDeadlock() ) {
2465
					// Retry after a randomized delay
2466
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2467
				} else {
2468
					// Throw the error back up
2469
					throw $e;
2470
				}
2471
			}
2472
		} while ( --$tries > 0 );
2473
2474
		if ( $tries <= 0 ) {
2475
			// Too many deadlocks; give up
2476
			$this->rollback( __METHOD__ );
2477
			throw $e;
2478
		} else {
2479
			$this->commit( __METHOD__ );
2480
2481
			return $retVal;
2482
		}
2483
	}
2484
2485
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2486
		# Real waits are implemented in the subclass.
2487
		return 0;
2488
	}
2489
2490
	public function getSlavePos() {
2491
		# Stub
2492
		return false;
2493
	}
2494
2495
	public function getMasterPos() {
2496
		# Stub
2497
		return false;
2498
	}
2499
2500
	public function serverIsReadOnly() {
2501
		return false;
2502
	}
2503
2504
	final public function onTransactionResolution( callable $callback ) {
2505
		if ( !$this->mTrxLevel ) {
2506
			throw new DBUnexpectedError( $this, "No transaction is active." );
2507
		}
2508
		$this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
2509
	}
2510
2511
	final public function onTransactionIdle( callable $callback ) {
2512
		$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2513
		if ( !$this->mTrxLevel ) {
2514
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2515
		}
2516
	}
2517
2518
	final public function onTransactionPreCommitOrIdle( callable $callback ) {
2519
		if ( $this->mTrxLevel ) {
2520
			$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2521
		} else {
2522
			// If no transaction is active, then make one for this callback
2523
			$this->begin( __METHOD__ );
2524
			try {
2525
				call_user_func( $callback );
2526
				$this->commit( __METHOD__ );
2527
			} catch ( Exception $e ) {
2528
				$this->rollback( __METHOD__ );
2529
				throw $e;
2530
			}
2531
		}
2532
	}
2533
2534
	/**
2535
	 * Whether to disable running of post-commit callbacks
2536
	 *
2537
	 * This method should not be used outside of Database/LoadBalancer
2538
	 *
2539
	 * @param bool $suppress
2540
	 * @since 1.28
2541
	 */
2542
	final public function setPostCommitCallbackSupression( $suppress ) {
2543
		$this->suppressPostCommitCallbacks = $suppress;
2544
	}
2545
2546
	/**
2547
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2548
	 *
2549
	 * This method should not be used outside of Database/LoadBalancer
2550
	 *
2551
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2552
	 * @since 1.20
2553
	 * @throws Exception
2554
	 */
2555
	public function runOnTransactionIdleCallbacks( $trigger ) {
2556
		if ( $this->suppressPostCommitCallbacks ) {
2557
			return;
2558
		}
2559
2560
		$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2561
		/** @var Exception $e */
2562
		$e = $ePrior = null; // last exception
2563
		do { // callbacks may add callbacks :)
2564
			$callbacks = array_merge(
2565
				$this->mTrxIdleCallbacks,
2566
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2567
			);
2568
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2569
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2570
			foreach ( $callbacks as $callback ) {
2571
				try {
2572
					list( $phpCallback ) = $callback;
2573
					$this->clearFlag( DBO_TRX ); // make each query its own transaction
2574
					call_user_func_array( $phpCallback, [ $trigger ] );
2575
					if ( $autoTrx ) {
2576
						$this->setFlag( DBO_TRX ); // restore automatic begin()
2577
					} else {
2578
						$this->clearFlag( DBO_TRX ); // restore auto-commit
2579
					}
2580
				} catch ( Exception $e ) {
2581
					if ( $ePrior ) {
2582
						MWExceptionHandler::logException( $ePrior );
2583
					}
2584
					$ePrior = $e;
2585
					// Some callbacks may use startAtomic/endAtomic, so make sure
2586
					// their transactions are ended so other callbacks don't fail
2587
					if ( $this->trxLevel() ) {
2588
						$this->rollback( __METHOD__ );
2589
					}
2590
				}
2591
			}
2592
		} while ( count( $this->mTrxIdleCallbacks ) );
2593
2594
		if ( $e instanceof Exception ) {
2595
			throw $e; // re-throw any last exception
2596
		}
2597
	}
2598
2599
	/**
2600
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2601
	 *
2602
	 * This method should not be used outside of Database/LoadBalancer
2603
	 *
2604
	 * @since 1.22
2605
	 */
2606
	public function runOnTransactionPreCommitCallbacks() {
2607
		$e = $ePrior = null; // last exception
2608
		do { // callbacks may add callbacks :)
2609
			$callbacks = $this->mTrxPreCommitCallbacks;
2610
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2611
			foreach ( $callbacks as $callback ) {
2612
				try {
2613
					list( $phpCallback ) = $callback;
2614
					call_user_func( $phpCallback );
2615
				} catch ( Exception $e ) {
2616
					if ( $ePrior ) {
2617
						MWExceptionHandler::logException( $ePrior );
2618
					}
2619
					$ePrior = $e;
2620
				}
2621
			}
2622
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2623
2624
		if ( $e instanceof Exception ) {
2625
			throw $e; // re-throw any last exception
2626
		}
2627
	}
2628
2629
	final public function startAtomic( $fname = __METHOD__ ) {
2630
		if ( !$this->mTrxLevel ) {
2631
			$this->begin( $fname );
2632
			$this->mTrxAutomatic = true;
2633
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2634
			// in all changes being in one transaction to keep requests transactional.
2635
			if ( !$this->getFlag( DBO_TRX ) ) {
2636
				$this->mTrxAutomaticAtomic = true;
2637
			}
2638
		}
2639
2640
		$this->mTrxAtomicLevels[] = $fname;
2641
	}
2642
2643
	final public function endAtomic( $fname = __METHOD__ ) {
2644
		if ( !$this->mTrxLevel ) {
2645
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2646
		}
2647
		if ( !$this->mTrxAtomicLevels ||
2648
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2649
		) {
2650
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2651
		}
2652
2653
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2654
			$this->commit( $fname, 'flush' );
2655
		}
2656
	}
2657
2658
	final public function doAtomicSection( $fname, callable $callback ) {
2659
		$this->startAtomic( $fname );
2660
		try {
2661
			call_user_func_array( $callback, [ $this, $fname ] );
2662
		} catch ( Exception $e ) {
2663
			$this->rollback( $fname );
2664
			throw $e;
2665
		}
2666
		$this->endAtomic( $fname );
2667
	}
2668
2669
	final public function begin( $fname = __METHOD__ ) {
2670
		if ( $this->mTrxLevel ) { // implicit commit
2671
			if ( $this->mTrxAtomicLevels ) {
2672
				// If the current transaction was an automatic atomic one, then we definitely have
2673
				// a problem. Same if there is any unclosed atomic level.
2674
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2675
				throw new DBUnexpectedError(
2676
					$this,
2677
					"Got explicit BEGIN from $fname while atomic section(s) $levels are open."
2678
				);
2679
			} elseif ( !$this->mTrxAutomatic ) {
2680
				// We want to warn about inadvertently nested begin/commit pairs, but not about
2681
				// auto-committing implicit transactions that were started by query() via DBO_TRX
2682
				throw new DBUnexpectedError(
2683
					$this,
2684
					"$fname: Transaction already in progress (from {$this->mTrxFname}), " .
2685
						" performing implicit commit!"
2686
				);
2687
			} elseif ( $this->mTrxDoneWrites ) {
2688
				// The transaction was automatic and has done write operations
2689
				throw new DBUnexpectedError(
2690
					$this,
2691
					"$fname: Automatic transaction with writes in progress" .
2692
						" (from {$this->mTrxFname}), performing implicit commit!\n"
2693
				);
2694
			}
2695
2696
			$this->runOnTransactionPreCommitCallbacks();
2697
			$writeTime = $this->pendingWriteQueryDuration();
2698
			$this->doCommit( $fname );
2699 View Code Duplication
			if ( $this->mTrxDoneWrites ) {
2700
				$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...
2701
				$this->getTransactionProfiler()->transactionWritingOut(
2702
					$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 2697 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...
2703
			}
2704
2705
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2706
		}
2707
2708
		// Avoid fatals if close() was called
2709
		$this->assertOpen();
2710
2711
		$this->doBegin( $fname );
2712
		$this->mTrxTimestamp = microtime( true );
2713
		$this->mTrxFname = $fname;
2714
		$this->mTrxDoneWrites = false;
2715
		$this->mTrxAutomatic = false;
2716
		$this->mTrxAutomaticAtomic = false;
2717
		$this->mTrxAtomicLevels = [];
2718
		$this->mTrxShortId = wfRandomString( 12 );
2719
		$this->mTrxWriteDuration = 0.0;
2720
		$this->mTrxWriteCallers = [];
2721
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2722
		// Get an estimate of the slave lag before then, treating estimate staleness
2723
		// as lag itself just to be safe
2724
		$status = $this->getApproximateLagStatus();
2725
		$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...
2726
	}
2727
2728
	/**
2729
	 * Issues the BEGIN command to the database server.
2730
	 *
2731
	 * @see DatabaseBase::begin()
2732
	 * @param string $fname
2733
	 */
2734
	protected function doBegin( $fname ) {
2735
		$this->query( 'BEGIN', $fname );
2736
		$this->mTrxLevel = 1;
2737
	}
2738
2739
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2740
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2741
			// There are still atomic sections open. This cannot be ignored
2742
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2743
			throw new DBUnexpectedError(
2744
				$this,
2745
				"Got COMMIT while atomic sections $levels are still open"
2746
			);
2747
		}
2748
2749
		if ( $flush === 'flush' ) {
2750
			if ( !$this->mTrxLevel ) {
2751
				return; // nothing to do
2752
			} elseif ( !$this->mTrxAutomatic ) {
2753
				throw new DBUnexpectedError(
2754
					$this,
2755
					"$fname: Flushing an explicit transaction, getting out of sync!"
2756
				);
2757
			}
2758
		} else {
2759
			if ( !$this->mTrxLevel ) {
2760
				wfWarn( "$fname: No transaction to commit, something got out of sync!" );
2761
				return; // nothing to do
2762
			} elseif ( $this->mTrxAutomatic ) {
2763
				throw new DBUnexpectedError(
2764
					$this,
2765
					"$fname: Explicit commit of implicit transaction."
2766
				);
2767
			}
2768
		}
2769
2770
		// Avoid fatals if close() was called
2771
		$this->assertOpen();
2772
2773
		$this->runOnTransactionPreCommitCallbacks();
2774
		$writeTime = $this->pendingWriteQueryDuration();
2775
		$this->doCommit( $fname );
2776 View Code Duplication
		if ( $this->mTrxDoneWrites ) {
2777
			$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...
2778
			$this->getTransactionProfiler()->transactionWritingOut(
2779
				$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 2774 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...
2780
		}
2781
2782
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2783
	}
2784
2785
	/**
2786
	 * Issues the COMMIT command to the database server.
2787
	 *
2788
	 * @see DatabaseBase::commit()
2789
	 * @param string $fname
2790
	 */
2791
	protected function doCommit( $fname ) {
2792
		if ( $this->mTrxLevel ) {
2793
			$this->query( 'COMMIT', $fname );
2794
			$this->mTrxLevel = 0;
2795
		}
2796
	}
2797
2798
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
2799
		if ( $flush !== 'flush' ) {
2800
			if ( !$this->mTrxLevel ) {
2801
				wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
2802
				return; // nothing to do
2803
			}
2804
		} else {
2805
			if ( !$this->mTrxLevel ) {
2806
				return; // nothing to do
2807
			}
2808
		}
2809
2810
		// Avoid fatals if close() was called
2811
		$this->assertOpen();
2812
2813
		$this->doRollback( $fname );
2814
		$this->mTrxAtomicLevels = [];
2815
		if ( $this->mTrxDoneWrites ) {
2816
			$this->getTransactionProfiler()->transactionWritingOut(
2817
				$this->mServer, $this->mDBname, $this->mTrxShortId );
2818
		}
2819
2820
		$this->mTrxIdleCallbacks = []; // clear
2821
		$this->mTrxPreCommitCallbacks = []; // clear
2822
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2823
	}
2824
2825
	/**
2826
	 * Issues the ROLLBACK command to the database server.
2827
	 *
2828
	 * @see DatabaseBase::rollback()
2829
	 * @param string $fname
2830
	 */
2831
	protected function doRollback( $fname ) {
2832
		if ( $this->mTrxLevel ) {
2833
			# Disconnects cause rollback anyway, so ignore those errors
2834
			$ignoreErrors = true;
2835
			$this->query( 'ROLLBACK', $fname, $ignoreErrors );
2836
			$this->mTrxLevel = 0;
2837
		}
2838
	}
2839
2840
	/**
2841
	 * @return bool
2842
	 */
2843
	protected function explicitTrxActive() {
2844
		return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
2845
	}
2846
2847
	/**
2848
	 * Creates a new table with structure copied from existing table
2849
	 * Note that unlike most database abstraction functions, this function does not
2850
	 * automatically append database prefix, because it works at a lower
2851
	 * abstraction level.
2852
	 * The table names passed to this function shall not be quoted (this
2853
	 * function calls addIdentifierQuotes when needed).
2854
	 *
2855
	 * @param string $oldName Name of table whose structure should be copied
2856
	 * @param string $newName Name of table to be created
2857
	 * @param bool $temporary Whether the new table should be temporary
2858
	 * @param string $fname Calling function name
2859
	 * @throws MWException
2860
	 * @return bool True if operation was successful
2861
	 */
2862
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
2863
		$fname = __METHOD__
2864
	) {
2865
		throw new MWException(
2866
			'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
2867
	}
2868
2869
	function listTables( $prefix = null, $fname = __METHOD__ ) {
2870
		throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
2871
	}
2872
2873
	/**
2874
	 * Reset the views process cache set by listViews()
2875
	 * @since 1.22
2876
	 */
2877
	final public function clearViewsCache() {
2878
		$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...
2879
	}
2880
2881
	/**
2882
	 * Lists all the VIEWs in the database
2883
	 *
2884
	 * For caching purposes the list of all views should be stored in
2885
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
2886
	 *
2887
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
2888
	 * @param string $fname Name of calling function
2889
	 * @throws MWException
2890
	 * @return array
2891
	 * @since 1.22
2892
	 */
2893
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
2894
		throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
2895
	}
2896
2897
	/**
2898
	 * Differentiates between a TABLE and a VIEW
2899
	 *
2900
	 * @param string $name Name of the database-structure to test.
2901
	 * @throws MWException
2902
	 * @return bool
2903
	 * @since 1.22
2904
	 */
2905
	public function isView( $name ) {
2906
		throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
2907
	}
2908
2909
	public function timestamp( $ts = 0 ) {
2910
		return wfTimestamp( TS_MW, $ts );
2911
	}
2912
2913
	public function timestampOrNull( $ts = null ) {
2914
		if ( is_null( $ts ) ) {
2915
			return null;
2916
		} else {
2917
			return $this->timestamp( $ts );
2918
		}
2919
	}
2920
2921
	/**
2922
	 * Take the result from a query, and wrap it in a ResultWrapper if
2923
	 * necessary. Boolean values are passed through as is, to indicate success
2924
	 * of write queries or failure.
2925
	 *
2926
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
2927
	 * resource, and it was necessary to call this function to convert it to
2928
	 * a wrapper. Nowadays, raw database objects are never exposed to external
2929
	 * callers, so this is unnecessary in external code.
2930
	 *
2931
	 * @param bool|ResultWrapper|resource|object $result
2932
	 * @return bool|ResultWrapper
2933
	 */
2934
	protected function resultObject( $result ) {
2935
		if ( !$result ) {
2936
			return false;
2937
		} elseif ( $result instanceof ResultWrapper ) {
2938
			return $result;
2939
		} elseif ( $result === true ) {
2940
			// Successful write query
2941
			return $result;
2942
		} else {
2943
			return new ResultWrapper( $this, $result );
2944
		}
2945
	}
2946
2947
	public function ping() {
2948
		try {
2949
			// This will reconnect if possible, or error out if not
2950
			$this->query( "SELECT 1 AS ping", __METHOD__ );
2951
			return true;
2952
		} catch ( DBError $e ) {
2953
			return false;
2954
		}
2955
	}
2956
2957
	/**
2958
	 * @return bool
2959
	 */
2960
	protected function reconnect() {
2961
		# Stub. Not essential to override.
2962
		return true;
2963
	}
2964
2965
	public function getSessionLagStatus() {
2966
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
2967
	}
2968
2969
	/**
2970
	 * Get the slave lag when the current transaction started
2971
	 *
2972
	 * This is useful when transactions might use snapshot isolation
2973
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
2974
	 * is this lag plus transaction duration. If they don't, it is still
2975
	 * safe to be pessimistic. This returns null if there is no transaction.
2976
	 *
2977
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
2978
	 * @since 1.27
2979
	 */
2980
	public function getTransactionLagStatus() {
2981
		return $this->mTrxLevel
2982
			? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
2983
			: null;
2984
	}
2985
2986
	/**
2987
	 * Get a slave lag estimate for this server
2988
	 *
2989
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
2990
	 * @since 1.27
2991
	 */
2992
	public function getApproximateLagStatus() {
2993
		return [
2994
			'lag'   => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
2995
			'since' => microtime( true )
2996
		];
2997
	}
2998
2999
	/**
3000
	 * Merge the result of getSessionLagStatus() for several DBs
3001
	 * using the most pessimistic values to estimate the lag of
3002
	 * any data derived from them in combination
3003
	 *
3004
	 * This is information is useful for caching modules
3005
	 *
3006
	 * @see WANObjectCache::set()
3007
	 * @see WANObjectCache::getWithSetCallback()
3008
	 *
3009
	 * @param IDatabase $db1
3010
	 * @param IDatabase ...
3011
	 * @return array Map of values:
3012
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3013
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
3014
	 *   - pending: whether any of the DBs have uncommitted changes
3015
	 * @since 1.27
3016
	 */
3017
	public static function getCacheSetOptions( IDatabase $db1 ) {
3018
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3019
		foreach ( func_get_args() as $db ) {
3020
			/** @var IDatabase $db */
3021
			$status = $db->getSessionLagStatus();
3022
			if ( $status['lag'] === false ) {
3023
				$res['lag'] = false;
3024
			} elseif ( $res['lag'] !== false ) {
3025
				$res['lag'] = max( $res['lag'], $status['lag'] );
3026
			}
3027
			$res['since'] = min( $res['since'], $status['since'] );
3028
			$res['pending'] = $res['pending'] ?: $db->writesPending();
3029
		}
3030
3031
		return $res;
3032
	}
3033
3034
	public function getLag() {
3035
		return 0;
3036
	}
3037
3038
	function maxListLen() {
3039
		return 0;
3040
	}
3041
3042
	public function encodeBlob( $b ) {
3043
		return $b;
3044
	}
3045
3046
	public function decodeBlob( $b ) {
3047
		if ( $b instanceof Blob ) {
3048
			$b = $b->fetch();
3049
		}
3050
		return $b;
3051
	}
3052
3053
	public function setSessionOptions( array $options ) {
3054
	}
3055
3056
	/**
3057
	 * Read and execute SQL commands from a file.
3058
	 *
3059
	 * Returns true on success, error string or exception on failure (depending
3060
	 * on object's error ignore settings).
3061
	 *
3062
	 * @param string $filename File name to open
3063
	 * @param bool|callable $lineCallback Optional function called before reading each line
3064
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3065
	 * @param bool|string $fname Calling function name or false if name should be
3066
	 *   generated dynamically using $filename
3067
	 * @param bool|callable $inputCallback Optional function called for each
3068
	 *   complete line sent
3069
	 * @throws Exception|MWException
3070
	 * @return bool|string
3071
	 */
3072
	public function sourceFile(
3073
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3074
	) {
3075
		MediaWiki\suppressWarnings();
3076
		$fp = fopen( $filename, 'r' );
3077
		MediaWiki\restoreWarnings();
3078
3079
		if ( false === $fp ) {
3080
			throw new MWException( "Could not open \"{$filename}\".\n" );
3081
		}
3082
3083
		if ( !$fname ) {
3084
			$fname = __METHOD__ . "( $filename )";
3085
		}
3086
3087
		try {
3088
			$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 3073 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...
3089
		} catch ( Exception $e ) {
3090
			fclose( $fp );
3091
			throw $e;
3092
		}
3093
3094
		fclose( $fp );
3095
3096
		return $error;
3097
	}
3098
3099
	/**
3100
	 * Get the full path of a patch file. Originally based on archive()
3101
	 * from updaters.inc. Keep in mind this always returns a patch, as
3102
	 * it fails back to MySQL if no DB-specific patch can be found
3103
	 *
3104
	 * @param string $patch The name of the patch, like patch-something.sql
3105
	 * @return string Full path to patch file
3106
	 */
3107 View Code Duplication
	public function patchPath( $patch ) {
3108
		global $IP;
3109
3110
		$dbType = $this->getType();
3111
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
3112
			return "$IP/maintenance/$dbType/archives/$patch";
3113
		} else {
3114
			return "$IP/maintenance/archives/$patch";
3115
		}
3116
	}
3117
3118
	public function setSchemaVars( $vars ) {
3119
		$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...
3120
	}
3121
3122
	/**
3123
	 * Read and execute commands from an open file handle.
3124
	 *
3125
	 * Returns true on success, error string or exception on failure (depending
3126
	 * on object's error ignore settings).
3127
	 *
3128
	 * @param resource $fp File handle
3129
	 * @param bool|callable $lineCallback Optional function called before reading each query
3130
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3131
	 * @param string $fname Calling function name
3132
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3133
	 * @return bool|string
3134
	 */
3135
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3136
		$fname = __METHOD__, $inputCallback = false
3137
	) {
3138
		$cmd = '';
3139
3140
		while ( !feof( $fp ) ) {
3141
			if ( $lineCallback ) {
3142
				call_user_func( $lineCallback );
3143
			}
3144
3145
			$line = trim( fgets( $fp ) );
3146
3147
			if ( $line == '' ) {
3148
				continue;
3149
			}
3150
3151
			if ( '-' == $line[0] && '-' == $line[1] ) {
3152
				continue;
3153
			}
3154
3155
			if ( $cmd != '' ) {
3156
				$cmd .= ' ';
3157
			}
3158
3159
			$done = $this->streamStatementEnd( $cmd, $line );
3160
3161
			$cmd .= "$line\n";
3162
3163
			if ( $done || feof( $fp ) ) {
3164
				$cmd = $this->replaceVars( $cmd );
3165
3166
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3167
					$res = $this->query( $cmd, $fname );
3168
3169
					if ( $resultCallback ) {
3170
						call_user_func( $resultCallback, $res, $this );
3171
					}
3172
3173
					if ( false === $res ) {
3174
						$err = $this->lastError();
3175
3176
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3177
					}
3178
				}
3179
				$cmd = '';
3180
			}
3181
		}
3182
3183
		return true;
3184
	}
3185
3186
	/**
3187
	 * Called by sourceStream() to check if we've reached a statement end
3188
	 *
3189
	 * @param string $sql SQL assembled so far
3190
	 * @param string $newLine New line about to be added to $sql
3191
	 * @return bool Whether $newLine contains end of the statement
3192
	 */
3193
	public function streamStatementEnd( &$sql, &$newLine ) {
3194
		if ( $this->delimiter ) {
3195
			$prev = $newLine;
3196
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3197
			if ( $newLine != $prev ) {
3198
				return true;
3199
			}
3200
		}
3201
3202
		return false;
3203
	}
3204
3205
	/**
3206
	 * Database independent variable replacement. Replaces a set of variables
3207
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3208
	 *
3209
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3210
	 *
3211
	 * - '{$var}' should be used for text and is passed through the database's
3212
	 *   addQuotes method.
3213
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3214
	 *   It is passed through the database's addIdentifierQuotes method which
3215
	 *   can be overridden if the database uses something other than backticks.
3216
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3217
	 *   database's tableName method.
3218
	 * - / *i* / passes the name that follows through the database's indexName method.
3219
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3220
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3221
	 *
3222
	 * @param string $ins SQL statement to replace variables in
3223
	 * @return string The new SQL statement with variables replaced
3224
	 */
3225
	protected function replaceVars( $ins ) {
3226
		$vars = $this->getSchemaVars();
3227
		return preg_replace_callback(
3228
			'!
3229
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3230
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3231
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3232
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3233
			!x',
3234
			function ( $m ) use ( $vars ) {
3235
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3236
				// check for both nonexistent keys *and* the empty string.
3237
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3238
					if ( $m[1] === 'i' ) {
3239
						return $this->indexName( $m[2] );
3240
					} else {
3241
						return $this->tableName( $m[2] );
3242
					}
3243 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3244
					return $this->addQuotes( $vars[$m[3]] );
3245
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3246
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3247 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3248
					return $vars[$m[5]];
3249
				} else {
3250
					return $m[0];
3251
				}
3252
			},
3253
			$ins
3254
		);
3255
	}
3256
3257
	/**
3258
	 * Get schema variables. If none have been set via setSchemaVars(), then
3259
	 * use some defaults from the current object.
3260
	 *
3261
	 * @return array
3262
	 */
3263
	protected function getSchemaVars() {
3264
		if ( $this->mSchemaVars ) {
3265
			return $this->mSchemaVars;
3266
		} else {
3267
			return $this->getDefaultSchemaVars();
3268
		}
3269
	}
3270
3271
	/**
3272
	 * Get schema variables to use if none have been set via setSchemaVars().
3273
	 *
3274
	 * Override this in derived classes to provide variables for tables.sql
3275
	 * and SQL patch files.
3276
	 *
3277
	 * @return array
3278
	 */
3279
	protected function getDefaultSchemaVars() {
3280
		return [];
3281
	}
3282
3283
	public function lockIsFree( $lockName, $method ) {
3284
		return true;
3285
	}
3286
3287
	public function lock( $lockName, $method, $timeout = 5 ) {
3288
		$this->mNamedLocksHeld[$lockName] = 1;
3289
3290
		return true;
3291
	}
3292
3293
	public function unlock( $lockName, $method ) {
3294
		unset( $this->mNamedLocksHeld[$lockName] );
3295
3296
		return true;
3297
	}
3298
3299
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3300
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3301
			return null;
3302
		}
3303
3304
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3305
			$this->commit( __METHOD__, 'flush' );
3306
			$this->unlock( $lockKey, $fname );
3307
		} );
3308
3309
		$this->commit( __METHOD__, 'flush' );
3310
3311
		return $unlocker;
3312
	}
3313
3314
	public function namedLocksEnqueue() {
3315
		return false;
3316
	}
3317
3318
	/**
3319
	 * Lock specific tables
3320
	 *
3321
	 * @param array $read Array of tables to lock for read access
3322
	 * @param array $write Array of tables to lock for write access
3323
	 * @param string $method Name of caller
3324
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3325
	 * @return bool
3326
	 */
3327
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3328
		return true;
3329
	}
3330
3331
	/**
3332
	 * Unlock specific tables
3333
	 *
3334
	 * @param string $method The caller
3335
	 * @return bool
3336
	 */
3337
	public function unlockTables( $method ) {
3338
		return true;
3339
	}
3340
3341
	/**
3342
	 * Delete a table
3343
	 * @param string $tableName
3344
	 * @param string $fName
3345
	 * @return bool|ResultWrapper
3346
	 * @since 1.18
3347
	 */
3348 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3349
		if ( !$this->tableExists( $tableName, $fName ) ) {
3350
			return false;
3351
		}
3352
		$sql = "DROP TABLE " . $this->tableName( $tableName );
3353
		if ( $this->cascadingDeletes() ) {
3354
			$sql .= " CASCADE";
3355
		}
3356
3357
		return $this->query( $sql, $fName );
3358
	}
3359
3360
	/**
3361
	 * Get search engine class. All subclasses of this need to implement this
3362
	 * if they wish to use searching.
3363
	 *
3364
	 * @return string
3365
	 */
3366
	public function getSearchEngine() {
3367
		return 'SearchEngineDummy';
3368
	}
3369
3370
	public function getInfinity() {
3371
		return 'infinity';
3372
	}
3373
3374
	public function encodeExpiry( $expiry ) {
3375
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3376
			? $this->getInfinity()
3377
			: $this->timestamp( $expiry );
3378
	}
3379
3380
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3381
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3382
			? 'infinity'
3383
			: wfTimestamp( $format, $expiry );
3384
	}
3385
3386
	public function setBigSelects( $value = true ) {
3387
		// no-op
3388
	}
3389
3390
	public function isReadOnly() {
3391
		return ( $this->getReadOnlyReason() !== false );
3392
	}
3393
3394
	/**
3395
	 * @return string|bool Reason this DB is read-only or false if it is not
3396
	 */
3397
	protected function getReadOnlyReason() {
3398
		$reason = $this->getLBInfo( 'readOnlyReason' );
3399
3400
		return is_string( $reason ) ? $reason : false;
3401
	}
3402
3403
	/**
3404
	 * @since 1.19
3405
	 * @return string
3406
	 */
3407
	public function __toString() {
3408
		return (string)$this->mConn;
3409
	}
3410
3411
	/**
3412
	 * Run a few simple sanity checks
3413
	 */
3414
	public function __destruct() {
3415
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3416
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3417
		}
3418
		$danglingCallbacks = array_merge(
3419
			$this->mTrxIdleCallbacks,
3420
			$this->mTrxPreCommitCallbacks,
3421
			$this->mTrxEndCallbacks
3422
		);
3423
		if ( $danglingCallbacks ) {
3424
			$callers = [];
3425
			foreach ( $danglingCallbacks as $callbackInfo ) {
3426
				$callers[] = $callbackInfo[1];
3427
			}
3428
			$callers = implode( ', ', $callers );
3429
			trigger_error( "DB transaction callbacks still pending (from $callers)." );
3430
		}
3431
	}
3432
}
3433
3434
/**
3435
 * @since 1.27
3436
 */
3437
abstract class Database extends DatabaseBase {
3438
	// B/C until nothing type hints for DatabaseBase
3439
	// @TODO: finish renaming DatabaseBase => Database
3440
}
3441