Completed
Branch master (9259dd)
by
unknown
27:26
created

DatabaseBase::bufferResults()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
rs 9.4285
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 callable[] */
56
	protected $mTrxIdleCallbacks = [];
57
	/** @var callable[] */
58
	protected $mTrxPreCommitCallbacks = [];
59
60
	protected $mTablePrefix;
61
	protected $mSchema;
62
	protected $mFlags;
63
	protected $mForeign;
64
	protected $mLBInfo = [];
65
	protected $mDefaultBigSelects = null;
66
	protected $mSchemaVars = false;
67
	/** @var array */
68
	protected $mSessionVars = [];
69
70
	protected $preparedArgs;
71
72
	protected $htmlErrors;
73
74
	protected $delimiter = ';';
75
76
	/**
77
	 * Either 1 if a transaction is active or 0 otherwise.
78
	 * The other Trx fields may not be meaningfull if this is 0.
79
	 *
80
	 * @var int
81
	 */
82
	protected $mTrxLevel = 0;
83
84
	/**
85
	 * Either a short hexidecimal string if a transaction is active or ""
86
	 *
87
	 * @var string
88
	 * @see DatabaseBase::mTrxLevel
89
	 */
90
	protected $mTrxShortId = '';
91
92
	/**
93
	 * The UNIX time that the transaction started. Callers can assume that if
94
	 * snapshot isolation is used, then the data is *at least* up to date to that
95
	 * point (possibly more up-to-date since the first SELECT defines the snapshot).
96
	 *
97
	 * @var float|null
98
	 * @see DatabaseBase::mTrxLevel
99
	 */
100
	private $mTrxTimestamp = null;
101
102
	/** @var float Lag estimate at the time of BEGIN */
103
	private $mTrxSlaveLag = null;
104
105
	/**
106
	 * Remembers the function name given for starting the most recent transaction via begin().
107
	 * Used to provide additional context for error reporting.
108
	 *
109
	 * @var string
110
	 * @see DatabaseBase::mTrxLevel
111
	 */
112
	private $mTrxFname = null;
113
114
	/**
115
	 * Record if possible write queries were done in the last transaction started
116
	 *
117
	 * @var bool
118
	 * @see DatabaseBase::mTrxLevel
119
	 */
120
	private $mTrxDoneWrites = false;
121
122
	/**
123
	 * Record if the current transaction was started implicitly due to DBO_TRX being set.
124
	 *
125
	 * @var bool
126
	 * @see DatabaseBase::mTrxLevel
127
	 */
128
	private $mTrxAutomatic = false;
129
130
	/**
131
	 * Array of levels of atomicity within transactions
132
	 *
133
	 * @var array
134
	 */
135
	private $mTrxAtomicLevels = [];
136
137
	/**
138
	 * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
139
	 *
140
	 * @var bool
141
	 */
142
	private $mTrxAutomaticAtomic = false;
143
144
	/**
145
	 * Track the write query callers of the current transaction
146
	 *
147
	 * @var string[]
148
	 */
149
	private $mTrxWriteCallers = [];
150
151
	/**
152
	 * Track the seconds spent in write queries for the current transaction
153
	 *
154
	 * @var float
155
	 */
156
	private $mTrxWriteDuration = 0.0;
157
158
	/** @var array Map of (name => 1) for locks obtained via lock() */
159
	private $mNamedLocksHeld = [];
160
161
	/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
162
	private $lazyMasterHandle;
163
164
	/**
165
	 * @since 1.21
166
	 * @var resource File handle for upgrade
167
	 */
168
	protected $fileHandle = null;
169
170
	/**
171
	 * @since 1.22
172
	 * @var string[] Process cache of VIEWs names in the database
173
	 */
174
	protected $allViews = null;
175
176
	/** @var TransactionProfiler */
177
	protected $trxProfiler;
178
179
	public function getServerInfo() {
180
		return $this->getServerVersion();
181
	}
182
183
	/**
184
	 * @return string Command delimiter used by this database engine
185
	 */
186
	public function getDelimiter() {
187
		return $this->delimiter;
188
	}
189
190
	/**
191
	 * Boolean, controls output of large amounts of debug information.
192
	 * @param bool|null $debug
193
	 *   - true to enable debugging
194
	 *   - false to disable debugging
195
	 *   - omitted or null to do nothing
196
	 *
197
	 * @return bool|null Previous value of the flag
198
	 */
199
	public function debug( $debug = null ) {
200
		return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
0 ignored issues
show
Bug introduced by
It seems like $debug defined by parameter $debug on line 199 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...
201
	}
202
203
	public function bufferResults( $buffer = null ) {
204
		if ( is_null( $buffer ) ) {
205
			return !(bool)( $this->mFlags & DBO_NOBUFFER );
206
		} else {
207
			return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
208
		}
209
	}
210
211
	/**
212
	 * Turns on (false) or off (true) the automatic generation and sending
213
	 * of a "we're sorry, but there has been a database error" page on
214
	 * database errors. Default is on (false). When turned off, the
215
	 * code should use lastErrno() and lastError() to handle the
216
	 * situation as appropriate.
217
	 *
218
	 * Do not use this function outside of the Database classes.
219
	 *
220
	 * @param null|bool $ignoreErrors
221
	 * @return bool The previous value of the flag.
222
	 */
223
	protected function ignoreErrors( $ignoreErrors = null ) {
224
		return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
0 ignored issues
show
Bug introduced by
It seems like $ignoreErrors defined by parameter $ignoreErrors on line 223 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...
225
	}
226
227
	public function trxLevel() {
228
		return $this->mTrxLevel;
229
	}
230
231
	public function trxTimestamp() {
232
		return $this->mTrxLevel ? $this->mTrxTimestamp : null;
233
	}
234
235
	public function tablePrefix( $prefix = null ) {
236
		return wfSetVar( $this->mTablePrefix, $prefix );
237
	}
238
239
	public function dbSchema( $schema = null ) {
240
		return wfSetVar( $this->mSchema, $schema );
241
	}
242
243
	/**
244
	 * Set the filehandle to copy write statements to.
245
	 *
246
	 * @param resource $fh File handle
247
	 */
248
	public function setFileHandle( $fh ) {
249
		$this->fileHandle = $fh;
250
	}
251
252
	public function getLBInfo( $name = null ) {
253
		if ( is_null( $name ) ) {
254
			return $this->mLBInfo;
255
		} else {
256
			if ( array_key_exists( $name, $this->mLBInfo ) ) {
257
				return $this->mLBInfo[$name];
258
			} else {
259
				return null;
260
			}
261
		}
262
	}
263
264
	public function setLBInfo( $name, $value = null ) {
265
		if ( is_null( $value ) ) {
266
			$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...
267
		} else {
268
			$this->mLBInfo[$name] = $value;
269
		}
270
	}
271
272
	/**
273
	 * Set a lazy-connecting DB handle to the master DB (for replication status purposes)
274
	 *
275
	 * @param IDatabase $conn
276
	 * @since 1.27
277
	 */
278
	public function setLazyMasterHandle( IDatabase $conn ) {
279
		$this->lazyMasterHandle = $conn;
280
	}
281
282
	/**
283
	 * @return IDatabase|null
284
	 * @see setLazyMasterHandle()
285
	 * @since 1.27
286
	 */
287
	public function getLazyMasterHandle() {
288
		return $this->lazyMasterHandle;
289
	}
290
291
	/**
292
	 * @return TransactionProfiler
293
	 */
294
	protected function getTransactionProfiler() {
295
		if ( !$this->trxProfiler ) {
296
			$this->trxProfiler = new TransactionProfiler();
297
		}
298
299
		return $this->trxProfiler;
300
	}
301
302
	/**
303
	 * @param TransactionProfiler $profiler
304
	 * @since 1.27
305
	 */
306
	public function setTransactionProfiler( TransactionProfiler $profiler ) {
307
		$this->trxProfiler = $profiler;
308
	}
309
310
	/**
311
	 * Returns true if this database supports (and uses) cascading deletes
312
	 *
313
	 * @return bool
314
	 */
315
	public function cascadingDeletes() {
316
		return false;
317
	}
318
319
	/**
320
	 * Returns true if this database supports (and uses) triggers (e.g. on the page table)
321
	 *
322
	 * @return bool
323
	 */
324
	public function cleanupTriggers() {
325
		return false;
326
	}
327
328
	/**
329
	 * Returns true if this database is strict about what can be put into an IP field.
330
	 * Specifically, it uses a NULL value instead of an empty string.
331
	 *
332
	 * @return bool
333
	 */
334
	public function strictIPs() {
335
		return false;
336
	}
337
338
	/**
339
	 * Returns true if this database uses timestamps rather than integers
340
	 *
341
	 * @return bool
342
	 */
343
	public function realTimestamps() {
344
		return false;
345
	}
346
347
	public function implicitGroupby() {
348
		return true;
349
	}
350
351
	public function implicitOrderby() {
352
		return true;
353
	}
354
355
	/**
356
	 * Returns true if this database can do a native search on IP columns
357
	 * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
358
	 *
359
	 * @return bool
360
	 */
361
	public function searchableIPs() {
362
		return false;
363
	}
364
365
	/**
366
	 * Returns true if this database can use functional indexes
367
	 *
368
	 * @return bool
369
	 */
370
	public function functionalIndexes() {
371
		return false;
372
	}
373
374
	public function lastQuery() {
375
		return $this->mLastQuery;
376
	}
377
378
	public function doneWrites() {
379
		return (bool)$this->mDoneWrites;
380
	}
381
382
	public function lastDoneWrites() {
383
		return $this->mDoneWrites ?: false;
384
	}
385
386
	public function writesPending() {
387
		return $this->mTrxLevel && $this->mTrxDoneWrites;
388
	}
389
390
	public function writesOrCallbacksPending() {
391
		return $this->mTrxLevel && (
392
			$this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
393
		);
394
	}
395
396
	public function pendingWriteQueryDuration() {
397
		return $this->mTrxLevel ? $this->mTrxWriteDuration : false;
398
	}
399
400
	public function pendingWriteCallers() {
401
		return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
402
	}
403
404
	public function isOpen() {
405
		return $this->mOpened;
406
	}
407
408
	public function setFlag( $flag ) {
409
		$this->mFlags |= $flag;
410
	}
411
412
	public function clearFlag( $flag ) {
413
		$this->mFlags &= ~$flag;
414
	}
415
416
	public function getFlag( $flag ) {
417
		return !!( $this->mFlags & $flag );
418
	}
419
420
	public function getProperty( $name ) {
421
		return $this->$name;
422
	}
423
424
	public function getWikiID() {
425
		if ( $this->mTablePrefix ) {
426
			return "{$this->mDBname}-{$this->mTablePrefix}";
427
		} else {
428
			return $this->mDBname;
429
		}
430
	}
431
432
	/**
433
	 * Return a path to the DBMS-specific SQL file if it exists,
434
	 * otherwise default SQL file
435
	 *
436
	 * @param string $filename
437
	 * @return string
438
	 */
439 View Code Duplication
	private function getSqlFilePath( $filename ) {
440
		global $IP;
441
		$dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename";
442
		if ( file_exists( $dbmsSpecificFilePath ) ) {
443
			return $dbmsSpecificFilePath;
444
		} else {
445
			return "$IP/maintenance/$filename";
446
		}
447
	}
448
449
	/**
450
	 * Return a path to the DBMS-specific schema file,
451
	 * otherwise default to tables.sql
452
	 *
453
	 * @return string
454
	 */
455
	public function getSchemaPath() {
456
		return $this->getSqlFilePath( 'tables.sql' );
457
	}
458
459
	/**
460
	 * Return a path to the DBMS-specific update key file,
461
	 * otherwise default to update-keys.sql
462
	 *
463
	 * @return string
464
	 */
465
	public function getUpdateKeysPath() {
466
		return $this->getSqlFilePath( 'update-keys.sql' );
467
	}
468
469
	/**
470
	 * Get information about an index into an object
471
	 * @param string $table Table name
472
	 * @param string $index Index name
473
	 * @param string $fname Calling function name
474
	 * @return mixed Database-specific index description class or false if the index does not exist
475
	 */
476
	abstract function indexInfo( $table, $index, $fname = __METHOD__ );
477
478
	/**
479
	 * Wrapper for addslashes()
480
	 *
481
	 * @param string $s String to be slashed.
482
	 * @return string Slashed string.
483
	 */
484
	abstract function strencode( $s );
485
486
	/**
487
	 * Constructor.
488
	 *
489
	 * FIXME: It is possible to construct a Database object with no associated
490
	 * connection object, by specifying no parameters to __construct(). This
491
	 * feature is deprecated and should be removed.
492
	 *
493
	 * DatabaseBase subclasses should not be constructed directly in external
494
	 * code. DatabaseBase::factory() should be used instead.
495
	 *
496
	 * @param array $params Parameters passed from DatabaseBase::factory()
497
	 */
498
	function __construct( array $params ) {
499
		global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
500
501
		$this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
502
503
		$server = $params['host'];
504
		$user = $params['user'];
505
		$password = $params['password'];
506
		$dbName = $params['dbname'];
507
		$flags = $params['flags'];
508
		$tablePrefix = $params['tablePrefix'];
509
		$schema = $params['schema'];
510
		$foreign = $params['foreign'];
511
512
		$this->mFlags = $flags;
513
		if ( $this->mFlags & DBO_DEFAULT ) {
514
			if ( $wgCommandLineMode ) {
515
				$this->mFlags &= ~DBO_TRX;
516
			} else {
517
				$this->mFlags |= DBO_TRX;
518
			}
519
		}
520
521
		$this->mSessionVars = $params['variables'];
522
523
		/** Get the default table prefix*/
524
		if ( $tablePrefix === 'get from global' ) {
525
			$this->mTablePrefix = $wgDBprefix;
526
		} else {
527
			$this->mTablePrefix = $tablePrefix;
528
		}
529
530
		/** Get the database schema*/
531
		if ( $schema === 'get from global' ) {
532
			$this->mSchema = $wgDBmwschema;
533
		} else {
534
			$this->mSchema = $schema;
535
		}
536
537
		$this->mForeign = $foreign;
538
539
		if ( isset( $params['trxProfiler'] ) ) {
540
			$this->trxProfiler = $params['trxProfiler']; // override
541
		}
542
543
		if ( $user ) {
544
			$this->open( $server, $user, $password, $dbName );
545
		}
546
	}
547
548
	/**
549
	 * Called by serialize. Throw an exception when DB connection is serialized.
550
	 * This causes problems on some database engines because the connection is
551
	 * not restored on unserialize.
552
	 */
553
	public function __sleep() {
554
		throw new MWException( 'Database serialization may cause problems, since ' .
555
			'the connection is not restored on wakeup.' );
556
	}
557
558
	/**
559
	 * Given a DB type, construct the name of the appropriate child class of
560
	 * DatabaseBase. This is designed to replace all of the manual stuff like:
561
	 *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
562
	 * as well as validate against the canonical list of DB types we have
563
	 *
564
	 * This factory function is mostly useful for when you need to connect to a
565
	 * database other than the MediaWiki default (such as for external auth,
566
	 * an extension, et cetera). Do not use this to connect to the MediaWiki
567
	 * database. Example uses in core:
568
	 * @see LoadBalancer::reallyOpenConnection()
569
	 * @see ForeignDBRepo::getMasterDB()
570
	 * @see WebInstallerDBConnect::execute()
571
	 *
572
	 * @since 1.18
573
	 *
574
	 * @param string $dbType A possible DB type
575
	 * @param array $p An array of options to pass to the constructor.
576
	 *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
577
	 * @throws MWException If the database driver or extension cannot be found
578
	 * @return DatabaseBase|null DatabaseBase subclass or null
579
	 */
580
	final public static function factory( $dbType, $p = [] ) {
581
		$canonicalDBTypes = [
582
			'mysql' => [ 'mysqli', 'mysql' ],
583
			'postgres' => [],
584
			'sqlite' => [],
585
			'oracle' => [],
586
			'mssql' => [],
587
		];
588
589
		$driver = false;
590
		$dbType = strtolower( $dbType );
591
		if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
592
			$possibleDrivers = $canonicalDBTypes[$dbType];
593
			if ( !empty( $p['driver'] ) ) {
594
				if ( in_array( $p['driver'], $possibleDrivers ) ) {
595
					$driver = $p['driver'];
596
				} else {
597
					throw new MWException( __METHOD__ .
598
						" cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
599
				}
600
			} else {
601
				foreach ( $possibleDrivers as $posDriver ) {
602
					if ( extension_loaded( $posDriver ) ) {
603
						$driver = $posDriver;
604
						break;
605
					}
606
				}
607
			}
608
		} else {
609
			$driver = $dbType;
610
		}
611
		if ( $driver === false ) {
612
			throw new MWException( __METHOD__ .
613
				" no viable database extension found for type '$dbType'" );
614
		}
615
616
		// Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
617
		// and everything else doesn't use a schema (e.g. null)
618
		// Although postgres and oracle support schemas, we don't use them (yet)
619
		// to maintain backwards compatibility
620
		$defaultSchemas = [
621
			'mssql' => 'get from global',
622
		];
623
624
		$class = 'Database' . ucfirst( $driver );
625
		if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
626
			// Resolve some defaults for b/c
627
			$p['host'] = isset( $p['host'] ) ? $p['host'] : false;
628
			$p['user'] = isset( $p['user'] ) ? $p['user'] : false;
629
			$p['password'] = isset( $p['password'] ) ? $p['password'] : false;
630
			$p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
631
			$p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
632
			$p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
633
			$p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
634
			if ( !isset( $p['schema'] ) ) {
635
				$p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
636
			}
637
			$p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
638
639
			return new $class( $p );
640
		} else {
641
			return null;
642
		}
643
	}
644
645
	protected function installErrorHandler() {
646
		$this->mPHPError = false;
647
		$this->htmlErrors = ini_set( 'html_errors', '0' );
648
		set_error_handler( [ $this, 'connectionErrorHandler' ] );
649
	}
650
651
	/**
652
	 * @return bool|string
653
	 */
654
	protected function restoreErrorHandler() {
655
		restore_error_handler();
656
		if ( $this->htmlErrors !== false ) {
657
			ini_set( 'html_errors', $this->htmlErrors );
658
		}
659
		if ( $this->mPHPError ) {
660
			$error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
661
			$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
662
663
			return $error;
664
		} else {
665
			return false;
666
		}
667
	}
668
669
	/**
670
	 * @param int $errno
671
	 * @param string $errstr
672
	 */
673
	public function connectionErrorHandler( $errno, $errstr ) {
674
		$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...
675
	}
676
677
	/**
678
	 * Create a log context to pass to wfLogDBError or other logging functions.
679
	 *
680
	 * @param array $extras Additional data to add to context
681
	 * @return array
682
	 */
683
	protected function getLogContext( array $extras = [] ) {
684
		return array_merge(
685
			[
686
				'db_server' => $this->mServer,
687
				'db_name' => $this->mDBname,
688
				'db_user' => $this->mUser,
689
			],
690
			$extras
691
		);
692
	}
693
694
	public function close() {
695
		if ( count( $this->mTrxIdleCallbacks ) ) { // sanity
696
			throw new MWException( "Transaction idle callbacks still pending." );
697
		}
698
		if ( $this->mConn ) {
699
			if ( $this->trxLevel() ) {
700
				if ( !$this->mTrxAutomatic ) {
701
					wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " .
702
						" performing implicit commit before closing connection!" );
703
				}
704
705
				$this->commit( __METHOD__, 'flush' );
706
			}
707
708
			$closed = $this->closeConnection();
709
			$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...
710
		} else {
711
			$closed = true;
712
		}
713
		$this->mOpened = false;
714
715
		return $closed;
716
	}
717
718
	/**
719
	 * Make sure isOpen() returns true as a sanity check
720
	 *
721
	 * @throws DBUnexpectedError
722
	 */
723
	protected function assertOpen() {
724
		if ( !$this->isOpen() ) {
725
			throw new DBUnexpectedError( $this, "DB connection was already closed." );
726
		}
727
	}
728
729
	/**
730
	 * Closes underlying database connection
731
	 * @since 1.20
732
	 * @return bool Whether connection was closed successfully
733
	 */
734
	abstract protected function closeConnection();
735
736
	function reportConnectionError( $error = 'Unknown error' ) {
737
		$myError = $this->lastError();
738
		if ( $myError ) {
739
			$error = $myError;
740
		}
741
742
		# New method
743
		throw new DBConnectionError( $this, $error );
744
	}
745
746
	/**
747
	 * The DBMS-dependent part of query()
748
	 *
749
	 * @param string $sql SQL query.
750
	 * @return ResultWrapper|bool Result object to feed to fetchObject,
751
	 *   fetchRow, ...; or false on failure
752
	 */
753
	abstract protected function doQuery( $sql );
754
755
	/**
756
	 * Determine whether a query writes to the DB.
757
	 * Should return true if unsure.
758
	 *
759
	 * @param string $sql
760
	 * @return bool
761
	 */
762
	protected function isWriteQuery( $sql ) {
763
		return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
764
	}
765
766
	/**
767
	 * Determine whether a SQL statement is sensitive to isolation level.
768
	 * A SQL statement is considered transactable if its result could vary
769
	 * depending on the transaction isolation level. Operational commands
770
	 * such as 'SET' and 'SHOW' are not considered to be transactable.
771
	 *
772
	 * @param string $sql
773
	 * @return bool
774
	 */
775
	protected function isTransactableQuery( $sql ) {
776
		$verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
777
		return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ] );
778
	}
779
780
	public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
781
		global $wgUser;
782
783
		$this->mLastQuery = $sql;
784
785
		$isWriteQuery = $this->isWriteQuery( $sql );
786
		if ( $isWriteQuery ) {
787
			$reason = $this->getReadOnlyReason();
788
			if ( $reason !== false ) {
789
				throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
790
			}
791
			# Set a flag indicating that writes have been done
792
			$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...
793
		}
794
795
		# Add a comment for easy SHOW PROCESSLIST interpretation
796
		if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
797
			$userName = $wgUser->getName();
798
			if ( mb_strlen( $userName ) > 15 ) {
799
				$userName = mb_substr( $userName, 0, 15 ) . '...';
800
			}
801
			$userName = str_replace( '/', '', $userName );
802
		} else {
803
			$userName = '';
804
		}
805
806
		// Add trace comment to the begin of the sql string, right after the operator.
807
		// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
808
		$commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
809
810
		if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX ) && $this->isTransactableQuery( $sql ) ) {
811
			$this->begin( __METHOD__ . " ($fname)" );
812
			$this->mTrxAutomatic = true;
813
		}
814
815
		# Keep track of whether the transaction has write queries pending
816
		if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
817
			$this->mTrxDoneWrites = true;
818
			$this->getTransactionProfiler()->transactionWritingIn(
819
				$this->mServer, $this->mDBname, $this->mTrxShortId );
820
		}
821
822
		$isMaster = !is_null( $this->getLBInfo( 'master' ) );
823
		# generalizeSQL will probably cut down the query to reasonable
824
		# logging size most of the time. The substr is really just a sanity check.
825
		if ( $isMaster ) {
826
			$queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
827
			$totalProf = 'DatabaseBase::query-master';
828
		} else {
829
			$queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
830
			$totalProf = 'DatabaseBase::query';
831
		}
832
		# Include query transaction state
833
		$queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
834
835
		$profiler = Profiler::instance();
836
		if ( !$profiler instanceof ProfilerStub ) {
837
			$totalProfSection = $profiler->scopedProfileIn( $totalProf );
838
			$queryProfSection = $profiler->scopedProfileIn( $queryProf );
839
		}
840
841
		if ( $this->debug() ) {
842
			wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
843
		}
844
845
		$queryId = MWDebug::query( $sql, $fname, $isMaster );
846
847
		# Avoid fatals if close() was called
848
		$this->assertOpen();
849
850
		# Do the query and handle errors
851
		$startTime = microtime( true );
852
		$ret = $this->doQuery( $commentedSql );
853
		$queryRuntime = microtime( true ) - $startTime;
854
		# Log the query time and feed it into the DB trx profiler
855
		$this->getTransactionProfiler()->recordQueryCompletion(
856
			$queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
857
858
		MWDebug::queryTime( $queryId );
859
860
		# Try reconnecting if the connection was lost
861
		if ( false === $ret && $this->wasErrorReissuable() ) {
862
			# Transaction is gone; this can mean lost writes or REPEATABLE-READ snapshots
863
			$hadTrx = $this->mTrxLevel;
864
			# T127428: for non-write transactions, a disconnect and a COMMIT are similar:
865
			# neither changed data and in both cases any read snapshots are reset anyway.
866
			$isNoopCommit = ( !$this->writesOrCallbacksPending() && $sql === 'COMMIT' );
867
			# Update state tracking to reflect transaction loss
868
			$this->mTrxLevel = 0;
869
			$this->mTrxIdleCallbacks = []; // bug 65263
870
			$this->mTrxPreCommitCallbacks = []; // bug 65263
871
			wfDebug( "Connection lost, reconnecting...\n" );
872
			# Stash the last error values since ping() might clear them
873
			$lastError = $this->lastError();
874
			$lastErrno = $this->lastErrno();
875
			if ( $this->ping() ) {
876
				wfDebug( "Reconnected\n" );
877
				$server = $this->getServer();
878
				$msg = __METHOD__ . ": lost connection to $server; reconnected";
879
				wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
880
881
				if ( ( $hadTrx && !$isNoopCommit ) || $this->mNamedLocksHeld ) {
882
					# Leave $ret as false and let an error be reported.
883
					# Callers may catch the exception and continue to use the DB.
884
					$this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
885
				} else {
886
					# Should be safe to silently retry (no trx/callbacks/locks)
887
					$startTime = microtime( true );
888
					$ret = $this->doQuery( $commentedSql );
889
					$queryRuntime = microtime( true ) - $startTime;
890
					# Log the query time and feed it into the DB trx profiler
891
					$this->getTransactionProfiler()->recordQueryCompletion(
892
						$queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
893
				}
894
			} else {
895
				wfDebug( "Failed\n" );
896
			}
897
		}
898
899
		if ( false === $ret ) {
900
			$this->reportQueryError(
901
				$this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
902
		}
903
904
		$res = $this->resultObject( $ret );
905
906
		// Destroy profile sections in the opposite order to their creation
907
		ScopedCallback::consume( $queryProfSection );
908
		ScopedCallback::consume( $totalProfSection );
909
910
		if ( $isWriteQuery && $this->mTrxLevel ) {
911
			$this->mTrxWriteDuration += $queryRuntime;
912
			$this->mTrxWriteCallers[] = $fname;
913
		}
914
915
		return $res;
916
	}
917
918
	public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
919
		if ( $this->ignoreErrors() || $tempIgnore ) {
920
			wfDebug( "SQL ERROR (ignored): $error\n" );
921
		} else {
922
			$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
923
			wfLogDBError(
924
				"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
925
				$this->getLogContext( [
926
					'method' => __METHOD__,
927
					'errno' => $errno,
928
					'error' => $error,
929
					'sql1line' => $sql1line,
930
					'fname' => $fname,
931
				] )
932
			);
933
			wfDebug( "SQL ERROR: " . $error . "\n" );
934
			throw new DBQueryError( $this, $error, $errno, $sql, $fname );
935
		}
936
	}
937
938
	/**
939
	 * Intended to be compatible with the PEAR::DB wrapper functions.
940
	 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
941
	 *
942
	 * ? = scalar value, quoted as necessary
943
	 * ! = raw SQL bit (a function for instance)
944
	 * & = filename; reads the file and inserts as a blob
945
	 *     (we don't use this though...)
946
	 *
947
	 * @param string $sql
948
	 * @param string $func
949
	 *
950
	 * @return array
951
	 */
952
	protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
953
		/* MySQL doesn't support prepared statements (yet), so just
954
		 * pack up the query for reference. We'll manually replace
955
		 * the bits later.
956
		 */
957
		return [ 'query' => $sql, 'func' => $func ];
958
	}
959
960
	/**
961
	 * Free a prepared query, generated by prepare().
962
	 * @param string $prepared
963
	 */
964
	protected function freePrepared( $prepared ) {
965
		/* No-op by default */
966
	}
967
968
	/**
969
	 * Execute a prepared query with the various arguments
970
	 * @param string $prepared The prepared sql
971
	 * @param mixed $args Either an array here, or put scalars as varargs
972
	 *
973
	 * @return ResultWrapper
974
	 */
975
	public function execute( $prepared, $args = null ) {
976
		if ( !is_array( $args ) ) {
977
			# Pull the var args
978
			$args = func_get_args();
979
			array_shift( $args );
980
		}
981
982
		$sql = $this->fillPrepared( $prepared['query'], $args );
983
984
		return $this->query( $sql, $prepared['func'] );
985
	}
986
987
	/**
988
	 * For faking prepared SQL statements on DBs that don't support it directly.
989
	 *
990
	 * @param string $preparedQuery A 'preparable' SQL statement
991
	 * @param array $args Array of Arguments to fill it with
992
	 * @return string Executable SQL
993
	 */
994
	public function fillPrepared( $preparedQuery, $args ) {
995
		reset( $args );
996
		$this->preparedArgs =& $args;
997
998
		return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
999
			[ &$this, 'fillPreparedArg' ], $preparedQuery );
1000
	}
1001
1002
	/**
1003
	 * preg_callback func for fillPrepared()
1004
	 * The arguments should be in $this->preparedArgs and must not be touched
1005
	 * while we're doing this.
1006
	 *
1007
	 * @param array $matches
1008
	 * @throws DBUnexpectedError
1009
	 * @return string
1010
	 */
1011
	protected function fillPreparedArg( $matches ) {
1012
		switch ( $matches[1] ) {
1013
			case '\\?':
1014
				return '?';
1015
			case '\\!':
1016
				return '!';
1017
			case '\\&':
1018
				return '&';
1019
		}
1020
1021
		list( /* $n */, $arg ) = each( $this->preparedArgs );
1022
1023
		switch ( $matches[1] ) {
1024
			case '?':
1025
				return $this->addQuotes( $arg );
1026
			case '!':
1027
				return $arg;
1028
			case '&':
1029
				# return $this->addQuotes( file_get_contents( $arg ) );
1030
				throw new DBUnexpectedError(
1031
					$this,
1032
					'& mode is not implemented. If it\'s really needed, uncomment the line above.'
1033
				);
1034
			default:
1035
				throw new DBUnexpectedError(
1036
					$this,
1037
					'Received invalid match. This should never happen!'
1038
				);
1039
		}
1040
	}
1041
1042
	public function freeResult( $res ) {
1043
	}
1044
1045
	public function selectField(
1046
		$table, $var, $cond = '', $fname = __METHOD__, $options = []
1047
	) {
1048
		if ( $var === '*' ) { // sanity
1049
			throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1050
		}
1051
1052
		if ( !is_array( $options ) ) {
1053
			$options = [ $options ];
1054
		}
1055
1056
		$options['LIMIT'] = 1;
1057
1058
		$res = $this->select( $table, $var, $cond, $fname, $options );
1059
		if ( $res === false || !$this->numRows( $res ) ) {
1060
			return false;
1061
		}
1062
1063
		$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 1058 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...
1064
1065
		if ( $row !== false ) {
1066
			return reset( $row );
1067
		} else {
1068
			return false;
1069
		}
1070
	}
1071
1072
	public function selectFieldValues(
1073
		$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1074
	) {
1075
		if ( $var === '*' ) { // sanity
1076
			throw new DBUnexpectedError( $this, "Cannot use a * field" );
1077
		} elseif ( !is_string( $var ) ) { // sanity
1078
			throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1079
		}
1080
1081
		if ( !is_array( $options ) ) {
1082
			$options = [ $options ];
1083
		}
1084
1085
		$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1086
		if ( $res === false ) {
1087
			return false;
1088
		}
1089
1090
		$values = [];
1091
		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...
1092
			$values[] = $row->$var;
1093
		}
1094
1095
		return $values;
1096
	}
1097
1098
	/**
1099
	 * Returns an optional USE INDEX clause to go after the table, and a
1100
	 * string to go at the end of the query.
1101
	 *
1102
	 * @param array $options Associative array of options to be turned into
1103
	 *   an SQL query, valid keys are listed in the function.
1104
	 * @return array
1105
	 * @see DatabaseBase::select()
1106
	 */
1107
	public function makeSelectOptions( $options ) {
1108
		$preLimitTail = $postLimitTail = '';
1109
		$startOpts = '';
1110
1111
		$noKeyOptions = [];
1112
1113
		foreach ( $options as $key => $option ) {
1114
			if ( is_numeric( $key ) ) {
1115
				$noKeyOptions[$option] = true;
1116
			}
1117
		}
1118
1119
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1120
1121
		$preLimitTail .= $this->makeOrderBy( $options );
1122
1123
		// if (isset($options['LIMIT'])) {
1124
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1125
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1126
		// 		: false);
1127
		// }
1128
1129
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1130
			$postLimitTail .= ' FOR UPDATE';
1131
		}
1132
1133
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1134
			$postLimitTail .= ' LOCK IN SHARE MODE';
1135
		}
1136
1137
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1138
			$startOpts .= 'DISTINCT';
1139
		}
1140
1141
		# Various MySQL extensions
1142
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1143
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1144
		}
1145
1146
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1147
			$startOpts .= ' HIGH_PRIORITY';
1148
		}
1149
1150
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1151
			$startOpts .= ' SQL_BIG_RESULT';
1152
		}
1153
1154
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1155
			$startOpts .= ' SQL_BUFFER_RESULT';
1156
		}
1157
1158
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1159
			$startOpts .= ' SQL_SMALL_RESULT';
1160
		}
1161
1162
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1163
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1164
		}
1165
1166
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1167
			$startOpts .= ' SQL_CACHE';
1168
		}
1169
1170
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1171
			$startOpts .= ' SQL_NO_CACHE';
1172
		}
1173
1174 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1175
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1176
		} else {
1177
			$useIndex = '';
1178
		}
1179
1180
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1181
	}
1182
1183
	/**
1184
	 * Returns an optional GROUP BY with an optional HAVING
1185
	 *
1186
	 * @param array $options Associative array of options
1187
	 * @return string
1188
	 * @see DatabaseBase::select()
1189
	 * @since 1.21
1190
	 */
1191
	public function makeGroupByWithHaving( $options ) {
1192
		$sql = '';
1193 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1194
			$gb = is_array( $options['GROUP BY'] )
1195
				? implode( ',', $options['GROUP BY'] )
1196
				: $options['GROUP BY'];
1197
			$sql .= ' GROUP BY ' . $gb;
1198
		}
1199 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1200
			$having = is_array( $options['HAVING'] )
1201
				? $this->makeList( $options['HAVING'], LIST_AND )
1202
				: $options['HAVING'];
1203
			$sql .= ' HAVING ' . $having;
1204
		}
1205
1206
		return $sql;
1207
	}
1208
1209
	/**
1210
	 * Returns an optional ORDER BY
1211
	 *
1212
	 * @param array $options Associative array of options
1213
	 * @return string
1214
	 * @see DatabaseBase::select()
1215
	 * @since 1.21
1216
	 */
1217
	public function makeOrderBy( $options ) {
1218 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1219
			$ob = is_array( $options['ORDER BY'] )
1220
				? implode( ',', $options['ORDER BY'] )
1221
				: $options['ORDER BY'];
1222
1223
			return ' ORDER BY ' . $ob;
1224
		}
1225
1226
		return '';
1227
	}
1228
1229
	// See IDatabase::select for the docs for this function
1230
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1231
		$options = [], $join_conds = [] ) {
1232
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1233
1234
		return $this->query( $sql, $fname );
1235
	}
1236
1237
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1238
		$options = [], $join_conds = []
1239
	) {
1240
		if ( is_array( $vars ) ) {
1241
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1242
		}
1243
1244
		$options = (array)$options;
1245
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1246
			? $options['USE INDEX']
1247
			: [];
1248
1249
		if ( is_array( $table ) ) {
1250
			$from = ' FROM ' .
1251
				$this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
1252
		} elseif ( $table != '' ) {
1253
			if ( $table[0] == ' ' ) {
1254
				$from = ' FROM ' . $table;
1255
			} else {
1256
				$from = ' FROM ' .
1257
					$this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
1258
			}
1259
		} else {
1260
			$from = '';
1261
		}
1262
1263
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
1264
			$this->makeSelectOptions( $options );
1265
1266
		if ( !empty( $conds ) ) {
1267
			if ( is_array( $conds ) ) {
1268
				$conds = $this->makeList( $conds, LIST_AND );
1269
			}
1270
			$sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1271
		} else {
1272
			$sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1273
		}
1274
1275
		if ( isset( $options['LIMIT'] ) ) {
1276
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1277
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1278
		}
1279
		$sql = "$sql $postLimitTail";
1280
1281
		if ( isset( $options['EXPLAIN'] ) ) {
1282
			$sql = 'EXPLAIN ' . $sql;
1283
		}
1284
1285
		return $sql;
1286
	}
1287
1288
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1289
		$options = [], $join_conds = []
1290
	) {
1291
		$options = (array)$options;
1292
		$options['LIMIT'] = 1;
1293
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1294
1295
		if ( $res === false ) {
1296
			return false;
1297
		}
1298
1299
		if ( !$this->numRows( $res ) ) {
1300
			return false;
1301
		}
1302
1303
		$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 1293 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...
1304
1305
		return $obj;
1306
	}
1307
1308
	public function estimateRowCount(
1309
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1310
	) {
1311
		$rows = 0;
1312
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1313
1314 View Code Duplication
		if ( $res ) {
1315
			$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 1312 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...
1316
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1317
		}
1318
1319
		return $rows;
1320
	}
1321
1322
	public function selectRowCount(
1323
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1324
	) {
1325
		$rows = 0;
1326
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1327
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1328
1329 View Code Duplication
		if ( $res ) {
1330
			$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 1327 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...
1331
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1332
		}
1333
1334
		return $rows;
1335
	}
1336
1337
	/**
1338
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1339
	 * It's only slightly flawed. Don't use for anything important.
1340
	 *
1341
	 * @param string $sql A SQL Query
1342
	 *
1343
	 * @return string
1344
	 */
1345
	protected static function generalizeSQL( $sql ) {
1346
		# This does the same as the regexp below would do, but in such a way
1347
		# as to avoid crashing php on some large strings.
1348
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1349
1350
		$sql = str_replace( "\\\\", '', $sql );
1351
		$sql = str_replace( "\\'", '', $sql );
1352
		$sql = str_replace( "\\\"", '', $sql );
1353
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1354
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1355
1356
		# All newlines, tabs, etc replaced by single space
1357
		$sql = preg_replace( '/\s+/', ' ', $sql );
1358
1359
		# All numbers => N,
1360
		# except the ones surrounded by characters, e.g. l10n
1361
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1362
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1363
1364
		return $sql;
1365
	}
1366
1367
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1368
		$info = $this->fieldInfo( $table, $field );
1369
1370
		return (bool)$info;
1371
	}
1372
1373
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1374
		if ( !$this->tableExists( $table ) ) {
1375
			return null;
1376
		}
1377
1378
		$info = $this->indexInfo( $table, $index, $fname );
1379
		if ( is_null( $info ) ) {
1380
			return null;
1381
		} else {
1382
			return $info !== false;
1383
		}
1384
	}
1385
1386
	public function tableExists( $table, $fname = __METHOD__ ) {
1387
		$table = $this->tableName( $table );
1388
		$old = $this->ignoreErrors( true );
1389
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1390
		$this->ignoreErrors( $old );
1391
1392
		return (bool)$res;
1393
	}
1394
1395
	public function indexUnique( $table, $index ) {
1396
		$indexInfo = $this->indexInfo( $table, $index );
1397
1398
		if ( !$indexInfo ) {
1399
			return null;
1400
		}
1401
1402
		return !$indexInfo[0]->Non_unique;
1403
	}
1404
1405
	/**
1406
	 * Helper for DatabaseBase::insert().
1407
	 *
1408
	 * @param array $options
1409
	 * @return string
1410
	 */
1411
	protected function makeInsertOptions( $options ) {
1412
		return implode( ' ', $options );
1413
	}
1414
1415
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1416
		# No rows to insert, easy just return now
1417
		if ( !count( $a ) ) {
1418
			return true;
1419
		}
1420
1421
		$table = $this->tableName( $table );
1422
1423
		if ( !is_array( $options ) ) {
1424
			$options = [ $options ];
1425
		}
1426
1427
		$fh = null;
1428
		if ( isset( $options['fileHandle'] ) ) {
1429
			$fh = $options['fileHandle'];
1430
		}
1431
		$options = $this->makeInsertOptions( $options );
1432
1433
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1434
			$multi = true;
1435
			$keys = array_keys( $a[0] );
1436
		} else {
1437
			$multi = false;
1438
			$keys = array_keys( $a );
1439
		}
1440
1441
		$sql = 'INSERT ' . $options .
1442
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1443
1444
		if ( $multi ) {
1445
			$first = true;
1446 View Code Duplication
			foreach ( $a as $row ) {
1447
				if ( $first ) {
1448
					$first = false;
1449
				} else {
1450
					$sql .= ',';
1451
				}
1452
				$sql .= '(' . $this->makeList( $row ) . ')';
1453
			}
1454
		} else {
1455
			$sql .= '(' . $this->makeList( $a ) . ')';
1456
		}
1457
1458
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1459
			return false;
1460
		} elseif ( $fh !== null ) {
1461
			return true;
1462
		}
1463
1464
		return (bool)$this->query( $sql, $fname );
1465
	}
1466
1467
	/**
1468
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1469
	 *
1470
	 * @param array $options
1471
	 * @return array
1472
	 */
1473
	protected function makeUpdateOptionsArray( $options ) {
1474
		if ( !is_array( $options ) ) {
1475
			$options = [ $options ];
1476
		}
1477
1478
		$opts = [];
1479
1480
		if ( in_array( 'LOW_PRIORITY', $options ) ) {
1481
			$opts[] = $this->lowPriorityOption();
1482
		}
1483
1484
		if ( in_array( 'IGNORE', $options ) ) {
1485
			$opts[] = 'IGNORE';
1486
		}
1487
1488
		return $opts;
1489
	}
1490
1491
	/**
1492
	 * Make UPDATE options for the DatabaseBase::update function
1493
	 *
1494
	 * @param array $options The options passed to DatabaseBase::update
1495
	 * @return string
1496
	 */
1497
	protected function makeUpdateOptions( $options ) {
1498
		$opts = $this->makeUpdateOptionsArray( $options );
1499
1500
		return implode( ' ', $opts );
1501
	}
1502
1503
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1504
		$table = $this->tableName( $table );
1505
		$opts = $this->makeUpdateOptions( $options );
1506
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1507
1508 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
1509
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1510
		}
1511
1512
		return $this->query( $sql, $fname );
1513
	}
1514
1515
	public function makeList( $a, $mode = LIST_COMMA ) {
1516
		if ( !is_array( $a ) ) {
1517
			throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1518
		}
1519
1520
		$first = true;
1521
		$list = '';
1522
1523
		foreach ( $a as $field => $value ) {
1524
			if ( !$first ) {
1525
				if ( $mode == LIST_AND ) {
1526
					$list .= ' AND ';
1527
				} elseif ( $mode == LIST_OR ) {
1528
					$list .= ' OR ';
1529
				} else {
1530
					$list .= ',';
1531
				}
1532
			} else {
1533
				$first = false;
1534
			}
1535
1536
			if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1537
				$list .= "($value)";
1538
			} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1539
				$list .= "$value";
1540
			} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1541
				// Remove null from array to be handled separately if found
1542
				$includeNull = false;
1543
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1544
					$includeNull = true;
1545
					unset( $value[$nullKey] );
1546
				}
1547
				if ( count( $value ) == 0 && !$includeNull ) {
1548
					throw new MWException( __METHOD__ . ": empty input for field $field" );
1549
				} elseif ( count( $value ) == 0 ) {
1550
					// only check if $field is null
1551
					$list .= "$field IS NULL";
1552
				} else {
1553
					// IN clause contains at least one valid element
1554
					if ( $includeNull ) {
1555
						// Group subconditions to ensure correct precedence
1556
						$list .= '(';
1557
					}
1558
					if ( count( $value ) == 1 ) {
1559
						// Special-case single values, as IN isn't terribly efficient
1560
						// Don't necessarily assume the single key is 0; we don't
1561
						// enforce linear numeric ordering on other arrays here.
1562
						$value = array_values( $value )[0];
1563
						$list .= $field . " = " . $this->addQuotes( $value );
1564
					} else {
1565
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1566
					}
1567
					// if null present in array, append IS NULL
1568
					if ( $includeNull ) {
1569
						$list .= " OR $field IS NULL)";
1570
					}
1571
				}
1572
			} elseif ( $value === null ) {
1573 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR ) {
1574
					$list .= "$field IS ";
1575
				} elseif ( $mode == LIST_SET ) {
1576
					$list .= "$field = ";
1577
				}
1578
				$list .= 'NULL';
1579
			} else {
1580 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1581
					$list .= "$field = ";
1582
				}
1583
				$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1584
			}
1585
		}
1586
1587
		return $list;
1588
	}
1589
1590
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1591
		$conds = [];
1592
1593
		foreach ( $data as $base => $sub ) {
1594
			if ( count( $sub ) ) {
1595
				$conds[] = $this->makeList(
1596
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1597
					LIST_AND );
1598
			}
1599
		}
1600
1601
		if ( $conds ) {
1602
			return $this->makeList( $conds, LIST_OR );
1603
		} else {
1604
			// Nothing to search for...
1605
			return false;
1606
		}
1607
	}
1608
1609
	/**
1610
	 * Return aggregated value alias
1611
	 *
1612
	 * @param array $valuedata
1613
	 * @param string $valuename
1614
	 *
1615
	 * @return string
1616
	 */
1617
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1618
		return $valuename;
1619
	}
1620
1621
	public function bitNot( $field ) {
1622
		return "(~$field)";
1623
	}
1624
1625
	public function bitAnd( $fieldLeft, $fieldRight ) {
1626
		return "($fieldLeft & $fieldRight)";
1627
	}
1628
1629
	public function bitOr( $fieldLeft, $fieldRight ) {
1630
		return "($fieldLeft | $fieldRight)";
1631
	}
1632
1633
	public function buildConcat( $stringList ) {
1634
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1635
	}
1636
1637 View Code Duplication
	public function buildGroupConcatField(
1638
		$delim, $table, $field, $conds = '', $join_conds = []
1639
	) {
1640
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1641
1642
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1643
	}
1644
1645
	public function selectDB( $db ) {
1646
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1647
		# if your database engine supports a concept similar to MySQL's
1648
		# databases you may as well.
1649
		$this->mDBname = $db;
1650
1651
		return true;
1652
	}
1653
1654
	public function getDBname() {
1655
		return $this->mDBname;
1656
	}
1657
1658
	public function getServer() {
1659
		return $this->mServer;
1660
	}
1661
1662
	/**
1663
	 * Format a table name ready for use in constructing an SQL query
1664
	 *
1665
	 * This does two important things: it quotes the table names to clean them up,
1666
	 * and it adds a table prefix if only given a table name with no quotes.
1667
	 *
1668
	 * All functions of this object which require a table name call this function
1669
	 * themselves. Pass the canonical name to such functions. This is only needed
1670
	 * when calling query() directly.
1671
	 *
1672
	 * @note This function does not sanitize user input. It is not safe to use
1673
	 *   this function to escape user input.
1674
	 * @param string $name Database table name
1675
	 * @param string $format One of:
1676
	 *   quoted - Automatically pass the table name through addIdentifierQuotes()
1677
	 *            so that it can be used in a query.
1678
	 *   raw - Do not add identifier quotes to the table name
1679
	 * @return string Full database name
1680
	 */
1681
	public function tableName( $name, $format = 'quoted' ) {
1682
		global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1683
		# Skip the entire process when we have a string quoted on both ends.
1684
		# Note that we check the end so that we will still quote any use of
1685
		# use of `database`.table. But won't break things if someone wants
1686
		# to query a database table with a dot in the name.
1687
		if ( $this->isQuotedIdentifier( $name ) ) {
1688
			return $name;
1689
		}
1690
1691
		# Lets test for any bits of text that should never show up in a table
1692
		# name. Basically anything like JOIN or ON which are actually part of
1693
		# SQL queries, but may end up inside of the table value to combine
1694
		# sql. Such as how the API is doing.
1695
		# Note that we use a whitespace test rather than a \b test to avoid
1696
		# any remote case where a word like on may be inside of a table name
1697
		# surrounded by symbols which may be considered word breaks.
1698
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1699
			return $name;
1700
		}
1701
1702
		# Split database and table into proper variables.
1703
		# We reverse the explode so that database.table and table both output
1704
		# the correct table.
1705
		$dbDetails = explode( '.', $name, 3 );
1706
		if ( count( $dbDetails ) == 3 ) {
1707
			list( $database, $schema, $table ) = $dbDetails;
1708
			# We don't want any prefix added in this case
1709
			$prefix = '';
1710
		} elseif ( count( $dbDetails ) == 2 ) {
1711
			list( $database, $table ) = $dbDetails;
1712
			# We don't want any prefix added in this case
1713
			# In dbs that support it, $database may actually be the schema
1714
			# but that doesn't affect any of the functionality here
1715
			$prefix = '';
1716
			$schema = null;
1717
		} else {
1718
			list( $table ) = $dbDetails;
1719
			if ( $wgSharedDB !== null # We have a shared database
1720
				&& $this->mForeign == false # We're not working on a foreign database
1721
				&& !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1722
				&& in_array( $table, $wgSharedTables ) # A shared table is selected
1723
			) {
1724
				$database = $wgSharedDB;
1725
				$schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1726
				$prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1727
			} else {
1728
				$database = null;
1729
				$schema = $this->mSchema; # Default schema
1730
				$prefix = $this->mTablePrefix; # Default prefix
1731
			}
1732
		}
1733
1734
		# Quote $table and apply the prefix if not quoted.
1735
		# $tableName might be empty if this is called from Database::replaceVars()
1736
		$tableName = "{$prefix}{$table}";
1737
		if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1738
			$tableName = $this->addIdentifierQuotes( $tableName );
1739
		}
1740
1741
		# Quote $schema and merge it with the table name if needed
1742 View Code Duplication
		if ( strlen( $schema ) ) {
1743
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1744
				$schema = $this->addIdentifierQuotes( $schema );
1745
			}
1746
			$tableName = $schema . '.' . $tableName;
1747
		}
1748
1749
		# Quote $database and merge it with the table name if needed
1750 View Code Duplication
		if ( $database !== null ) {
1751
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1752
				$database = $this->addIdentifierQuotes( $database );
1753
			}
1754
			$tableName = $database . '.' . $tableName;
1755
		}
1756
1757
		return $tableName;
1758
	}
1759
1760
	/**
1761
	 * Fetch a number of table names into an array
1762
	 * This is handy when you need to construct SQL for joins
1763
	 *
1764
	 * Example:
1765
	 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1766
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1767
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1768
	 *
1769
	 * @return array
1770
	 */
1771 View Code Duplication
	public function tableNames() {
1772
		$inArray = func_get_args();
1773
		$retVal = [];
1774
1775
		foreach ( $inArray as $name ) {
1776
			$retVal[$name] = $this->tableName( $name );
1777
		}
1778
1779
		return $retVal;
1780
	}
1781
1782
	/**
1783
	 * Fetch a number of table names into an zero-indexed numerical array
1784
	 * This is handy when you need to construct SQL for joins
1785
	 *
1786
	 * Example:
1787
	 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1788
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1789
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1790
	 *
1791
	 * @return array
1792
	 */
1793 View Code Duplication
	public function tableNamesN() {
1794
		$inArray = func_get_args();
1795
		$retVal = [];
1796
1797
		foreach ( $inArray as $name ) {
1798
			$retVal[] = $this->tableName( $name );
1799
		}
1800
1801
		return $retVal;
1802
	}
1803
1804
	/**
1805
	 * Get an aliased table name
1806
	 * e.g. tableName AS newTableName
1807
	 *
1808
	 * @param string $name Table name, see tableName()
1809
	 * @param string|bool $alias Alias (optional)
1810
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1811
	 */
1812
	public function tableNameWithAlias( $name, $alias = false ) {
1813
		if ( !$alias || $alias == $name ) {
1814
			return $this->tableName( $name );
1815
		} else {
1816
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1812 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...
1817
		}
1818
	}
1819
1820
	/**
1821
	 * Gets an array of aliased table names
1822
	 *
1823
	 * @param array $tables Array( [alias] => table )
1824
	 * @return string[] See tableNameWithAlias()
1825
	 */
1826
	public function tableNamesWithAlias( $tables ) {
1827
		$retval = [];
1828
		foreach ( $tables as $alias => $table ) {
1829
			if ( is_numeric( $alias ) ) {
1830
				$alias = $table;
1831
			}
1832
			$retval[] = $this->tableNameWithAlias( $table, $alias );
1833
		}
1834
1835
		return $retval;
1836
	}
1837
1838
	/**
1839
	 * Get an aliased field name
1840
	 * e.g. fieldName AS newFieldName
1841
	 *
1842
	 * @param string $name Field name
1843
	 * @param string|bool $alias Alias (optional)
1844
	 * @return string SQL name for aliased field. Will not alias a field to its own name
1845
	 */
1846
	public function fieldNameWithAlias( $name, $alias = false ) {
1847
		if ( !$alias || (string)$alias === (string)$name ) {
1848
			return $name;
1849
		} else {
1850
			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 1846 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...
1851
		}
1852
	}
1853
1854
	/**
1855
	 * Gets an array of aliased field names
1856
	 *
1857
	 * @param array $fields Array( [alias] => field )
1858
	 * @return string[] See fieldNameWithAlias()
1859
	 */
1860
	public function fieldNamesWithAlias( $fields ) {
1861
		$retval = [];
1862
		foreach ( $fields as $alias => $field ) {
1863
			if ( is_numeric( $alias ) ) {
1864
				$alias = $field;
1865
			}
1866
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
1867
		}
1868
1869
		return $retval;
1870
	}
1871
1872
	/**
1873
	 * Get the aliased table name clause for a FROM clause
1874
	 * which might have a JOIN and/or USE INDEX clause
1875
	 *
1876
	 * @param array $tables ( [alias] => table )
1877
	 * @param array $use_index Same as for select()
1878
	 * @param array $join_conds Same as for select()
1879
	 * @return string
1880
	 */
1881
	protected function tableNamesWithUseIndexOrJOIN(
1882
		$tables, $use_index = [], $join_conds = []
1883
	) {
1884
		$ret = [];
1885
		$retJOIN = [];
1886
		$use_index = (array)$use_index;
1887
		$join_conds = (array)$join_conds;
1888
1889
		foreach ( $tables as $alias => $table ) {
1890
			if ( !is_string( $alias ) ) {
1891
				// No alias? Set it equal to the table name
1892
				$alias = $table;
1893
			}
1894
			// Is there a JOIN clause for this table?
1895
			if ( isset( $join_conds[$alias] ) ) {
1896
				list( $joinType, $conds ) = $join_conds[$alias];
1897
				$tableClause = $joinType;
1898
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1899
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1900
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1901
					if ( $use != '' ) {
1902
						$tableClause .= ' ' . $use;
1903
					}
1904
				}
1905
				$on = $this->makeList( (array)$conds, LIST_AND );
1906
				if ( $on != '' ) {
1907
					$tableClause .= ' ON (' . $on . ')';
1908
				}
1909
1910
				$retJOIN[] = $tableClause;
1911
			} elseif ( isset( $use_index[$alias] ) ) {
1912
				// Is there an INDEX clause for this table?
1913
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1914
				$tableClause .= ' ' . $this->useIndexClause(
1915
					implode( ',', (array)$use_index[$alias] )
1916
				);
1917
1918
				$ret[] = $tableClause;
1919
			} else {
1920
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1921
1922
				$ret[] = $tableClause;
1923
			}
1924
		}
1925
1926
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
1927
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
1928
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
1929
1930
		// Compile our final table clause
1931
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
1932
	}
1933
1934
	/**
1935
	 * Get the name of an index in a given table.
1936
	 *
1937
	 * @param string $index
1938
	 * @return string
1939
	 */
1940
	protected function indexName( $index ) {
1941
		// Backwards-compatibility hack
1942
		$renamed = [
1943
			'ar_usertext_timestamp' => 'usertext_timestamp',
1944
			'un_user_id' => 'user_id',
1945
			'un_user_ip' => 'user_ip',
1946
		];
1947
1948
		if ( isset( $renamed[$index] ) ) {
1949
			return $renamed[$index];
1950
		} else {
1951
			return $index;
1952
		}
1953
	}
1954
1955
	public function addQuotes( $s ) {
1956
		if ( $s instanceof Blob ) {
1957
			$s = $s->fetch();
1958
		}
1959
		if ( $s === null ) {
1960
			return 'NULL';
1961
		} else {
1962
			# This will also quote numeric values. This should be harmless,
1963
			# and protects against weird problems that occur when they really
1964
			# _are_ strings such as article titles and string->number->string
1965
			# conversion is not 1:1.
1966
			return "'" . $this->strencode( $s ) . "'";
1967
		}
1968
	}
1969
1970
	/**
1971
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
1972
	 * MySQL uses `backticks` while basically everything else uses double quotes.
1973
	 * Since MySQL is the odd one out here the double quotes are our generic
1974
	 * and we implement backticks in DatabaseMysql.
1975
	 *
1976
	 * @param string $s
1977
	 * @return string
1978
	 */
1979
	public function addIdentifierQuotes( $s ) {
1980
		return '"' . str_replace( '"', '""', $s ) . '"';
1981
	}
1982
1983
	/**
1984
	 * Returns if the given identifier looks quoted or not according to
1985
	 * the database convention for quoting identifiers .
1986
	 *
1987
	 * @note Do not use this to determine if untrusted input is safe.
1988
	 *   A malicious user can trick this function.
1989
	 * @param string $name
1990
	 * @return bool
1991
	 */
1992
	public function isQuotedIdentifier( $name ) {
1993
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
1994
	}
1995
1996
	/**
1997
	 * @param string $s
1998
	 * @return string
1999
	 */
2000
	protected function escapeLikeInternal( $s ) {
2001
		return addcslashes( $s, '\%_' );
2002
	}
2003
2004
	public function buildLike() {
2005
		$params = func_get_args();
2006
2007
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2008
			$params = $params[0];
2009
		}
2010
2011
		$s = '';
2012
2013
		foreach ( $params as $value ) {
2014
			if ( $value instanceof LikeMatch ) {
2015
				$s .= $value->toString();
2016
			} else {
2017
				$s .= $this->escapeLikeInternal( $value );
2018
			}
2019
		}
2020
2021
		return " LIKE {$this->addQuotes( $s )} ";
2022
	}
2023
2024
	public function anyChar() {
2025
		return new LikeMatch( '_' );
2026
	}
2027
2028
	public function anyString() {
2029
		return new LikeMatch( '%' );
2030
	}
2031
2032
	public function nextSequenceValue( $seqName ) {
2033
		return null;
2034
	}
2035
2036
	/**
2037
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2038
	 * is only needed because a) MySQL must be as efficient as possible due to
2039
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2040
	 * which index to pick. Anyway, other databases might have different
2041
	 * indexes on a given table. So don't bother overriding this unless you're
2042
	 * MySQL.
2043
	 * @param string $index
2044
	 * @return string
2045
	 */
2046
	public function useIndexClause( $index ) {
2047
		return '';
2048
	}
2049
2050
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2051
		$quotedTable = $this->tableName( $table );
2052
2053
		if ( count( $rows ) == 0 ) {
2054
			return;
2055
		}
2056
2057
		# Single row case
2058
		if ( !is_array( reset( $rows ) ) ) {
2059
			$rows = [ $rows ];
2060
		}
2061
2062
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2063
		foreach ( $rows as $row ) {
2064
			# Delete rows which collide
2065
			if ( $uniqueIndexes ) {
2066
				$sql = "DELETE FROM $quotedTable WHERE ";
2067
				$first = true;
2068
				foreach ( $uniqueIndexes as $index ) {
2069
					if ( $first ) {
2070
						$first = false;
2071
						$sql .= '( ';
2072
					} else {
2073
						$sql .= ' ) OR ( ';
2074
					}
2075
					if ( is_array( $index ) ) {
2076
						$first2 = true;
2077
						foreach ( $index as $col ) {
2078
							if ( $first2 ) {
2079
								$first2 = false;
2080
							} else {
2081
								$sql .= ' AND ';
2082
							}
2083
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2084
						}
2085
					} else {
2086
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2087
					}
2088
				}
2089
				$sql .= ' )';
2090
				$this->query( $sql, $fname );
2091
			}
2092
2093
			# Now insert the row
2094
			$this->insert( $table, $row, $fname );
2095
		}
2096
	}
2097
2098
	/**
2099
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2100
	 * statement.
2101
	 *
2102
	 * @param string $table Table name
2103
	 * @param array|string $rows Row(s) to insert
2104
	 * @param string $fname Caller function name
2105
	 *
2106
	 * @return ResultWrapper
2107
	 */
2108
	protected function nativeReplace( $table, $rows, $fname ) {
2109
		$table = $this->tableName( $table );
2110
2111
		# Single row case
2112
		if ( !is_array( reset( $rows ) ) ) {
2113
			$rows = [ $rows ];
2114
		}
2115
2116
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2117
		$first = true;
2118
2119 View Code Duplication
		foreach ( $rows as $row ) {
2120
			if ( $first ) {
2121
				$first = false;
2122
			} else {
2123
				$sql .= ',';
2124
			}
2125
2126
			$sql .= '(' . $this->makeList( $row ) . ')';
2127
		}
2128
2129
		return $this->query( $sql, $fname );
2130
	}
2131
2132
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2133
		$fname = __METHOD__
2134
	) {
2135
		if ( !count( $rows ) ) {
2136
			return true; // nothing to do
2137
		}
2138
2139
		if ( !is_array( reset( $rows ) ) ) {
2140
			$rows = [ $rows ];
2141
		}
2142
2143
		if ( count( $uniqueIndexes ) ) {
2144
			$clauses = []; // list WHERE clauses that each identify a single row
2145
			foreach ( $rows as $row ) {
2146
				foreach ( $uniqueIndexes as $index ) {
2147
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2148
					$rowKey = []; // unique key to this row
2149
					foreach ( $index as $column ) {
2150
						$rowKey[$column] = $row[$column];
2151
					}
2152
					$clauses[] = $this->makeList( $rowKey, LIST_AND );
2153
				}
2154
			}
2155
			$where = [ $this->makeList( $clauses, LIST_OR ) ];
2156
		} else {
2157
			$where = false;
2158
		}
2159
2160
		$useTrx = !$this->mTrxLevel;
2161
		if ( $useTrx ) {
2162
			$this->begin( $fname );
2163
		}
2164
		try {
2165
			# Update any existing conflicting row(s)
2166
			if ( $where !== false ) {
2167
				$ok = $this->update( $table, $set, $where, $fname );
2168
			} else {
2169
				$ok = true;
2170
			}
2171
			# Now insert any non-conflicting row(s)
2172
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2173
		} catch ( Exception $e ) {
2174
			if ( $useTrx ) {
2175
				$this->rollback( $fname );
2176
			}
2177
			throw $e;
2178
		}
2179
		if ( $useTrx ) {
2180
			$this->commit( $fname );
2181
		}
2182
2183
		return $ok;
2184
	}
2185
2186 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2187
		$fname = __METHOD__
2188
	) {
2189
		if ( !$conds ) {
2190
			throw new DBUnexpectedError( $this,
2191
				'DatabaseBase::deleteJoin() called with empty $conds' );
2192
		}
2193
2194
		$delTable = $this->tableName( $delTable );
2195
		$joinTable = $this->tableName( $joinTable );
2196
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2197
		if ( $conds != '*' ) {
2198
			$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2199
		}
2200
		$sql .= ')';
2201
2202
		$this->query( $sql, $fname );
2203
	}
2204
2205
	/**
2206
	 * Returns the size of a text field, or -1 for "unlimited"
2207
	 *
2208
	 * @param string $table
2209
	 * @param string $field
2210
	 * @return int
2211
	 */
2212
	public function textFieldSize( $table, $field ) {
2213
		$table = $this->tableName( $table );
2214
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2215
		$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2216
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, 'DatabaseBase::textFieldSize') on line 2215 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...
2217
2218
		$m = [];
2219
2220
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2221
			$size = $m[1];
2222
		} else {
2223
			$size = -1;
2224
		}
2225
2226
		return $size;
2227
	}
2228
2229
	/**
2230
	 * A string to insert into queries to show that they're low-priority, like
2231
	 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2232
	 * string and nothing bad should happen.
2233
	 *
2234
	 * @return string Returns the text of the low priority option if it is
2235
	 *   supported, or a blank string otherwise
2236
	 */
2237
	public function lowPriorityOption() {
2238
		return '';
2239
	}
2240
2241
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2242
		if ( !$conds ) {
2243
			throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2244
		}
2245
2246
		$table = $this->tableName( $table );
2247
		$sql = "DELETE FROM $table";
2248
2249 View Code Duplication
		if ( $conds != '*' ) {
2250
			if ( is_array( $conds ) ) {
2251
				$conds = $this->makeList( $conds, LIST_AND );
2252
			}
2253
			$sql .= ' WHERE ' . $conds;
2254
		}
2255
2256
		return $this->query( $sql, $fname );
2257
	}
2258
2259
	public function insertSelect( $destTable, $srcTable, $varMap, $conds,
2260
		$fname = __METHOD__,
2261
		$insertOptions = [], $selectOptions = []
2262
	) {
2263
		$destTable = $this->tableName( $destTable );
2264
2265
		if ( !is_array( $insertOptions ) ) {
2266
			$insertOptions = [ $insertOptions ];
2267
		}
2268
2269
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2270
2271
		if ( !is_array( $selectOptions ) ) {
2272
			$selectOptions = [ $selectOptions ];
2273
		}
2274
2275
		list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
2276
2277 View Code Duplication
		if ( is_array( $srcTable ) ) {
2278
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2279
		} else {
2280
			$srcTable = $this->tableName( $srcTable );
2281
		}
2282
2283
		$sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2284
			" SELECT $startOpts " . implode( ',', $varMap ) .
2285
			" FROM $srcTable $useIndex ";
2286
2287 View Code Duplication
		if ( $conds != '*' ) {
2288
			if ( is_array( $conds ) ) {
2289
				$conds = $this->makeList( $conds, LIST_AND );
2290
			}
2291
			$sql .= " WHERE $conds";
2292
		}
2293
2294
		$sql .= " $tailOpts";
2295
2296
		return $this->query( $sql, $fname );
2297
	}
2298
2299
	/**
2300
	 * Construct a LIMIT query with optional offset. This is used for query
2301
	 * pages. The SQL should be adjusted so that only the first $limit rows
2302
	 * are returned. If $offset is provided as well, then the first $offset
2303
	 * rows should be discarded, and the next $limit rows should be returned.
2304
	 * If the result of the query is not ordered, then the rows to be returned
2305
	 * are theoretically arbitrary.
2306
	 *
2307
	 * $sql is expected to be a SELECT, if that makes a difference.
2308
	 *
2309
	 * The version provided by default works in MySQL and SQLite. It will very
2310
	 * likely need to be overridden for most other DBMSes.
2311
	 *
2312
	 * @param string $sql SQL query we will append the limit too
2313
	 * @param int $limit The SQL limit
2314
	 * @param int|bool $offset The SQL offset (default false)
2315
	 * @throws DBUnexpectedError
2316
	 * @return string
2317
	 */
2318
	public function limitResult( $sql, $limit, $offset = false ) {
2319
		if ( !is_numeric( $limit ) ) {
2320
			throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2321
		}
2322
2323
		return "$sql LIMIT "
2324
			. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2325
			. "{$limit} ";
2326
	}
2327
2328
	public function unionSupportsOrderAndLimit() {
2329
		return true; // True for almost every DB supported
2330
	}
2331
2332
	public function unionQueries( $sqls, $all ) {
2333
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2334
2335
		return '(' . implode( $glue, $sqls ) . ')';
2336
	}
2337
2338
	public function conditional( $cond, $trueVal, $falseVal ) {
2339
		if ( is_array( $cond ) ) {
2340
			$cond = $this->makeList( $cond, LIST_AND );
2341
		}
2342
2343
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2344
	}
2345
2346
	public function strreplace( $orig, $old, $new ) {
2347
		return "REPLACE({$orig}, {$old}, {$new})";
2348
	}
2349
2350
	public function getServerUptime() {
2351
		return 0;
2352
	}
2353
2354
	public function wasDeadlock() {
2355
		return false;
2356
	}
2357
2358
	public function wasLockTimeout() {
2359
		return false;
2360
	}
2361
2362
	public function wasErrorReissuable() {
2363
		return false;
2364
	}
2365
2366
	public function wasReadOnlyError() {
2367
		return false;
2368
	}
2369
2370
	/**
2371
	 * Determines if the given query error was a connection drop
2372
	 * STUB
2373
	 *
2374
	 * @param integer|string $errno
2375
	 * @return bool
2376
	 */
2377
	public function wasConnectionError( $errno ) {
2378
		return false;
2379
	}
2380
2381
	/**
2382
	 * Perform a deadlock-prone transaction.
2383
	 *
2384
	 * This function invokes a callback function to perform a set of write
2385
	 * queries. If a deadlock occurs during the processing, the transaction
2386
	 * will be rolled back and the callback function will be called again.
2387
	 *
2388
	 * Usage:
2389
	 *   $dbw->deadlockLoop( callback, ... );
2390
	 *
2391
	 * Extra arguments are passed through to the specified callback function.
2392
	 *
2393
	 * Returns whatever the callback function returned on its successful,
2394
	 * iteration, or false on error, for example if the retry limit was
2395
	 * reached.
2396
	 * @return mixed
2397
	 * @throws DBUnexpectedError
2398
	 * @throws Exception
2399
	 */
2400
	public function deadlockLoop() {
2401
		$args = func_get_args();
2402
		$function = array_shift( $args );
2403
		$tries = self::DEADLOCK_TRIES;
2404
2405
		$this->begin( __METHOD__ );
2406
2407
		$retVal = null;
2408
		/** @var Exception $e */
2409
		$e = null;
2410
		do {
2411
			try {
2412
				$retVal = call_user_func_array( $function, $args );
2413
				break;
2414
			} catch ( DBQueryError $e ) {
2415
				if ( $this->wasDeadlock() ) {
2416
					// Retry after a randomized delay
2417
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2418
				} else {
2419
					// Throw the error back up
2420
					throw $e;
2421
				}
2422
			}
2423
		} while ( --$tries > 0 );
2424
2425
		if ( $tries <= 0 ) {
2426
			// Too many deadlocks; give up
2427
			$this->rollback( __METHOD__ );
2428
			throw $e;
2429
		} else {
2430
			$this->commit( __METHOD__ );
2431
2432
			return $retVal;
2433
		}
2434
	}
2435
2436
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2437
		# Real waits are implemented in the subclass.
2438
		return 0;
2439
	}
2440
2441
	public function getSlavePos() {
2442
		# Stub
2443
		return false;
2444
	}
2445
2446
	public function getMasterPos() {
2447
		# Stub
2448
		return false;
2449
	}
2450
2451
	final public function onTransactionIdle( $callback ) {
2452
		$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2453
		if ( !$this->mTrxLevel ) {
2454
			$this->runOnTransactionIdleCallbacks();
2455
		}
2456
	}
2457
2458
	final public function onTransactionPreCommitOrIdle( $callback ) {
2459
		if ( $this->mTrxLevel ) {
2460
			$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2461
		} else {
2462
			$this->onTransactionIdle( $callback ); // this will trigger immediately
2463
		}
2464
	}
2465
2466
	/**
2467
	 * Actually any "on transaction idle" callbacks.
2468
	 *
2469
	 * @since 1.20
2470
	 */
2471
	protected function runOnTransactionIdleCallbacks() {
2472
		$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2473
2474
		$e = $ePrior = null; // last exception
2475
		do { // callbacks may add callbacks :)
2476
			$callbacks = $this->mTrxIdleCallbacks;
2477
			$this->mTrxIdleCallbacks = []; // recursion guard
2478
			foreach ( $callbacks as $callback ) {
2479
				try {
2480
					list( $phpCallback ) = $callback;
2481
					$this->clearFlag( DBO_TRX ); // make each query its own transaction
2482
					call_user_func( $phpCallback );
2483
					if ( $autoTrx ) {
2484
						$this->setFlag( DBO_TRX ); // restore automatic begin()
2485
					} else {
2486
						$this->clearFlag( DBO_TRX ); // restore auto-commit
2487
					}
2488
				} catch ( Exception $e ) {
2489
					if ( $ePrior ) {
2490
						MWExceptionHandler::logException( $ePrior );
2491
					}
2492
					$ePrior = $e;
2493
					// Some callbacks may use startAtomic/endAtomic, so make sure
2494
					// their transactions are ended so other callbacks don't fail
2495
					if ( $this->trxLevel() ) {
2496
						$this->rollback( __METHOD__ );
2497
					}
2498
				}
2499
			}
2500
		} while ( count( $this->mTrxIdleCallbacks ) );
2501
2502
		if ( $e instanceof Exception ) {
2503
			throw $e; // re-throw any last exception
2504
		}
2505
	}
2506
2507
	/**
2508
	 * Actually any "on transaction pre-commit" callbacks.
2509
	 *
2510
	 * @since 1.22
2511
	 */
2512
	protected function runOnTransactionPreCommitCallbacks() {
2513
		$e = $ePrior = null; // last exception
2514
		do { // callbacks may add callbacks :)
2515
			$callbacks = $this->mTrxPreCommitCallbacks;
2516
			$this->mTrxPreCommitCallbacks = []; // recursion guard
2517
			foreach ( $callbacks as $callback ) {
2518
				try {
2519
					list( $phpCallback ) = $callback;
2520
					call_user_func( $phpCallback );
2521
				} catch ( Exception $e ) {
2522
					if ( $ePrior ) {
2523
						MWExceptionHandler::logException( $ePrior );
2524
					}
2525
					$ePrior = $e;
2526
				}
2527
			}
2528
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2529
2530
		if ( $e instanceof Exception ) {
2531
			throw $e; // re-throw any last exception
2532
		}
2533
	}
2534
2535
	final public function startAtomic( $fname = __METHOD__ ) {
2536
		if ( !$this->mTrxLevel ) {
2537
			$this->begin( $fname );
2538
			$this->mTrxAutomatic = true;
2539
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2540
			// in all changes being in one transaction to keep requests transactional.
2541
			if ( !$this->getFlag( DBO_TRX ) ) {
2542
				$this->mTrxAutomaticAtomic = true;
2543
			}
2544
		}
2545
2546
		$this->mTrxAtomicLevels[] = $fname;
2547
	}
2548
2549
	final public function endAtomic( $fname = __METHOD__ ) {
2550
		if ( !$this->mTrxLevel ) {
2551
			throw new DBUnexpectedError( $this, 'No atomic transaction is open.' );
2552
		}
2553
		if ( !$this->mTrxAtomicLevels ||
2554
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2555
		) {
2556
			throw new DBUnexpectedError( $this, 'Invalid atomic section ended.' );
2557
		}
2558
2559
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2560
			$this->commit( $fname, 'flush' );
2561
		}
2562
	}
2563
2564
	final public function doAtomicSection( $fname, callable $callback ) {
2565
		$this->startAtomic( $fname );
2566
		try {
2567
			call_user_func_array( $callback, [ $this, $fname ] );
2568
		} catch ( Exception $e ) {
2569
			$this->rollback( $fname );
2570
			throw $e;
2571
		}
2572
		$this->endAtomic( $fname );
2573
	}
2574
2575
	final public function begin( $fname = __METHOD__ ) {
2576
		if ( $this->mTrxLevel ) { // implicit commit
2577
			if ( $this->mTrxAtomicLevels ) {
2578
				// If the current transaction was an automatic atomic one, then we definitely have
2579
				// a problem. Same if there is any unclosed atomic level.
2580
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2581
				throw new DBUnexpectedError(
2582
					$this,
2583
					"Got explicit BEGIN from $fname while atomic section(s) $levels are open."
2584
				);
2585
			} elseif ( !$this->mTrxAutomatic ) {
2586
				// We want to warn about inadvertently nested begin/commit pairs, but not about
2587
				// auto-committing implicit transactions that were started by query() via DBO_TRX
2588
				throw new DBUnexpectedError(
2589
					$this,
2590
					"$fname: Transaction already in progress (from {$this->mTrxFname}), " .
2591
						" performing implicit commit!"
2592
				);
2593
			} else {
2594
				// The transaction was automatic and has done write operations
2595
				if ( $this->mTrxDoneWrites ) {
2596
					wfDebug( "$fname: Automatic transaction with writes in progress" .
2597
						" (from {$this->mTrxFname}), performing implicit commit!\n"
2598
					);
2599
				}
2600
			}
2601
2602
			$this->runOnTransactionPreCommitCallbacks();
2603
			$writeTime = $this->pendingWriteQueryDuration();
2604
			$this->doCommit( $fname );
2605 View Code Duplication
			if ( $this->mTrxDoneWrites ) {
2606
				$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...
2607
				$this->getTransactionProfiler()->transactionWritingOut(
2608
					$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 2603 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...
2609
			}
2610
			$this->runOnTransactionIdleCallbacks();
2611
		}
2612
2613
		# Avoid fatals if close() was called
2614
		$this->assertOpen();
2615
2616
		$this->doBegin( $fname );
2617
		$this->mTrxTimestamp = microtime( true );
2618
		$this->mTrxFname = $fname;
2619
		$this->mTrxDoneWrites = false;
2620
		$this->mTrxAutomatic = false;
2621
		$this->mTrxAutomaticAtomic = false;
2622
		$this->mTrxAtomicLevels = [];
2623
		$this->mTrxIdleCallbacks = [];
2624
		$this->mTrxPreCommitCallbacks = [];
2625
		$this->mTrxShortId = wfRandomString( 12 );
2626
		$this->mTrxWriteDuration = 0.0;
2627
		$this->mTrxWriteCallers = [];
2628
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2629
		// Get an estimate of the slave lag before then, treating estimate staleness
2630
		// as lag itself just to be safe
2631
		$status = $this->getApproximateLagStatus();
2632
		$this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
2633
	}
2634
2635
	/**
2636
	 * Issues the BEGIN command to the database server.
2637
	 *
2638
	 * @see DatabaseBase::begin()
2639
	 * @param string $fname
2640
	 */
2641
	protected function doBegin( $fname ) {
2642
		$this->query( 'BEGIN', $fname );
2643
		$this->mTrxLevel = 1;
2644
	}
2645
2646
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2647
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2648
			// There are still atomic sections open. This cannot be ignored
2649
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2650
			throw new DBUnexpectedError(
2651
				$this,
2652
				"Got COMMIT while atomic sections $levels are still open"
2653
			);
2654
		}
2655
2656
		if ( $flush === 'flush' ) {
2657
			if ( !$this->mTrxLevel ) {
2658
				return; // nothing to do
2659
			} elseif ( !$this->mTrxAutomatic ) {
2660
				throw new DBUnexpectedError(
2661
					$this,
2662
					"$fname: Flushing an explicit transaction, getting out of sync!"
2663
				);
2664
			}
2665
		} else {
2666
			if ( !$this->mTrxLevel ) {
2667
				wfWarn( "$fname: No transaction to commit, something got out of sync!" );
2668
				return; // nothing to do
2669
			} elseif ( $this->mTrxAutomatic ) {
2670
				wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" );
2671
			}
2672
		}
2673
2674
		# Avoid fatals if close() was called
2675
		$this->assertOpen();
2676
2677
		$this->runOnTransactionPreCommitCallbacks();
2678
		$writeTime = $this->pendingWriteQueryDuration();
2679
		$this->doCommit( $fname );
2680 View Code Duplication
		if ( $this->mTrxDoneWrites ) {
2681
			$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...
2682
			$this->getTransactionProfiler()->transactionWritingOut(
2683
				$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 2678 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...
2684
		}
2685
		$this->runOnTransactionIdleCallbacks();
2686
	}
2687
2688
	/**
2689
	 * Issues the COMMIT command to the database server.
2690
	 *
2691
	 * @see DatabaseBase::commit()
2692
	 * @param string $fname
2693
	 */
2694
	protected function doCommit( $fname ) {
2695
		if ( $this->mTrxLevel ) {
2696
			$this->query( 'COMMIT', $fname );
2697
			$this->mTrxLevel = 0;
2698
		}
2699
	}
2700
2701
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
2702
		if ( $flush !== 'flush' ) {
2703
			if ( !$this->mTrxLevel ) {
2704
				wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
2705
				return; // nothing to do
2706
			}
2707
		} else {
2708
			if ( !$this->mTrxLevel ) {
2709
				return; // nothing to do
2710
			}
2711
		}
2712
2713
		# Avoid fatals if close() was called
2714
		$this->assertOpen();
2715
2716
		$this->doRollback( $fname );
2717
		$this->mTrxIdleCallbacks = []; // cancel
2718
		$this->mTrxPreCommitCallbacks = []; // cancel
2719
		$this->mTrxAtomicLevels = [];
2720
		if ( $this->mTrxDoneWrites ) {
2721
			$this->getTransactionProfiler()->transactionWritingOut(
2722
				$this->mServer, $this->mDBname, $this->mTrxShortId );
2723
		}
2724
	}
2725
2726
	/**
2727
	 * Issues the ROLLBACK command to the database server.
2728
	 *
2729
	 * @see DatabaseBase::rollback()
2730
	 * @param string $fname
2731
	 */
2732
	protected function doRollback( $fname ) {
2733
		if ( $this->mTrxLevel ) {
2734
			$this->query( 'ROLLBACK', $fname, true );
2735
			$this->mTrxLevel = 0;
2736
		}
2737
	}
2738
2739
	/**
2740
	 * Creates a new table with structure copied from existing table
2741
	 * Note that unlike most database abstraction functions, this function does not
2742
	 * automatically append database prefix, because it works at a lower
2743
	 * abstraction level.
2744
	 * The table names passed to this function shall not be quoted (this
2745
	 * function calls addIdentifierQuotes when needed).
2746
	 *
2747
	 * @param string $oldName Name of table whose structure should be copied
2748
	 * @param string $newName Name of table to be created
2749
	 * @param bool $temporary Whether the new table should be temporary
2750
	 * @param string $fname Calling function name
2751
	 * @throws MWException
2752
	 * @return bool True if operation was successful
2753
	 */
2754
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
2755
		$fname = __METHOD__
2756
	) {
2757
		throw new MWException(
2758
			'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
2759
	}
2760
2761
	function listTables( $prefix = null, $fname = __METHOD__ ) {
2762
		throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
2763
	}
2764
2765
	/**
2766
	 * Reset the views process cache set by listViews()
2767
	 * @since 1.22
2768
	 */
2769
	final public function clearViewsCache() {
2770
		$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...
2771
	}
2772
2773
	/**
2774
	 * Lists all the VIEWs in the database
2775
	 *
2776
	 * For caching purposes the list of all views should be stored in
2777
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
2778
	 *
2779
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
2780
	 * @param string $fname Name of calling function
2781
	 * @throws MWException
2782
	 * @return array
2783
	 * @since 1.22
2784
	 */
2785
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
2786
		throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
2787
	}
2788
2789
	/**
2790
	 * Differentiates between a TABLE and a VIEW
2791
	 *
2792
	 * @param string $name Name of the database-structure to test.
2793
	 * @throws MWException
2794
	 * @return bool
2795
	 * @since 1.22
2796
	 */
2797
	public function isView( $name ) {
2798
		throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
2799
	}
2800
2801
	public function timestamp( $ts = 0 ) {
2802
		return wfTimestamp( TS_MW, $ts );
2803
	}
2804
2805
	public function timestampOrNull( $ts = null ) {
2806
		if ( is_null( $ts ) ) {
2807
			return null;
2808
		} else {
2809
			return $this->timestamp( $ts );
2810
		}
2811
	}
2812
2813
	/**
2814
	 * Take the result from a query, and wrap it in a ResultWrapper if
2815
	 * necessary. Boolean values are passed through as is, to indicate success
2816
	 * of write queries or failure.
2817
	 *
2818
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
2819
	 * resource, and it was necessary to call this function to convert it to
2820
	 * a wrapper. Nowadays, raw database objects are never exposed to external
2821
	 * callers, so this is unnecessary in external code.
2822
	 *
2823
	 * @param bool|ResultWrapper|resource|object $result
2824
	 * @return bool|ResultWrapper
2825
	 */
2826
	protected function resultObject( $result ) {
2827
		if ( !$result ) {
2828
			return false;
2829
		} elseif ( $result instanceof ResultWrapper ) {
2830
			return $result;
2831
		} elseif ( $result === true ) {
2832
			// Successful write query
2833
			return $result;
2834
		} else {
2835
			return new ResultWrapper( $this, $result );
2836
		}
2837
	}
2838
2839
	public function ping() {
2840
		# Stub. Not essential to override.
2841
		return true;
2842
	}
2843
2844
	public function getSessionLagStatus() {
2845
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
2846
	}
2847
2848
	/**
2849
	 * Get the slave lag when the current transaction started
2850
	 *
2851
	 * This is useful when transactions might use snapshot isolation
2852
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
2853
	 * is this lag plus transaction duration. If they don't, it is still
2854
	 * safe to be pessimistic. This returns null if there is no transaction.
2855
	 *
2856
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
2857
	 * @since 1.27
2858
	 */
2859
	public function getTransactionLagStatus() {
2860
		return $this->mTrxLevel
2861
			? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
2862
			: null;
2863
	}
2864
2865
	/**
2866
	 * Get a slave lag estimate for this server
2867
	 *
2868
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
2869
	 * @since 1.27
2870
	 */
2871
	public function getApproximateLagStatus() {
2872
		return [
2873
			'lag'   => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
2874
			'since' => microtime( true )
2875
		];
2876
	}
2877
2878
	/**
2879
	 * Merge the result of getSessionLagStatus() for several DBs
2880
	 * using the most pessimistic values to estimate the lag of
2881
	 * any data derived from them in combination
2882
	 *
2883
	 * This is information is useful for caching modules
2884
	 *
2885
	 * @see WANObjectCache::set()
2886
	 * @see WANObjectCache::getWithSetCallback()
2887
	 *
2888
	 * @param IDatabase $db1
2889
	 * @param IDatabase ...
2890
	 * @return array Map of values:
2891
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
2892
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
2893
	 *   - pending: whether any of the DBs have uncommitted changes
2894
	 * @since 1.27
2895
	 */
2896
	public static function getCacheSetOptions( IDatabase $db1 ) {
2897
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
2898
		foreach ( func_get_args() as $db ) {
2899
			/** @var IDatabase $db */
2900
			$status = $db->getSessionLagStatus();
2901
			if ( $status['lag'] === false ) {
2902
				$res['lag'] = false;
2903
			} elseif ( $res['lag'] !== false ) {
2904
				$res['lag'] = max( $res['lag'], $status['lag'] );
2905
			}
2906
			$res['since'] = min( $res['since'], $status['since'] );
2907
			$res['pending'] = $res['pending'] ?: $db->writesPending();
2908
		}
2909
2910
		return $res;
2911
	}
2912
2913
	public function getLag() {
2914
		return 0;
2915
	}
2916
2917
	function maxListLen() {
2918
		return 0;
2919
	}
2920
2921
	public function encodeBlob( $b ) {
2922
		return $b;
2923
	}
2924
2925
	public function decodeBlob( $b ) {
2926
		if ( $b instanceof Blob ) {
2927
			$b = $b->fetch();
2928
		}
2929
		return $b;
2930
	}
2931
2932
	public function setSessionOptions( array $options ) {
2933
	}
2934
2935
	/**
2936
	 * Read and execute SQL commands from a file.
2937
	 *
2938
	 * Returns true on success, error string or exception on failure (depending
2939
	 * on object's error ignore settings).
2940
	 *
2941
	 * @param string $filename File name to open
2942
	 * @param bool|callable $lineCallback Optional function called before reading each line
2943
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
2944
	 * @param bool|string $fname Calling function name or false if name should be
2945
	 *   generated dynamically using $filename
2946
	 * @param bool|callable $inputCallback Optional function called for each
2947
	 *   complete line sent
2948
	 * @throws Exception|MWException
2949
	 * @return bool|string
2950
	 */
2951
	public function sourceFile(
2952
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
2953
	) {
2954
		MediaWiki\suppressWarnings();
2955
		$fp = fopen( $filename, 'r' );
2956
		MediaWiki\restoreWarnings();
2957
2958
		if ( false === $fp ) {
2959
			throw new MWException( "Could not open \"{$filename}\".\n" );
2960
		}
2961
2962
		if ( !$fname ) {
2963
			$fname = __METHOD__ . "( $filename )";
2964
		}
2965
2966
		try {
2967
			$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 2952 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...
2968
		} catch ( Exception $e ) {
2969
			fclose( $fp );
2970
			throw $e;
2971
		}
2972
2973
		fclose( $fp );
2974
2975
		return $error;
2976
	}
2977
2978
	/**
2979
	 * Get the full path of a patch file. Originally based on archive()
2980
	 * from updaters.inc. Keep in mind this always returns a patch, as
2981
	 * it fails back to MySQL if no DB-specific patch can be found
2982
	 *
2983
	 * @param string $patch The name of the patch, like patch-something.sql
2984
	 * @return string Full path to patch file
2985
	 */
2986 View Code Duplication
	public function patchPath( $patch ) {
2987
		global $IP;
2988
2989
		$dbType = $this->getType();
2990
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
2991
			return "$IP/maintenance/$dbType/archives/$patch";
2992
		} else {
2993
			return "$IP/maintenance/archives/$patch";
2994
		}
2995
	}
2996
2997
	public function setSchemaVars( $vars ) {
2998
		$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...
2999
	}
3000
3001
	/**
3002
	 * Read and execute commands from an open file handle.
3003
	 *
3004
	 * Returns true on success, error string or exception on failure (depending
3005
	 * on object's error ignore settings).
3006
	 *
3007
	 * @param resource $fp File handle
3008
	 * @param bool|callable $lineCallback Optional function called before reading each query
3009
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3010
	 * @param string $fname Calling function name
3011
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3012
	 * @return bool|string
3013
	 */
3014
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3015
		$fname = __METHOD__, $inputCallback = false
3016
	) {
3017
		$cmd = '';
3018
3019
		while ( !feof( $fp ) ) {
3020
			if ( $lineCallback ) {
3021
				call_user_func( $lineCallback );
3022
			}
3023
3024
			$line = trim( fgets( $fp ) );
3025
3026
			if ( $line == '' ) {
3027
				continue;
3028
			}
3029
3030
			if ( '-' == $line[0] && '-' == $line[1] ) {
3031
				continue;
3032
			}
3033
3034
			if ( $cmd != '' ) {
3035
				$cmd .= ' ';
3036
			}
3037
3038
			$done = $this->streamStatementEnd( $cmd, $line );
3039
3040
			$cmd .= "$line\n";
3041
3042
			if ( $done || feof( $fp ) ) {
3043
				$cmd = $this->replaceVars( $cmd );
3044
3045
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3046
					$res = $this->query( $cmd, $fname );
3047
3048
					if ( $resultCallback ) {
3049
						call_user_func( $resultCallback, $res, $this );
3050
					}
3051
3052
					if ( false === $res ) {
3053
						$err = $this->lastError();
3054
3055
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3056
					}
3057
				}
3058
				$cmd = '';
3059
			}
3060
		}
3061
3062
		return true;
3063
	}
3064
3065
	/**
3066
	 * Called by sourceStream() to check if we've reached a statement end
3067
	 *
3068
	 * @param string $sql SQL assembled so far
3069
	 * @param string $newLine New line about to be added to $sql
3070
	 * @return bool Whether $newLine contains end of the statement
3071
	 */
3072
	public function streamStatementEnd( &$sql, &$newLine ) {
3073
		if ( $this->delimiter ) {
3074
			$prev = $newLine;
3075
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3076
			if ( $newLine != $prev ) {
3077
				return true;
3078
			}
3079
		}
3080
3081
		return false;
3082
	}
3083
3084
	/**
3085
	 * Database independent variable replacement. Replaces a set of variables
3086
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3087
	 *
3088
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3089
	 *
3090
	 * - '{$var}' should be used for text and is passed through the database's
3091
	 *   addQuotes method.
3092
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3093
	 *   It is passed through the database's addIdentifierQuotes method which
3094
	 *   can be overridden if the database uses something other than backticks.
3095
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3096
	 *   database's tableName method.
3097
	 * - / *i* / passes the name that follows through the database's indexName method.
3098
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3099
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3100
	 *
3101
	 * @param string $ins SQL statement to replace variables in
3102
	 * @return string The new SQL statement with variables replaced
3103
	 */
3104
	protected function replaceVars( $ins ) {
3105
		$vars = $this->getSchemaVars();
3106
		return preg_replace_callback(
3107
			'!
3108
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3109
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3110
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3111
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3112
			!x',
3113
			function ( $m ) use ( $vars ) {
3114
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3115
				// check for both nonexistent keys *and* the empty string.
3116
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3117
					if ( $m[1] === 'i' ) {
3118
						return $this->indexName( $m[2] );
3119
					} else {
3120
						return $this->tableName( $m[2] );
3121
					}
3122 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3123
					return $this->addQuotes( $vars[$m[3]] );
3124
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3125
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3126 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3127
					return $vars[$m[5]];
3128
				} else {
3129
					return $m[0];
3130
				}
3131
			},
3132
			$ins
3133
		);
3134
	}
3135
3136
	/**
3137
	 * Get schema variables. If none have been set via setSchemaVars(), then
3138
	 * use some defaults from the current object.
3139
	 *
3140
	 * @return array
3141
	 */
3142
	protected function getSchemaVars() {
3143
		if ( $this->mSchemaVars ) {
3144
			return $this->mSchemaVars;
3145
		} else {
3146
			return $this->getDefaultSchemaVars();
3147
		}
3148
	}
3149
3150
	/**
3151
	 * Get schema variables to use if none have been set via setSchemaVars().
3152
	 *
3153
	 * Override this in derived classes to provide variables for tables.sql
3154
	 * and SQL patch files.
3155
	 *
3156
	 * @return array
3157
	 */
3158
	protected function getDefaultSchemaVars() {
3159
		return [];
3160
	}
3161
3162
	public function lockIsFree( $lockName, $method ) {
3163
		return true;
3164
	}
3165
3166
	public function lock( $lockName, $method, $timeout = 5 ) {
3167
		$this->mNamedLocksHeld[$lockName] = 1;
3168
3169
		return true;
3170
	}
3171
3172
	public function unlock( $lockName, $method ) {
3173
		unset( $this->mNamedLocksHeld[$lockName] );
3174
3175
		return true;
3176
	}
3177
3178
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3179
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3180
			return null;
3181
		}
3182
3183
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3184
			$this->commit( __METHOD__, 'flush' );
3185
			$this->unlock( $lockKey, $fname );
3186
		} );
3187
3188
		$this->commit( __METHOD__, 'flush' );
3189
3190
		return $unlocker;
3191
	}
3192
3193
	public function namedLocksEnqueue() {
3194
		return false;
3195
	}
3196
3197
	/**
3198
	 * Lock specific tables
3199
	 *
3200
	 * @param array $read Array of tables to lock for read access
3201
	 * @param array $write Array of tables to lock for write access
3202
	 * @param string $method Name of caller
3203
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3204
	 * @return bool
3205
	 */
3206
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3207
		return true;
3208
	}
3209
3210
	/**
3211
	 * Unlock specific tables
3212
	 *
3213
	 * @param string $method The caller
3214
	 * @return bool
3215
	 */
3216
	public function unlockTables( $method ) {
3217
		return true;
3218
	}
3219
3220
	/**
3221
	 * Delete a table
3222
	 * @param string $tableName
3223
	 * @param string $fName
3224
	 * @return bool|ResultWrapper
3225
	 * @since 1.18
3226
	 */
3227 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3228
		if ( !$this->tableExists( $tableName, $fName ) ) {
3229
			return false;
3230
		}
3231
		$sql = "DROP TABLE " . $this->tableName( $tableName );
3232
		if ( $this->cascadingDeletes() ) {
3233
			$sql .= " CASCADE";
3234
		}
3235
3236
		return $this->query( $sql, $fName );
3237
	}
3238
3239
	/**
3240
	 * Get search engine class. All subclasses of this need to implement this
3241
	 * if they wish to use searching.
3242
	 *
3243
	 * @return string
3244
	 */
3245
	public function getSearchEngine() {
3246
		return 'SearchEngineDummy';
3247
	}
3248
3249
	public function getInfinity() {
3250
		return 'infinity';
3251
	}
3252
3253
	public function encodeExpiry( $expiry ) {
3254
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3255
			? $this->getInfinity()
3256
			: $this->timestamp( $expiry );
3257
	}
3258
3259
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3260
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3261
			? 'infinity'
3262
			: wfTimestamp( $format, $expiry );
3263
	}
3264
3265
	public function setBigSelects( $value = true ) {
3266
		// no-op
3267
	}
3268
3269
	public function isReadOnly() {
3270
		return ( $this->getReadOnlyReason() !== false );
3271
	}
3272
3273
	/**
3274
	 * @return string|bool Reason this DB is read-only or false if it is not
3275
	 */
3276
	protected function getReadOnlyReason() {
3277
		$reason = $this->getLBInfo( 'readOnlyReason' );
3278
3279
		return is_string( $reason ) ? $reason : false;
3280
	}
3281
3282
	/**
3283
	 * @since 1.19
3284
	 * @return string
3285
	 */
3286
	public function __toString() {
3287
		return (string)$this->mConn;
3288
	}
3289
3290
	/**
3291
	 * Run a few simple sanity checks
3292
	 */
3293
	public function __destruct() {
3294
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3295
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3296
		}
3297
		if ( count( $this->mTrxIdleCallbacks ) || count( $this->mTrxPreCommitCallbacks ) ) {
3298
			$callers = [];
3299
			foreach ( $this->mTrxIdleCallbacks as $callbackInfo ) {
3300
				$callers[] = $callbackInfo[1];
3301
			}
3302
			$callers = implode( ', ', $callers );
3303
			trigger_error( "DB transaction callbacks still pending (from $callers)." );
3304
		}
3305
	}
3306
}
3307
3308
/**
3309
 * @since 1.27
3310
 */
3311
abstract class Database extends DatabaseBase {
3312
	// B/C until nothing type hints for DatabaseBase
3313
	// @TODO: finish renaming DatabaseBase => Database
3314
}
3315