Completed
Branch master (d7c4e6)
by
unknown
29:20
created

DatabaseBase::doProfiledQuery()   C

Complexity

Conditions 7
Paths 24

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 23
c 1
b 0
f 0
nc 24
nop 4
dl 0
loc 39
rs 6.7272
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
	/** Minimum time to wait before retry, in microseconds */
36
	const DEADLOCK_DELAY_MIN = 500000;
37
	/** Maximum time to wait before retry */
38
	const DEADLOCK_DELAY_MAX = 1500000;
39
40
	/** How long before it is worth doing a dummy query to test the connection */
41
	const PING_TTL = 1.0;
42
43
	/** @var string SQL query */
44
	protected $mLastQuery = '';
45
	/** @var bool */
46
	protected $mDoneWrites = false;
47
	/** @var string|bool */
48
	protected $mPHPError = false;
49
	/** @var string */
50
	protected $mServer;
51
	/** @var string */
52
	protected $mUser;
53
	/** @var string */
54
	protected $mPassword;
55
	/** @var string */
56
	protected $mDBname;
57
58
	/** @var BagOStuff APC cache */
59
	protected $srvCache;
60
61
	/** @var resource Database connection */
62
	protected $mConn = null;
63
	/** @var bool */
64
	protected $mOpened = false;
65
66
	/** @var array[] List of (callable, method name) */
67
	protected $mTrxIdleCallbacks = [];
68
	/** @var array[] List of (callable, method name) */
69
	protected $mTrxPreCommitCallbacks = [];
70
	/** @var array[] List of (callable, method name) */
71
	protected $mTrxEndCallbacks = [];
72
	/** @var bool Whether to suppress triggering of post-commit callbacks */
73
	protected $suppressPostCommitCallbacks = false;
74
75
	/** @var string */
76
	protected $mTablePrefix;
77
	/** @var string */
78
	protected $mSchema;
79
	/** @var integer */
80
	protected $mFlags;
81
	/** @var bool */
82
	protected $mForeign;
83
	/** @var array */
84
	protected $mLBInfo = [];
85
	/** @var bool|null */
86
	protected $mDefaultBigSelects = null;
87
	/** @var array|bool */
88
	protected $mSchemaVars = false;
89
	/** @var array */
90
	protected $mSessionVars = [];
91
	/** @var array|null */
92
	protected $preparedArgs;
93
	/** @var string|bool|null Stashed value of html_errors INI setting */
94
	protected $htmlErrors;
95
	/** @var string */
96
	protected $delimiter = ';';
97
98
	/**
99
	 * Either 1 if a transaction is active or 0 otherwise.
100
	 * The other Trx fields may not be meaningfull if this is 0.
101
	 *
102
	 * @var int
103
	 */
104
	protected $mTrxLevel = 0;
105
106
	/**
107
	 * Either a short hexidecimal string if a transaction is active or ""
108
	 *
109
	 * @var string
110
	 * @see DatabaseBase::mTrxLevel
111
	 */
112
	protected $mTrxShortId = '';
113
114
	/**
115
	 * The UNIX time that the transaction started. Callers can assume that if
116
	 * snapshot isolation is used, then the data is *at least* up to date to that
117
	 * point (possibly more up-to-date since the first SELECT defines the snapshot).
118
	 *
119
	 * @var float|null
120
	 * @see DatabaseBase::mTrxLevel
121
	 */
122
	private $mTrxTimestamp = null;
123
124
	/** @var float Lag estimate at the time of BEGIN */
125
	private $mTrxSlaveLag = null;
126
127
	/**
128
	 * Remembers the function name given for starting the most recent transaction via begin().
129
	 * Used to provide additional context for error reporting.
130
	 *
131
	 * @var string
132
	 * @see DatabaseBase::mTrxLevel
133
	 */
134
	private $mTrxFname = null;
135
136
	/**
137
	 * Record if possible write queries were done in the last transaction started
138
	 *
139
	 * @var bool
140
	 * @see DatabaseBase::mTrxLevel
141
	 */
142
	private $mTrxDoneWrites = false;
143
144
	/**
145
	 * Record if the current transaction was started implicitly due to DBO_TRX being set.
146
	 *
147
	 * @var bool
148
	 * @see DatabaseBase::mTrxLevel
149
	 */
150
	private $mTrxAutomatic = false;
151
152
	/**
153
	 * Array of levels of atomicity within transactions
154
	 *
155
	 * @var array
156
	 */
157
	private $mTrxAtomicLevels = [];
158
159
	/**
160
	 * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
161
	 *
162
	 * @var bool
163
	 */
164
	private $mTrxAutomaticAtomic = false;
165
166
	/**
167
	 * Track the write query callers of the current transaction
168
	 *
169
	 * @var string[]
170
	 */
171
	private $mTrxWriteCallers = [];
172
173
	/**
174
	 * Track the seconds spent in write queries for the current transaction
175
	 *
176
	 * @var float
177
	 */
178
	private $mTrxWriteDuration = 0.0;
179
180
	/** @var array Map of (name => 1) for locks obtained via lock() */
181
	private $mNamedLocksHeld = [];
182
183
	/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
184
	private $lazyMasterHandle;
185
186
	/**
187
	 * @since 1.21
188
	 * @var resource File handle for upgrade
189
	 */
190
	protected $fileHandle = null;
191
192
	/**
193
	 * @since 1.22
194
	 * @var string[] Process cache of VIEWs names in the database
195
	 */
196
	protected $allViews = null;
197
198
	/** @var float UNIX timestamp */
199
	protected $lastPing = 0.0;
200
201
	/** @var TransactionProfiler */
202
	protected $trxProfiler;
203
204
	public function getServerInfo() {
205
		return $this->getServerVersion();
206
	}
207
208
	/**
209
	 * @return string Command delimiter used by this database engine
210
	 */
211
	public function getDelimiter() {
212
		return $this->delimiter;
213
	}
214
215
	/**
216
	 * Boolean, controls output of large amounts of debug information.
217
	 * @param bool|null $debug
218
	 *   - true to enable debugging
219
	 *   - false to disable debugging
220
	 *   - omitted or null to do nothing
221
	 *
222
	 * @return bool|null Previous value of the flag
223
	 */
224
	public function debug( $debug = null ) {
225
		return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
0 ignored issues
show
Bug introduced by
It seems like $debug defined by parameter $debug on line 224 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...
226
	}
227
228
	public function bufferResults( $buffer = null ) {
229
		if ( is_null( $buffer ) ) {
230
			return !(bool)( $this->mFlags & DBO_NOBUFFER );
231
		} else {
232
			return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
233
		}
234
	}
235
236
	/**
237
	 * Turns on (false) or off (true) the automatic generation and sending
238
	 * of a "we're sorry, but there has been a database error" page on
239
	 * database errors. Default is on (false). When turned off, the
240
	 * code should use lastErrno() and lastError() to handle the
241
	 * situation as appropriate.
242
	 *
243
	 * Do not use this function outside of the Database classes.
244
	 *
245
	 * @param null|bool $ignoreErrors
246
	 * @return bool The previous value of the flag.
247
	 */
248
	protected function ignoreErrors( $ignoreErrors = null ) {
249
		return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
0 ignored issues
show
Bug introduced by
It seems like $ignoreErrors defined by parameter $ignoreErrors on line 248 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...
250
	}
251
252
	public function trxLevel() {
253
		return $this->mTrxLevel;
254
	}
255
256
	public function trxTimestamp() {
257
		return $this->mTrxLevel ? $this->mTrxTimestamp : null;
258
	}
259
260
	public function tablePrefix( $prefix = null ) {
261
		return wfSetVar( $this->mTablePrefix, $prefix );
262
	}
263
264
	public function dbSchema( $schema = null ) {
265
		return wfSetVar( $this->mSchema, $schema );
266
	}
267
268
	/**
269
	 * Set the filehandle to copy write statements to.
270
	 *
271
	 * @param resource $fh File handle
272
	 */
273
	public function setFileHandle( $fh ) {
274
		$this->fileHandle = $fh;
275
	}
276
277
	public function getLBInfo( $name = null ) {
278
		if ( is_null( $name ) ) {
279
			return $this->mLBInfo;
280
		} else {
281
			if ( array_key_exists( $name, $this->mLBInfo ) ) {
282
				return $this->mLBInfo[$name];
283
			} else {
284
				return null;
285
			}
286
		}
287
	}
288
289
	public function setLBInfo( $name, $value = null ) {
290
		if ( is_null( $value ) ) {
291
			$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...
292
		} else {
293
			$this->mLBInfo[$name] = $value;
294
		}
295
	}
296
297
	/**
298
	 * Set a lazy-connecting DB handle to the master DB (for replication status purposes)
299
	 *
300
	 * @param IDatabase $conn
301
	 * @since 1.27
302
	 */
303
	public function setLazyMasterHandle( IDatabase $conn ) {
304
		$this->lazyMasterHandle = $conn;
305
	}
306
307
	/**
308
	 * @return IDatabase|null
309
	 * @see setLazyMasterHandle()
310
	 * @since 1.27
311
	 */
312
	public function getLazyMasterHandle() {
313
		return $this->lazyMasterHandle;
314
	}
315
316
	/**
317
	 * @return TransactionProfiler
318
	 */
319
	protected function getTransactionProfiler() {
320
		if ( !$this->trxProfiler ) {
321
			$this->trxProfiler = new TransactionProfiler();
322
		}
323
324
		return $this->trxProfiler;
325
	}
326
327
	/**
328
	 * @param TransactionProfiler $profiler
329
	 * @since 1.27
330
	 */
331
	public function setTransactionProfiler( TransactionProfiler $profiler ) {
332
		$this->trxProfiler = $profiler;
333
	}
334
335
	/**
336
	 * Returns true if this database supports (and uses) cascading deletes
337
	 *
338
	 * @return bool
339
	 */
340
	public function cascadingDeletes() {
341
		return false;
342
	}
343
344
	/**
345
	 * Returns true if this database supports (and uses) triggers (e.g. on the page table)
346
	 *
347
	 * @return bool
348
	 */
349
	public function cleanupTriggers() {
350
		return false;
351
	}
352
353
	/**
354
	 * Returns true if this database is strict about what can be put into an IP field.
355
	 * Specifically, it uses a NULL value instead of an empty string.
356
	 *
357
	 * @return bool
358
	 */
359
	public function strictIPs() {
360
		return false;
361
	}
362
363
	/**
364
	 * Returns true if this database uses timestamps rather than integers
365
	 *
366
	 * @return bool
367
	 */
368
	public function realTimestamps() {
369
		return false;
370
	}
371
372
	public function implicitGroupby() {
373
		return true;
374
	}
375
376
	public function implicitOrderby() {
377
		return true;
378
	}
379
380
	/**
381
	 * Returns true if this database can do a native search on IP columns
382
	 * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
383
	 *
384
	 * @return bool
385
	 */
386
	public function searchableIPs() {
387
		return false;
388
	}
389
390
	/**
391
	 * Returns true if this database can use functional indexes
392
	 *
393
	 * @return bool
394
	 */
395
	public function functionalIndexes() {
396
		return false;
397
	}
398
399
	public function lastQuery() {
400
		return $this->mLastQuery;
401
	}
402
403
	public function doneWrites() {
404
		return (bool)$this->mDoneWrites;
405
	}
406
407
	public function lastDoneWrites() {
408
		return $this->mDoneWrites ?: false;
409
	}
410
411
	public function writesPending() {
412
		return $this->mTrxLevel && $this->mTrxDoneWrites;
413
	}
414
415
	public function writesOrCallbacksPending() {
416
		return $this->mTrxLevel && (
417
			$this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
418
		);
419
	}
420
421
	public function pendingWriteQueryDuration() {
422
		return $this->mTrxLevel ? $this->mTrxWriteDuration : false;
423
	}
424
425
	public function pendingWriteCallers() {
426
		return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
427
	}
428
429
	public function isOpen() {
430
		return $this->mOpened;
431
	}
432
433
	public function setFlag( $flag ) {
434
		$this->mFlags |= $flag;
435
	}
436
437
	public function clearFlag( $flag ) {
438
		$this->mFlags &= ~$flag;
439
	}
440
441
	public function getFlag( $flag ) {
442
		return !!( $this->mFlags & $flag );
443
	}
444
445
	public function getProperty( $name ) {
446
		return $this->$name;
447
	}
448
449
	public function getWikiID() {
450
		if ( $this->mTablePrefix ) {
451
			return "{$this->mDBname}-{$this->mTablePrefix}";
452
		} else {
453
			return $this->mDBname;
454
		}
455
	}
456
457
	/**
458
	 * Return a path to the DBMS-specific SQL file if it exists,
459
	 * otherwise default SQL file
460
	 *
461
	 * @param string $filename
462
	 * @return string
463
	 */
464 View Code Duplication
	private function getSqlFilePath( $filename ) {
465
		global $IP;
466
		$dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename";
467
		if ( file_exists( $dbmsSpecificFilePath ) ) {
468
			return $dbmsSpecificFilePath;
469
		} else {
470
			return "$IP/maintenance/$filename";
471
		}
472
	}
473
474
	/**
475
	 * Return a path to the DBMS-specific schema file,
476
	 * otherwise default to tables.sql
477
	 *
478
	 * @return string
479
	 */
480
	public function getSchemaPath() {
481
		return $this->getSqlFilePath( 'tables.sql' );
482
	}
483
484
	/**
485
	 * Return a path to the DBMS-specific update key file,
486
	 * otherwise default to update-keys.sql
487
	 *
488
	 * @return string
489
	 */
490
	public function getUpdateKeysPath() {
491
		return $this->getSqlFilePath( 'update-keys.sql' );
492
	}
493
494
	/**
495
	 * Get information about an index into an object
496
	 * @param string $table Table name
497
	 * @param string $index Index name
498
	 * @param string $fname Calling function name
499
	 * @return mixed Database-specific index description class or false if the index does not exist
500
	 */
501
	abstract function indexInfo( $table, $index, $fname = __METHOD__ );
502
503
	/**
504
	 * Wrapper for addslashes()
505
	 *
506
	 * @param string $s String to be slashed.
507
	 * @return string Slashed string.
508
	 */
509
	abstract function strencode( $s );
510
511
	/**
512
	 * Constructor.
513
	 *
514
	 * FIXME: It is possible to construct a Database object with no associated
515
	 * connection object, by specifying no parameters to __construct(). This
516
	 * feature is deprecated and should be removed.
517
	 *
518
	 * DatabaseBase subclasses should not be constructed directly in external
519
	 * code. DatabaseBase::factory() should be used instead.
520
	 *
521
	 * @param array $params Parameters passed from DatabaseBase::factory()
522
	 */
523
	function __construct( array $params ) {
524
		global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
525
526
		$this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
527
528
		$server = $params['host'];
529
		$user = $params['user'];
530
		$password = $params['password'];
531
		$dbName = $params['dbname'];
532
		$flags = $params['flags'];
533
		$tablePrefix = $params['tablePrefix'];
534
		$schema = $params['schema'];
535
		$foreign = $params['foreign'];
536
537
		$this->mFlags = $flags;
538
		if ( $this->mFlags & DBO_DEFAULT ) {
539
			if ( $wgCommandLineMode ) {
540
				$this->mFlags &= ~DBO_TRX;
541
			} else {
542
				$this->mFlags |= DBO_TRX;
543
			}
544
		}
545
546
		$this->mSessionVars = $params['variables'];
547
548
		/** Get the default table prefix*/
549
		if ( $tablePrefix === 'get from global' ) {
550
			$this->mTablePrefix = $wgDBprefix;
551
		} else {
552
			$this->mTablePrefix = $tablePrefix;
553
		}
554
555
		/** Get the database schema*/
556
		if ( $schema === 'get from global' ) {
557
			$this->mSchema = $wgDBmwschema;
558
		} else {
559
			$this->mSchema = $schema;
560
		}
561
562
		$this->mForeign = $foreign;
563
564
		if ( isset( $params['trxProfiler'] ) ) {
565
			$this->trxProfiler = $params['trxProfiler']; // override
566
		}
567
568
		if ( $user ) {
569
			$this->open( $server, $user, $password, $dbName );
570
		}
571
	}
572
573
	/**
574
	 * Called by serialize. Throw an exception when DB connection is serialized.
575
	 * This causes problems on some database engines because the connection is
576
	 * not restored on unserialize.
577
	 */
578
	public function __sleep() {
579
		throw new MWException( 'Database serialization may cause problems, since ' .
580
			'the connection is not restored on wakeup.' );
581
	}
582
583
	/**
584
	 * Given a DB type, construct the name of the appropriate child class of
585
	 * DatabaseBase. This is designed to replace all of the manual stuff like:
586
	 *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
587
	 * as well as validate against the canonical list of DB types we have
588
	 *
589
	 * This factory function is mostly useful for when you need to connect to a
590
	 * database other than the MediaWiki default (such as for external auth,
591
	 * an extension, et cetera). Do not use this to connect to the MediaWiki
592
	 * database. Example uses in core:
593
	 * @see LoadBalancer::reallyOpenConnection()
594
	 * @see ForeignDBRepo::getMasterDB()
595
	 * @see WebInstallerDBConnect::execute()
596
	 *
597
	 * @since 1.18
598
	 *
599
	 * @param string $dbType A possible DB type
600
	 * @param array $p An array of options to pass to the constructor.
601
	 *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
602
	 * @throws MWException If the database driver or extension cannot be found
603
	 * @return DatabaseBase|null DatabaseBase subclass or null
604
	 */
605
	final public static function factory( $dbType, $p = [] ) {
606
		$canonicalDBTypes = [
607
			'mysql' => [ 'mysqli', 'mysql' ],
608
			'postgres' => [],
609
			'sqlite' => [],
610
			'oracle' => [],
611
			'mssql' => [],
612
		];
613
614
		$driver = false;
615
		$dbType = strtolower( $dbType );
616
		if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
617
			$possibleDrivers = $canonicalDBTypes[$dbType];
618
			if ( !empty( $p['driver'] ) ) {
619
				if ( in_array( $p['driver'], $possibleDrivers ) ) {
620
					$driver = $p['driver'];
621
				} else {
622
					throw new MWException( __METHOD__ .
623
						" cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
624
				}
625
			} else {
626
				foreach ( $possibleDrivers as $posDriver ) {
627
					if ( extension_loaded( $posDriver ) ) {
628
						$driver = $posDriver;
629
						break;
630
					}
631
				}
632
			}
633
		} else {
634
			$driver = $dbType;
635
		}
636
		if ( $driver === false ) {
637
			throw new MWException( __METHOD__ .
638
				" no viable database extension found for type '$dbType'" );
639
		}
640
641
		// Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
642
		// and everything else doesn't use a schema (e.g. null)
643
		// Although postgres and oracle support schemas, we don't use them (yet)
644
		// to maintain backwards compatibility
645
		$defaultSchemas = [
646
			'mssql' => 'get from global',
647
		];
648
649
		$class = 'Database' . ucfirst( $driver );
650
		if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
651
			// Resolve some defaults for b/c
652
			$p['host'] = isset( $p['host'] ) ? $p['host'] : false;
653
			$p['user'] = isset( $p['user'] ) ? $p['user'] : false;
654
			$p['password'] = isset( $p['password'] ) ? $p['password'] : false;
655
			$p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
656
			$p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
657
			$p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
658
			$p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
659
			if ( !isset( $p['schema'] ) ) {
660
				$p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
661
			}
662
			$p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
663
664
			return new $class( $p );
665
		} else {
666
			return null;
667
		}
668
	}
669
670
	protected function installErrorHandler() {
671
		$this->mPHPError = false;
672
		$this->htmlErrors = ini_set( 'html_errors', '0' );
673
		set_error_handler( [ $this, 'connectionErrorHandler' ] );
674
	}
675
676
	/**
677
	 * @return bool|string
678
	 */
679
	protected function restoreErrorHandler() {
680
		restore_error_handler();
681
		if ( $this->htmlErrors !== false ) {
682
			ini_set( 'html_errors', $this->htmlErrors );
683
		}
684
		if ( $this->mPHPError ) {
685
			$error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
686
			$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
687
688
			return $error;
689
		} else {
690
			return false;
691
		}
692
	}
693
694
	/**
695
	 * @param int $errno
696
	 * @param string $errstr
697
	 */
698
	public function connectionErrorHandler( $errno, $errstr ) {
699
		$this->mPHPError = $errstr;
700
	}
701
702
	/**
703
	 * Create a log context to pass to wfLogDBError or other logging functions.
704
	 *
705
	 * @param array $extras Additional data to add to context
706
	 * @return array
707
	 */
708
	protected function getLogContext( array $extras = [] ) {
709
		return array_merge(
710
			[
711
				'db_server' => $this->mServer,
712
				'db_name' => $this->mDBname,
713
				'db_user' => $this->mUser,
714
			],
715
			$extras
716
		);
717
	}
718
719
	public function close() {
720
		if ( $this->mConn ) {
721
			if ( $this->trxLevel() ) {
722
				if ( !$this->mTrxAutomatic ) {
723
					wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " .
724
						" performing implicit commit before closing connection!" );
725
				}
726
727
				$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
728
			}
729
730
			$closed = $this->closeConnection();
731
			$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...
732
		} elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
733
			throw new MWException( "Transaction callbacks still pending." );
734
		} else {
735
			$closed = true;
736
		}
737
		$this->mOpened = false;
738
739
		return $closed;
740
	}
741
742
	/**
743
	 * Make sure isOpen() returns true as a sanity check
744
	 *
745
	 * @throws DBUnexpectedError
746
	 */
747
	protected function assertOpen() {
748
		if ( !$this->isOpen() ) {
749
			throw new DBUnexpectedError( $this, "DB connection was already closed." );
750
		}
751
	}
752
753
	/**
754
	 * Closes underlying database connection
755
	 * @since 1.20
756
	 * @return bool Whether connection was closed successfully
757
	 */
758
	abstract protected function closeConnection();
759
760
	function reportConnectionError( $error = 'Unknown error' ) {
761
		$myError = $this->lastError();
762
		if ( $myError ) {
763
			$error = $myError;
764
		}
765
766
		# New method
767
		throw new DBConnectionError( $this, $error );
768
	}
769
770
	/**
771
	 * The DBMS-dependent part of query()
772
	 *
773
	 * @param string $sql SQL query.
774
	 * @return ResultWrapper|bool Result object to feed to fetchObject,
775
	 *   fetchRow, ...; or false on failure
776
	 */
777
	abstract protected function doQuery( $sql );
778
779
	/**
780
	 * Determine whether a query writes to the DB.
781
	 * Should return true if unsure.
782
	 *
783
	 * @param string $sql
784
	 * @return bool
785
	 */
786
	protected function isWriteQuery( $sql ) {
787
		return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
788
	}
789
790
	/**
791
	 * Determine whether a SQL statement is sensitive to isolation level.
792
	 * A SQL statement is considered transactable if its result could vary
793
	 * depending on the transaction isolation level. Operational commands
794
	 * such as 'SET' and 'SHOW' are not considered to be transactable.
795
	 *
796
	 * @param string $sql
797
	 * @return bool
798
	 */
799
	protected function isTransactableQuery( $sql ) {
800
		$verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
801
		return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ] );
802
	}
803
804
	public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
805
		global $wgUser;
806
807
		$priorWritesPending = $this->writesOrCallbacksPending();
808
		$this->mLastQuery = $sql;
809
810
		$isWrite = $this->isWriteQuery( $sql );
811
		if ( $isWrite ) {
812
			$reason = $this->getReadOnlyReason();
813
			if ( $reason !== false ) {
814
				throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
815
			}
816
			# Set a flag indicating that writes have been done
817
			$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...
818
		}
819
820
		# Add a comment for easy SHOW PROCESSLIST interpretation
821
		if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
822
			$userName = $wgUser->getName();
823
			if ( mb_strlen( $userName ) > 15 ) {
824
				$userName = mb_substr( $userName, 0, 15 ) . '...';
825
			}
826
			$userName = str_replace( '/', '', $userName );
827
		} else {
828
			$userName = '';
829
		}
830
831
		// Add trace comment to the begin of the sql string, right after the operator.
832
		// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
833
		$commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
834
835
		# Start implicit transactions that wrap the request if DBO_TRX is enabled
836
		if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
837
			&& $this->isTransactableQuery( $sql )
838
		) {
839
			$this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
840
			$this->mTrxAutomatic = true;
841
		}
842
843
		# Keep track of whether the transaction has write queries pending
844
		if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
845
			$this->mTrxDoneWrites = true;
846
			$this->getTransactionProfiler()->transactionWritingIn(
847
				$this->mServer, $this->mDBname, $this->mTrxShortId );
848
		}
849
850
		if ( $this->debug() ) {
851
			wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
852
		}
853
854
		# Avoid fatals if close() was called
855
		$this->assertOpen();
856
857
		# Send the query to the server
858
		$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
859
860
		# Try reconnecting if the connection was lost
861
		if ( false === $ret && $this->wasErrorReissuable() ) {
862
			$recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
863
			# Stash the last error values before anything might clear them
864
			$lastError = $this->lastError();
865
			$lastErrno = $this->lastErrno();
866
			# Update state tracking to reflect transaction loss due to disconnection
867
			$this->handleTransactionLoss();
868
			wfDebug( "Connection lost, reconnecting...\n" );
869
			if ( $this->reconnect() ) {
870
				wfDebug( "Reconnected\n" );
871
				$msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
872
				wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
873
874
				if ( !$recoverable ) {
875
					# Callers may catch the exception and continue to use the DB
876
					$this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
877
				} else {
878
					# Should be safe to silently retry the query
879
					$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
880
				}
881
			} else {
882
				wfDebug( "Failed\n" );
883
			}
884
		}
885
886
		if ( false === $ret ) {
887
			# Deadlocks cause the entire transaction to abort, not just the statement.
888
			# http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
889
			# https://www.postgresql.org/docs/9.1/static/explicit-locking.html
890
			if ( $this->wasDeadlock() ) {
891
				if ( $this->explicitTrxActive() || $priorWritesPending ) {
892
					$tempIgnore = false; // not recoverable
893
				}
894
				# Update state tracking to reflect transaction loss
895
				$this->handleTransactionLoss();
896
			}
897
898
			$this->reportQueryError(
899
				$this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
900
		}
901
902
		$res = $this->resultObject( $ret );
903
904
		return $res;
905
	}
906
907
	private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
908
		$isMaster = !is_null( $this->getLBInfo( 'master' ) );
909
		# generalizeSQL() will probably cut down the query to reasonable
910
		# logging size most of the time. The substr is really just a sanity check.
911
		if ( $isMaster ) {
912
			$queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
913
		} else {
914
			$queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
915
		}
916
917
		# Include query transaction state
918
		$queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
919
920
		$profiler = Profiler::instance();
921
		if ( !( $profiler instanceof ProfilerStub ) ) {
922
			$queryProfSection = $profiler->scopedProfileIn( $queryProf );
923
		}
924
925
		$startTime = microtime( true );
926
		$ret = $this->doQuery( $commentedSql );
927
		$queryRuntime = microtime( true ) - $startTime;
928
929
		unset( $queryProfSection ); // profile out (if set)
930
931
		if ( $ret !== false ) {
932
			$this->lastPing = $startTime;
933
			if ( $isWrite && $this->mTrxLevel ) {
934
				$this->mTrxWriteDuration += $queryRuntime;
935
				$this->mTrxWriteCallers[] = $fname;
936
			}
937
		}
938
939
		$this->getTransactionProfiler()->recordQueryCompletion(
940
			$queryProf, $startTime, $isWrite, $this->affectedRows()
941
		);
942
		MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
943
944
		return $ret;
945
	}
946
947
	private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
948
		# Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
949
		# Dropped connections also mean that named locks are automatically released.
950
		# Only allow error suppression in autocommit mode or when the lost transaction
951
		# didn't matter anyway (aside from DBO_TRX snapshot loss).
952
		if ( $this->mNamedLocksHeld ) {
953
			return false; // possible critical section violation
954
		} elseif ( $sql === 'COMMIT' ) {
955
			return !$priorWritesPending; // nothing written anyway? (T127428)
956
		} elseif ( $sql === 'ROLLBACK' ) {
957
			return true; // transaction lost...which is also what was requested :)
958
		} elseif ( $this->explicitTrxActive() ) {
959
			return false; // don't drop atomocity
960
		} elseif ( $priorWritesPending ) {
961
			return false; // prior writes lost from implicit transaction
962
		}
963
964
		return true;
965
	}
966
967
	private function handleTransactionLoss() {
968
		$this->mTrxLevel = 0;
969
		$this->mTrxIdleCallbacks = []; // bug 65263
970
		$this->mTrxPreCommitCallbacks = []; // bug 65263
971
		try {
972
			// Handle callbacks in mTrxEndCallbacks
973
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
974
			return null;
975
		} catch ( Exception $e ) {
976
			// Already logged; move on...
977
			return $e;
978
		}
979
	}
980
981
	public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
982
		if ( $this->ignoreErrors() || $tempIgnore ) {
983
			wfDebug( "SQL ERROR (ignored): $error\n" );
984
		} else {
985
			$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
986
			wfLogDBError(
987
				"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
988
				$this->getLogContext( [
989
					'method' => __METHOD__,
990
					'errno' => $errno,
991
					'error' => $error,
992
					'sql1line' => $sql1line,
993
					'fname' => $fname,
994
				] )
995
			);
996
			wfDebug( "SQL ERROR: " . $error . "\n" );
997
			throw new DBQueryError( $this, $error, $errno, $sql, $fname );
998
		}
999
	}
1000
1001
	/**
1002
	 * Intended to be compatible with the PEAR::DB wrapper functions.
1003
	 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
1004
	 *
1005
	 * ? = scalar value, quoted as necessary
1006
	 * ! = raw SQL bit (a function for instance)
1007
	 * & = filename; reads the file and inserts as a blob
1008
	 *     (we don't use this though...)
1009
	 *
1010
	 * @param string $sql
1011
	 * @param string $func
1012
	 *
1013
	 * @return array
1014
	 */
1015
	protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
1016
		/* MySQL doesn't support prepared statements (yet), so just
1017
		 * pack up the query for reference. We'll manually replace
1018
		 * the bits later.
1019
		 */
1020
		return [ 'query' => $sql, 'func' => $func ];
1021
	}
1022
1023
	/**
1024
	 * Free a prepared query, generated by prepare().
1025
	 * @param string $prepared
1026
	 */
1027
	protected function freePrepared( $prepared ) {
1028
		/* No-op by default */
1029
	}
1030
1031
	/**
1032
	 * Execute a prepared query with the various arguments
1033
	 * @param string $prepared The prepared sql
1034
	 * @param mixed $args Either an array here, or put scalars as varargs
1035
	 *
1036
	 * @return ResultWrapper
1037
	 */
1038
	public function execute( $prepared, $args = null ) {
1039
		if ( !is_array( $args ) ) {
1040
			# Pull the var args
1041
			$args = func_get_args();
1042
			array_shift( $args );
1043
		}
1044
1045
		$sql = $this->fillPrepared( $prepared['query'], $args );
1046
1047
		return $this->query( $sql, $prepared['func'] );
1048
	}
1049
1050
	/**
1051
	 * For faking prepared SQL statements on DBs that don't support it directly.
1052
	 *
1053
	 * @param string $preparedQuery A 'preparable' SQL statement
1054
	 * @param array $args Array of Arguments to fill it with
1055
	 * @return string Executable SQL
1056
	 */
1057
	public function fillPrepared( $preparedQuery, $args ) {
1058
		reset( $args );
1059
		$this->preparedArgs =& $args;
1060
1061
		return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
1062
			[ &$this, 'fillPreparedArg' ], $preparedQuery );
1063
	}
1064
1065
	/**
1066
	 * preg_callback func for fillPrepared()
1067
	 * The arguments should be in $this->preparedArgs and must not be touched
1068
	 * while we're doing this.
1069
	 *
1070
	 * @param array $matches
1071
	 * @throws DBUnexpectedError
1072
	 * @return string
1073
	 */
1074
	protected function fillPreparedArg( $matches ) {
1075
		switch ( $matches[1] ) {
1076
			case '\\?':
1077
				return '?';
1078
			case '\\!':
1079
				return '!';
1080
			case '\\&':
1081
				return '&';
1082
		}
1083
1084
		list( /* $n */, $arg ) = each( $this->preparedArgs );
1085
1086
		switch ( $matches[1] ) {
1087
			case '?':
1088
				return $this->addQuotes( $arg );
1089
			case '!':
1090
				return $arg;
1091
			case '&':
1092
				# return $this->addQuotes( file_get_contents( $arg ) );
1093
				throw new DBUnexpectedError(
1094
					$this,
1095
					'& mode is not implemented. If it\'s really needed, uncomment the line above.'
1096
				);
1097
			default:
1098
				throw new DBUnexpectedError(
1099
					$this,
1100
					'Received invalid match. This should never happen!'
1101
				);
1102
		}
1103
	}
1104
1105
	public function freeResult( $res ) {
1106
	}
1107
1108
	public function selectField(
1109
		$table, $var, $cond = '', $fname = __METHOD__, $options = []
1110
	) {
1111
		if ( $var === '*' ) { // sanity
1112
			throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1113
		}
1114
1115
		if ( !is_array( $options ) ) {
1116
			$options = [ $options ];
1117
		}
1118
1119
		$options['LIMIT'] = 1;
1120
1121
		$res = $this->select( $table, $var, $cond, $fname, $options );
1122
		if ( $res === false || !$this->numRows( $res ) ) {
1123
			return false;
1124
		}
1125
1126
		$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 1121 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...
1127
1128
		if ( $row !== false ) {
1129
			return reset( $row );
1130
		} else {
1131
			return false;
1132
		}
1133
	}
1134
1135
	public function selectFieldValues(
1136
		$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1137
	) {
1138
		if ( $var === '*' ) { // sanity
1139
			throw new DBUnexpectedError( $this, "Cannot use a * field" );
1140
		} elseif ( !is_string( $var ) ) { // sanity
1141
			throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1142
		}
1143
1144
		if ( !is_array( $options ) ) {
1145
			$options = [ $options ];
1146
		}
1147
1148
		$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1149
		if ( $res === false ) {
1150
			return false;
1151
		}
1152
1153
		$values = [];
1154
		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...
1155
			$values[] = $row->$var;
1156
		}
1157
1158
		return $values;
1159
	}
1160
1161
	/**
1162
	 * Returns an optional USE INDEX clause to go after the table, and a
1163
	 * string to go at the end of the query.
1164
	 *
1165
	 * @param array $options Associative array of options to be turned into
1166
	 *   an SQL query, valid keys are listed in the function.
1167
	 * @return array
1168
	 * @see DatabaseBase::select()
1169
	 */
1170
	public function makeSelectOptions( $options ) {
1171
		$preLimitTail = $postLimitTail = '';
1172
		$startOpts = '';
1173
1174
		$noKeyOptions = [];
1175
1176
		foreach ( $options as $key => $option ) {
1177
			if ( is_numeric( $key ) ) {
1178
				$noKeyOptions[$option] = true;
1179
			}
1180
		}
1181
1182
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1183
1184
		$preLimitTail .= $this->makeOrderBy( $options );
1185
1186
		// if (isset($options['LIMIT'])) {
1187
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1188
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1189
		// 		: false);
1190
		// }
1191
1192
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1193
			$postLimitTail .= ' FOR UPDATE';
1194
		}
1195
1196
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1197
			$postLimitTail .= ' LOCK IN SHARE MODE';
1198
		}
1199
1200
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1201
			$startOpts .= 'DISTINCT';
1202
		}
1203
1204
		# Various MySQL extensions
1205
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1206
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1207
		}
1208
1209
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1210
			$startOpts .= ' HIGH_PRIORITY';
1211
		}
1212
1213
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1214
			$startOpts .= ' SQL_BIG_RESULT';
1215
		}
1216
1217
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1218
			$startOpts .= ' SQL_BUFFER_RESULT';
1219
		}
1220
1221
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1222
			$startOpts .= ' SQL_SMALL_RESULT';
1223
		}
1224
1225
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1226
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1227
		}
1228
1229
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1230
			$startOpts .= ' SQL_CACHE';
1231
		}
1232
1233
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1234
			$startOpts .= ' SQL_NO_CACHE';
1235
		}
1236
1237 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1238
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1239
		} else {
1240
			$useIndex = '';
1241
		}
1242
1243
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1244
	}
1245
1246
	/**
1247
	 * Returns an optional GROUP BY with an optional HAVING
1248
	 *
1249
	 * @param array $options Associative array of options
1250
	 * @return string
1251
	 * @see DatabaseBase::select()
1252
	 * @since 1.21
1253
	 */
1254
	public function makeGroupByWithHaving( $options ) {
1255
		$sql = '';
1256 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1257
			$gb = is_array( $options['GROUP BY'] )
1258
				? implode( ',', $options['GROUP BY'] )
1259
				: $options['GROUP BY'];
1260
			$sql .= ' GROUP BY ' . $gb;
1261
		}
1262 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1263
			$having = is_array( $options['HAVING'] )
1264
				? $this->makeList( $options['HAVING'], LIST_AND )
1265
				: $options['HAVING'];
1266
			$sql .= ' HAVING ' . $having;
1267
		}
1268
1269
		return $sql;
1270
	}
1271
1272
	/**
1273
	 * Returns an optional ORDER BY
1274
	 *
1275
	 * @param array $options Associative array of options
1276
	 * @return string
1277
	 * @see DatabaseBase::select()
1278
	 * @since 1.21
1279
	 */
1280
	public function makeOrderBy( $options ) {
1281 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1282
			$ob = is_array( $options['ORDER BY'] )
1283
				? implode( ',', $options['ORDER BY'] )
1284
				: $options['ORDER BY'];
1285
1286
			return ' ORDER BY ' . $ob;
1287
		}
1288
1289
		return '';
1290
	}
1291
1292
	// See IDatabase::select for the docs for this function
1293
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1294
		$options = [], $join_conds = [] ) {
1295
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1296
1297
		return $this->query( $sql, $fname );
1298
	}
1299
1300
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1301
		$options = [], $join_conds = []
1302
	) {
1303
		if ( is_array( $vars ) ) {
1304
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1305
		}
1306
1307
		$options = (array)$options;
1308
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1309
			? $options['USE INDEX']
1310
			: [];
1311
1312
		if ( is_array( $table ) ) {
1313
			$from = ' FROM ' .
1314
				$this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
1315
		} elseif ( $table != '' ) {
1316
			if ( $table[0] == ' ' ) {
1317
				$from = ' FROM ' . $table;
1318
			} else {
1319
				$from = ' FROM ' .
1320
					$this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
1321
			}
1322
		} else {
1323
			$from = '';
1324
		}
1325
1326
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
1327
			$this->makeSelectOptions( $options );
1328
1329
		if ( !empty( $conds ) ) {
1330
			if ( is_array( $conds ) ) {
1331
				$conds = $this->makeList( $conds, LIST_AND );
1332
			}
1333
			$sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1334
		} else {
1335
			$sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1336
		}
1337
1338
		if ( isset( $options['LIMIT'] ) ) {
1339
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1340
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1341
		}
1342
		$sql = "$sql $postLimitTail";
1343
1344
		if ( isset( $options['EXPLAIN'] ) ) {
1345
			$sql = 'EXPLAIN ' . $sql;
1346
		}
1347
1348
		return $sql;
1349
	}
1350
1351
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1352
		$options = [], $join_conds = []
1353
	) {
1354
		$options = (array)$options;
1355
		$options['LIMIT'] = 1;
1356
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1357
1358
		if ( $res === false ) {
1359
			return false;
1360
		}
1361
1362
		if ( !$this->numRows( $res ) ) {
1363
			return false;
1364
		}
1365
1366
		$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 1356 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...
1367
1368
		return $obj;
1369
	}
1370
1371
	public function estimateRowCount(
1372
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1373
	) {
1374
		$rows = 0;
1375
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1376
1377 View Code Duplication
		if ( $res ) {
1378
			$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 1375 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...
1379
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1380
		}
1381
1382
		return $rows;
1383
	}
1384
1385
	public function selectRowCount(
1386
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1387
	) {
1388
		$rows = 0;
1389
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1390
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1391
1392 View Code Duplication
		if ( $res ) {
1393
			$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 1390 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...
1394
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1395
		}
1396
1397
		return $rows;
1398
	}
1399
1400
	/**
1401
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1402
	 * It's only slightly flawed. Don't use for anything important.
1403
	 *
1404
	 * @param string $sql A SQL Query
1405
	 *
1406
	 * @return string
1407
	 */
1408
	protected static function generalizeSQL( $sql ) {
1409
		# This does the same as the regexp below would do, but in such a way
1410
		# as to avoid crashing php on some large strings.
1411
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1412
1413
		$sql = str_replace( "\\\\", '', $sql );
1414
		$sql = str_replace( "\\'", '', $sql );
1415
		$sql = str_replace( "\\\"", '', $sql );
1416
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1417
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1418
1419
		# All newlines, tabs, etc replaced by single space
1420
		$sql = preg_replace( '/\s+/', ' ', $sql );
1421
1422
		# All numbers => N,
1423
		# except the ones surrounded by characters, e.g. l10n
1424
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1425
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1426
1427
		return $sql;
1428
	}
1429
1430
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1431
		$info = $this->fieldInfo( $table, $field );
1432
1433
		return (bool)$info;
1434
	}
1435
1436
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1437
		if ( !$this->tableExists( $table ) ) {
1438
			return null;
1439
		}
1440
1441
		$info = $this->indexInfo( $table, $index, $fname );
1442
		if ( is_null( $info ) ) {
1443
			return null;
1444
		} else {
1445
			return $info !== false;
1446
		}
1447
	}
1448
1449
	public function tableExists( $table, $fname = __METHOD__ ) {
1450
		$table = $this->tableName( $table );
1451
		$old = $this->ignoreErrors( true );
1452
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1453
		$this->ignoreErrors( $old );
1454
1455
		return (bool)$res;
1456
	}
1457
1458
	public function indexUnique( $table, $index ) {
1459
		$indexInfo = $this->indexInfo( $table, $index );
1460
1461
		if ( !$indexInfo ) {
1462
			return null;
1463
		}
1464
1465
		return !$indexInfo[0]->Non_unique;
1466
	}
1467
1468
	/**
1469
	 * Helper for DatabaseBase::insert().
1470
	 *
1471
	 * @param array $options
1472
	 * @return string
1473
	 */
1474
	protected function makeInsertOptions( $options ) {
1475
		return implode( ' ', $options );
1476
	}
1477
1478
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1479
		# No rows to insert, easy just return now
1480
		if ( !count( $a ) ) {
1481
			return true;
1482
		}
1483
1484
		$table = $this->tableName( $table );
1485
1486
		if ( !is_array( $options ) ) {
1487
			$options = [ $options ];
1488
		}
1489
1490
		$fh = null;
1491
		if ( isset( $options['fileHandle'] ) ) {
1492
			$fh = $options['fileHandle'];
1493
		}
1494
		$options = $this->makeInsertOptions( $options );
1495
1496
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1497
			$multi = true;
1498
			$keys = array_keys( $a[0] );
1499
		} else {
1500
			$multi = false;
1501
			$keys = array_keys( $a );
1502
		}
1503
1504
		$sql = 'INSERT ' . $options .
1505
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1506
1507
		if ( $multi ) {
1508
			$first = true;
1509 View Code Duplication
			foreach ( $a as $row ) {
1510
				if ( $first ) {
1511
					$first = false;
1512
				} else {
1513
					$sql .= ',';
1514
				}
1515
				$sql .= '(' . $this->makeList( $row ) . ')';
1516
			}
1517
		} else {
1518
			$sql .= '(' . $this->makeList( $a ) . ')';
1519
		}
1520
1521
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1522
			return false;
1523
		} elseif ( $fh !== null ) {
1524
			return true;
1525
		}
1526
1527
		return (bool)$this->query( $sql, $fname );
1528
	}
1529
1530
	/**
1531
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1532
	 *
1533
	 * @param array $options
1534
	 * @return array
1535
	 */
1536
	protected function makeUpdateOptionsArray( $options ) {
1537
		if ( !is_array( $options ) ) {
1538
			$options = [ $options ];
1539
		}
1540
1541
		$opts = [];
1542
1543
		if ( in_array( 'LOW_PRIORITY', $options ) ) {
1544
			$opts[] = $this->lowPriorityOption();
1545
		}
1546
1547
		if ( in_array( 'IGNORE', $options ) ) {
1548
			$opts[] = 'IGNORE';
1549
		}
1550
1551
		return $opts;
1552
	}
1553
1554
	/**
1555
	 * Make UPDATE options for the DatabaseBase::update function
1556
	 *
1557
	 * @param array $options The options passed to DatabaseBase::update
1558
	 * @return string
1559
	 */
1560
	protected function makeUpdateOptions( $options ) {
1561
		$opts = $this->makeUpdateOptionsArray( $options );
1562
1563
		return implode( ' ', $opts );
1564
	}
1565
1566
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1567
		$table = $this->tableName( $table );
1568
		$opts = $this->makeUpdateOptions( $options );
1569
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1570
1571 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
1572
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1573
		}
1574
1575
		return $this->query( $sql, $fname );
1576
	}
1577
1578
	public function makeList( $a, $mode = LIST_COMMA ) {
1579
		if ( !is_array( $a ) ) {
1580
			throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1581
		}
1582
1583
		$first = true;
1584
		$list = '';
1585
1586
		foreach ( $a as $field => $value ) {
1587
			if ( !$first ) {
1588
				if ( $mode == LIST_AND ) {
1589
					$list .= ' AND ';
1590
				} elseif ( $mode == LIST_OR ) {
1591
					$list .= ' OR ';
1592
				} else {
1593
					$list .= ',';
1594
				}
1595
			} else {
1596
				$first = false;
1597
			}
1598
1599
			if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1600
				$list .= "($value)";
1601
			} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1602
				$list .= "$value";
1603
			} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1604
				// Remove null from array to be handled separately if found
1605
				$includeNull = false;
1606
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1607
					$includeNull = true;
1608
					unset( $value[$nullKey] );
1609
				}
1610
				if ( count( $value ) == 0 && !$includeNull ) {
1611
					throw new MWException( __METHOD__ . ": empty input for field $field" );
1612
				} elseif ( count( $value ) == 0 ) {
1613
					// only check if $field is null
1614
					$list .= "$field IS NULL";
1615
				} else {
1616
					// IN clause contains at least one valid element
1617
					if ( $includeNull ) {
1618
						// Group subconditions to ensure correct precedence
1619
						$list .= '(';
1620
					}
1621
					if ( count( $value ) == 1 ) {
1622
						// Special-case single values, as IN isn't terribly efficient
1623
						// Don't necessarily assume the single key is 0; we don't
1624
						// enforce linear numeric ordering on other arrays here.
1625
						$value = array_values( $value )[0];
1626
						$list .= $field . " = " . $this->addQuotes( $value );
1627
					} else {
1628
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1629
					}
1630
					// if null present in array, append IS NULL
1631
					if ( $includeNull ) {
1632
						$list .= " OR $field IS NULL)";
1633
					}
1634
				}
1635
			} elseif ( $value === null ) {
1636 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR ) {
1637
					$list .= "$field IS ";
1638
				} elseif ( $mode == LIST_SET ) {
1639
					$list .= "$field = ";
1640
				}
1641
				$list .= 'NULL';
1642
			} else {
1643 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1644
					$list .= "$field = ";
1645
				}
1646
				$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1647
			}
1648
		}
1649
1650
		return $list;
1651
	}
1652
1653
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1654
		$conds = [];
1655
1656
		foreach ( $data as $base => $sub ) {
1657
			if ( count( $sub ) ) {
1658
				$conds[] = $this->makeList(
1659
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1660
					LIST_AND );
1661
			}
1662
		}
1663
1664
		if ( $conds ) {
1665
			return $this->makeList( $conds, LIST_OR );
1666
		} else {
1667
			// Nothing to search for...
1668
			return false;
1669
		}
1670
	}
1671
1672
	/**
1673
	 * Return aggregated value alias
1674
	 *
1675
	 * @param array $valuedata
1676
	 * @param string $valuename
1677
	 *
1678
	 * @return string
1679
	 */
1680
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1681
		return $valuename;
1682
	}
1683
1684
	public function bitNot( $field ) {
1685
		return "(~$field)";
1686
	}
1687
1688
	public function bitAnd( $fieldLeft, $fieldRight ) {
1689
		return "($fieldLeft & $fieldRight)";
1690
	}
1691
1692
	public function bitOr( $fieldLeft, $fieldRight ) {
1693
		return "($fieldLeft | $fieldRight)";
1694
	}
1695
1696
	public function buildConcat( $stringList ) {
1697
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1698
	}
1699
1700 View Code Duplication
	public function buildGroupConcatField(
1701
		$delim, $table, $field, $conds = '', $join_conds = []
1702
	) {
1703
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1704
1705
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1706
	}
1707
1708
	public function selectDB( $db ) {
1709
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1710
		# if your database engine supports a concept similar to MySQL's
1711
		# databases you may as well.
1712
		$this->mDBname = $db;
1713
1714
		return true;
1715
	}
1716
1717
	public function getDBname() {
1718
		return $this->mDBname;
1719
	}
1720
1721
	public function getServer() {
1722
		return $this->mServer;
1723
	}
1724
1725
	/**
1726
	 * Format a table name ready for use in constructing an SQL query
1727
	 *
1728
	 * This does two important things: it quotes the table names to clean them up,
1729
	 * and it adds a table prefix if only given a table name with no quotes.
1730
	 *
1731
	 * All functions of this object which require a table name call this function
1732
	 * themselves. Pass the canonical name to such functions. This is only needed
1733
	 * when calling query() directly.
1734
	 *
1735
	 * @note This function does not sanitize user input. It is not safe to use
1736
	 *   this function to escape user input.
1737
	 * @param string $name Database table name
1738
	 * @param string $format One of:
1739
	 *   quoted - Automatically pass the table name through addIdentifierQuotes()
1740
	 *            so that it can be used in a query.
1741
	 *   raw - Do not add identifier quotes to the table name
1742
	 * @return string Full database name
1743
	 */
1744
	public function tableName( $name, $format = 'quoted' ) {
1745
		global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1746
		# Skip the entire process when we have a string quoted on both ends.
1747
		# Note that we check the end so that we will still quote any use of
1748
		# use of `database`.table. But won't break things if someone wants
1749
		# to query a database table with a dot in the name.
1750
		if ( $this->isQuotedIdentifier( $name ) ) {
1751
			return $name;
1752
		}
1753
1754
		# Lets test for any bits of text that should never show up in a table
1755
		# name. Basically anything like JOIN or ON which are actually part of
1756
		# SQL queries, but may end up inside of the table value to combine
1757
		# sql. Such as how the API is doing.
1758
		# Note that we use a whitespace test rather than a \b test to avoid
1759
		# any remote case where a word like on may be inside of a table name
1760
		# surrounded by symbols which may be considered word breaks.
1761
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1762
			return $name;
1763
		}
1764
1765
		# Split database and table into proper variables.
1766
		# We reverse the explode so that database.table and table both output
1767
		# the correct table.
1768
		$dbDetails = explode( '.', $name, 3 );
1769
		if ( count( $dbDetails ) == 3 ) {
1770
			list( $database, $schema, $table ) = $dbDetails;
1771
			# We don't want any prefix added in this case
1772
			$prefix = '';
1773
		} elseif ( count( $dbDetails ) == 2 ) {
1774
			list( $database, $table ) = $dbDetails;
1775
			# We don't want any prefix added in this case
1776
			# In dbs that support it, $database may actually be the schema
1777
			# but that doesn't affect any of the functionality here
1778
			$prefix = '';
1779
			$schema = null;
1780
		} else {
1781
			list( $table ) = $dbDetails;
1782
			if ( $wgSharedDB !== null # We have a shared database
1783
				&& $this->mForeign == false # We're not working on a foreign database
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1784
				&& !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1785
				&& in_array( $table, $wgSharedTables ) # A shared table is selected
1786
			) {
1787
				$database = $wgSharedDB;
1788
				$schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1789
				$prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1790
			} else {
1791
				$database = null;
1792
				$schema = $this->mSchema; # Default schema
1793
				$prefix = $this->mTablePrefix; # Default prefix
1794
			}
1795
		}
1796
1797
		# Quote $table and apply the prefix if not quoted.
1798
		# $tableName might be empty if this is called from Database::replaceVars()
1799
		$tableName = "{$prefix}{$table}";
1800
		if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1801
			$tableName = $this->addIdentifierQuotes( $tableName );
1802
		}
1803
1804
		# Quote $schema and merge it with the table name if needed
1805 View Code Duplication
		if ( strlen( $schema ) ) {
1806
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1807
				$schema = $this->addIdentifierQuotes( $schema );
1808
			}
1809
			$tableName = $schema . '.' . $tableName;
1810
		}
1811
1812
		# Quote $database and merge it with the table name if needed
1813 View Code Duplication
		if ( $database !== null ) {
1814
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1815
				$database = $this->addIdentifierQuotes( $database );
1816
			}
1817
			$tableName = $database . '.' . $tableName;
1818
		}
1819
1820
		return $tableName;
1821
	}
1822
1823
	/**
1824
	 * Fetch a number of table names into an array
1825
	 * This is handy when you need to construct SQL for joins
1826
	 *
1827
	 * Example:
1828
	 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1829
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1830
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1831
	 *
1832
	 * @return array
1833
	 */
1834 View Code Duplication
	public function tableNames() {
1835
		$inArray = func_get_args();
1836
		$retVal = [];
1837
1838
		foreach ( $inArray as $name ) {
1839
			$retVal[$name] = $this->tableName( $name );
1840
		}
1841
1842
		return $retVal;
1843
	}
1844
1845
	/**
1846
	 * Fetch a number of table names into an zero-indexed numerical array
1847
	 * This is handy when you need to construct SQL for joins
1848
	 *
1849
	 * Example:
1850
	 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1851
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1852
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1853
	 *
1854
	 * @return array
1855
	 */
1856 View Code Duplication
	public function tableNamesN() {
1857
		$inArray = func_get_args();
1858
		$retVal = [];
1859
1860
		foreach ( $inArray as $name ) {
1861
			$retVal[] = $this->tableName( $name );
1862
		}
1863
1864
		return $retVal;
1865
	}
1866
1867
	/**
1868
	 * Get an aliased table name
1869
	 * e.g. tableName AS newTableName
1870
	 *
1871
	 * @param string $name Table name, see tableName()
1872
	 * @param string|bool $alias Alias (optional)
1873
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1874
	 */
1875
	public function tableNameWithAlias( $name, $alias = false ) {
1876
		if ( !$alias || $alias == $name ) {
1877
			return $this->tableName( $name );
1878
		} else {
1879
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1875 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...
1880
		}
1881
	}
1882
1883
	/**
1884
	 * Gets an array of aliased table names
1885
	 *
1886
	 * @param array $tables [ [alias] => table ]
1887
	 * @return string[] See tableNameWithAlias()
1888
	 */
1889
	public function tableNamesWithAlias( $tables ) {
1890
		$retval = [];
1891
		foreach ( $tables as $alias => $table ) {
1892
			if ( is_numeric( $alias ) ) {
1893
				$alias = $table;
1894
			}
1895
			$retval[] = $this->tableNameWithAlias( $table, $alias );
1896
		}
1897
1898
		return $retval;
1899
	}
1900
1901
	/**
1902
	 * Get an aliased field name
1903
	 * e.g. fieldName AS newFieldName
1904
	 *
1905
	 * @param string $name Field name
1906
	 * @param string|bool $alias Alias (optional)
1907
	 * @return string SQL name for aliased field. Will not alias a field to its own name
1908
	 */
1909
	public function fieldNameWithAlias( $name, $alias = false ) {
1910
		if ( !$alias || (string)$alias === (string)$name ) {
1911
			return $name;
1912
		} else {
1913
			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 1909 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...
1914
		}
1915
	}
1916
1917
	/**
1918
	 * Gets an array of aliased field names
1919
	 *
1920
	 * @param array $fields [ [alias] => field ]
1921
	 * @return string[] See fieldNameWithAlias()
1922
	 */
1923
	public function fieldNamesWithAlias( $fields ) {
1924
		$retval = [];
1925
		foreach ( $fields as $alias => $field ) {
1926
			if ( is_numeric( $alias ) ) {
1927
				$alias = $field;
1928
			}
1929
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
1930
		}
1931
1932
		return $retval;
1933
	}
1934
1935
	/**
1936
	 * Get the aliased table name clause for a FROM clause
1937
	 * which might have a JOIN and/or USE INDEX clause
1938
	 *
1939
	 * @param array $tables ( [alias] => table )
1940
	 * @param array $use_index Same as for select()
1941
	 * @param array $join_conds Same as for select()
1942
	 * @return string
1943
	 */
1944
	protected function tableNamesWithUseIndexOrJOIN(
1945
		$tables, $use_index = [], $join_conds = []
1946
	) {
1947
		$ret = [];
1948
		$retJOIN = [];
1949
		$use_index = (array)$use_index;
1950
		$join_conds = (array)$join_conds;
1951
1952
		foreach ( $tables as $alias => $table ) {
1953
			if ( !is_string( $alias ) ) {
1954
				// No alias? Set it equal to the table name
1955
				$alias = $table;
1956
			}
1957
			// Is there a JOIN clause for this table?
1958
			if ( isset( $join_conds[$alias] ) ) {
1959
				list( $joinType, $conds ) = $join_conds[$alias];
1960
				$tableClause = $joinType;
1961
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1962
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1963
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1964
					if ( $use != '' ) {
1965
						$tableClause .= ' ' . $use;
1966
					}
1967
				}
1968
				$on = $this->makeList( (array)$conds, LIST_AND );
1969
				if ( $on != '' ) {
1970
					$tableClause .= ' ON (' . $on . ')';
1971
				}
1972
1973
				$retJOIN[] = $tableClause;
1974
			} elseif ( isset( $use_index[$alias] ) ) {
1975
				// Is there an INDEX clause for this table?
1976
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1977
				$tableClause .= ' ' . $this->useIndexClause(
1978
					implode( ',', (array)$use_index[$alias] )
1979
				);
1980
1981
				$ret[] = $tableClause;
1982
			} else {
1983
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1984
1985
				$ret[] = $tableClause;
1986
			}
1987
		}
1988
1989
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
1990
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
1991
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
1992
1993
		// Compile our final table clause
1994
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
1995
	}
1996
1997
	/**
1998
	 * Get the name of an index in a given table.
1999
	 *
2000
	 * @param string $index
2001
	 * @return string
2002
	 */
2003
	protected function indexName( $index ) {
2004
		// Backwards-compatibility hack
2005
		$renamed = [
2006
			'ar_usertext_timestamp' => 'usertext_timestamp',
2007
			'un_user_id' => 'user_id',
2008
			'un_user_ip' => 'user_ip',
2009
		];
2010
2011
		if ( isset( $renamed[$index] ) ) {
2012
			return $renamed[$index];
2013
		} else {
2014
			return $index;
2015
		}
2016
	}
2017
2018
	public function addQuotes( $s ) {
2019
		if ( $s instanceof Blob ) {
2020
			$s = $s->fetch();
2021
		}
2022
		if ( $s === null ) {
2023
			return 'NULL';
2024
		} else {
2025
			# This will also quote numeric values. This should be harmless,
2026
			# and protects against weird problems that occur when they really
2027
			# _are_ strings such as article titles and string->number->string
2028
			# conversion is not 1:1.
2029
			return "'" . $this->strencode( $s ) . "'";
2030
		}
2031
	}
2032
2033
	/**
2034
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
2035
	 * MySQL uses `backticks` while basically everything else uses double quotes.
2036
	 * Since MySQL is the odd one out here the double quotes are our generic
2037
	 * and we implement backticks in DatabaseMysql.
2038
	 *
2039
	 * @param string $s
2040
	 * @return string
2041
	 */
2042
	public function addIdentifierQuotes( $s ) {
2043
		return '"' . str_replace( '"', '""', $s ) . '"';
2044
	}
2045
2046
	/**
2047
	 * Returns if the given identifier looks quoted or not according to
2048
	 * the database convention for quoting identifiers .
2049
	 *
2050
	 * @note Do not use this to determine if untrusted input is safe.
2051
	 *   A malicious user can trick this function.
2052
	 * @param string $name
2053
	 * @return bool
2054
	 */
2055
	public function isQuotedIdentifier( $name ) {
2056
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2057
	}
2058
2059
	/**
2060
	 * @param string $s
2061
	 * @return string
2062
	 */
2063
	protected function escapeLikeInternal( $s ) {
2064
		return addcslashes( $s, '\%_' );
2065
	}
2066
2067
	public function buildLike() {
2068
		$params = func_get_args();
2069
2070
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2071
			$params = $params[0];
2072
		}
2073
2074
		$s = '';
2075
2076
		foreach ( $params as $value ) {
2077
			if ( $value instanceof LikeMatch ) {
2078
				$s .= $value->toString();
2079
			} else {
2080
				$s .= $this->escapeLikeInternal( $value );
2081
			}
2082
		}
2083
2084
		return " LIKE {$this->addQuotes( $s )} ";
2085
	}
2086
2087
	public function anyChar() {
2088
		return new LikeMatch( '_' );
2089
	}
2090
2091
	public function anyString() {
2092
		return new LikeMatch( '%' );
2093
	}
2094
2095
	public function nextSequenceValue( $seqName ) {
2096
		return null;
2097
	}
2098
2099
	/**
2100
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2101
	 * is only needed because a) MySQL must be as efficient as possible due to
2102
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2103
	 * which index to pick. Anyway, other databases might have different
2104
	 * indexes on a given table. So don't bother overriding this unless you're
2105
	 * MySQL.
2106
	 * @param string $index
2107
	 * @return string
2108
	 */
2109
	public function useIndexClause( $index ) {
2110
		return '';
2111
	}
2112
2113
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2114
		$quotedTable = $this->tableName( $table );
2115
2116
		if ( count( $rows ) == 0 ) {
2117
			return;
2118
		}
2119
2120
		# Single row case
2121
		if ( !is_array( reset( $rows ) ) ) {
2122
			$rows = [ $rows ];
2123
		}
2124
2125
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2126
		foreach ( $rows as $row ) {
2127
			# Delete rows which collide
2128
			if ( $uniqueIndexes ) {
2129
				$sql = "DELETE FROM $quotedTable WHERE ";
2130
				$first = true;
2131
				foreach ( $uniqueIndexes as $index ) {
2132
					if ( $first ) {
2133
						$first = false;
2134
						$sql .= '( ';
2135
					} else {
2136
						$sql .= ' ) OR ( ';
2137
					}
2138
					if ( is_array( $index ) ) {
2139
						$first2 = true;
2140
						foreach ( $index as $col ) {
2141
							if ( $first2 ) {
2142
								$first2 = false;
2143
							} else {
2144
								$sql .= ' AND ';
2145
							}
2146
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2147
						}
2148
					} else {
2149
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2150
					}
2151
				}
2152
				$sql .= ' )';
2153
				$this->query( $sql, $fname );
2154
			}
2155
2156
			# Now insert the row
2157
			$this->insert( $table, $row, $fname );
2158
		}
2159
	}
2160
2161
	/**
2162
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2163
	 * statement.
2164
	 *
2165
	 * @param string $table Table name
2166
	 * @param array|string $rows Row(s) to insert
2167
	 * @param string $fname Caller function name
2168
	 *
2169
	 * @return ResultWrapper
2170
	 */
2171
	protected function nativeReplace( $table, $rows, $fname ) {
2172
		$table = $this->tableName( $table );
2173
2174
		# Single row case
2175
		if ( !is_array( reset( $rows ) ) ) {
2176
			$rows = [ $rows ];
2177
		}
2178
2179
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2180
		$first = true;
2181
2182 View Code Duplication
		foreach ( $rows as $row ) {
2183
			if ( $first ) {
2184
				$first = false;
2185
			} else {
2186
				$sql .= ',';
2187
			}
2188
2189
			$sql .= '(' . $this->makeList( $row ) . ')';
2190
		}
2191
2192
		return $this->query( $sql, $fname );
2193
	}
2194
2195
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2196
		$fname = __METHOD__
2197
	) {
2198
		if ( !count( $rows ) ) {
2199
			return true; // nothing to do
2200
		}
2201
2202
		if ( !is_array( reset( $rows ) ) ) {
2203
			$rows = [ $rows ];
2204
		}
2205
2206
		if ( count( $uniqueIndexes ) ) {
2207
			$clauses = []; // list WHERE clauses that each identify a single row
2208
			foreach ( $rows as $row ) {
2209
				foreach ( $uniqueIndexes as $index ) {
2210
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2211
					$rowKey = []; // unique key to this row
2212
					foreach ( $index as $column ) {
2213
						$rowKey[$column] = $row[$column];
2214
					}
2215
					$clauses[] = $this->makeList( $rowKey, LIST_AND );
2216
				}
2217
			}
2218
			$where = [ $this->makeList( $clauses, LIST_OR ) ];
2219
		} else {
2220
			$where = false;
2221
		}
2222
2223
		$useTrx = !$this->mTrxLevel;
2224
		if ( $useTrx ) {
2225
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2226
		}
2227
		try {
2228
			# Update any existing conflicting row(s)
2229
			if ( $where !== false ) {
2230
				$ok = $this->update( $table, $set, $where, $fname );
2231
			} else {
2232
				$ok = true;
2233
			}
2234
			# Now insert any non-conflicting row(s)
2235
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2236
		} catch ( Exception $e ) {
2237
			if ( $useTrx ) {
2238
				$this->rollback( $fname );
2239
			}
2240
			throw $e;
2241
		}
2242
		if ( $useTrx ) {
2243
			$this->commit( $fname, self::TRANSACTION_INTERNAL );
2244
		}
2245
2246
		return $ok;
2247
	}
2248
2249 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2250
		$fname = __METHOD__
2251
	) {
2252
		if ( !$conds ) {
2253
			throw new DBUnexpectedError( $this,
2254
				'DatabaseBase::deleteJoin() called with empty $conds' );
2255
		}
2256
2257
		$delTable = $this->tableName( $delTable );
2258
		$joinTable = $this->tableName( $joinTable );
2259
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2260
		if ( $conds != '*' ) {
2261
			$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2262
		}
2263
		$sql .= ')';
2264
2265
		$this->query( $sql, $fname );
2266
	}
2267
2268
	/**
2269
	 * Returns the size of a text field, or -1 for "unlimited"
2270
	 *
2271
	 * @param string $table
2272
	 * @param string $field
2273
	 * @return int
2274
	 */
2275
	public function textFieldSize( $table, $field ) {
2276
		$table = $this->tableName( $table );
2277
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2278
		$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2279
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, 'DatabaseBase::textFieldSize') on line 2278 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...
2280
2281
		$m = [];
2282
2283
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2284
			$size = $m[1];
2285
		} else {
2286
			$size = -1;
2287
		}
2288
2289
		return $size;
2290
	}
2291
2292
	/**
2293
	 * A string to insert into queries to show that they're low-priority, like
2294
	 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2295
	 * string and nothing bad should happen.
2296
	 *
2297
	 * @return string Returns the text of the low priority option if it is
2298
	 *   supported, or a blank string otherwise
2299
	 */
2300
	public function lowPriorityOption() {
2301
		return '';
2302
	}
2303
2304
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2305
		if ( !$conds ) {
2306
			throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2307
		}
2308
2309
		$table = $this->tableName( $table );
2310
		$sql = "DELETE FROM $table";
2311
2312 View Code Duplication
		if ( $conds != '*' ) {
2313
			if ( is_array( $conds ) ) {
2314
				$conds = $this->makeList( $conds, LIST_AND );
2315
			}
2316
			$sql .= ' WHERE ' . $conds;
2317
		}
2318
2319
		return $this->query( $sql, $fname );
2320
	}
2321
2322
	public function insertSelect( $destTable, $srcTable, $varMap, $conds,
2323
		$fname = __METHOD__,
2324
		$insertOptions = [], $selectOptions = []
2325
	) {
2326
		$destTable = $this->tableName( $destTable );
2327
2328
		if ( !is_array( $insertOptions ) ) {
2329
			$insertOptions = [ $insertOptions ];
2330
		}
2331
2332
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2333
2334
		if ( !is_array( $selectOptions ) ) {
2335
			$selectOptions = [ $selectOptions ];
2336
		}
2337
2338
		list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
2339
2340 View Code Duplication
		if ( is_array( $srcTable ) ) {
2341
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2342
		} else {
2343
			$srcTable = $this->tableName( $srcTable );
2344
		}
2345
2346
		$sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2347
			" SELECT $startOpts " . implode( ',', $varMap ) .
2348
			" FROM $srcTable $useIndex ";
2349
2350 View Code Duplication
		if ( $conds != '*' ) {
2351
			if ( is_array( $conds ) ) {
2352
				$conds = $this->makeList( $conds, LIST_AND );
2353
			}
2354
			$sql .= " WHERE $conds";
2355
		}
2356
2357
		$sql .= " $tailOpts";
2358
2359
		return $this->query( $sql, $fname );
2360
	}
2361
2362
	/**
2363
	 * Construct a LIMIT query with optional offset. This is used for query
2364
	 * pages. The SQL should be adjusted so that only the first $limit rows
2365
	 * are returned. If $offset is provided as well, then the first $offset
2366
	 * rows should be discarded, and the next $limit rows should be returned.
2367
	 * If the result of the query is not ordered, then the rows to be returned
2368
	 * are theoretically arbitrary.
2369
	 *
2370
	 * $sql is expected to be a SELECT, if that makes a difference.
2371
	 *
2372
	 * The version provided by default works in MySQL and SQLite. It will very
2373
	 * likely need to be overridden for most other DBMSes.
2374
	 *
2375
	 * @param string $sql SQL query we will append the limit too
2376
	 * @param int $limit The SQL limit
2377
	 * @param int|bool $offset The SQL offset (default false)
2378
	 * @throws DBUnexpectedError
2379
	 * @return string
2380
	 */
2381
	public function limitResult( $sql, $limit, $offset = false ) {
2382
		if ( !is_numeric( $limit ) ) {
2383
			throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2384
		}
2385
2386
		return "$sql LIMIT "
2387
			. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2388
			. "{$limit} ";
2389
	}
2390
2391
	public function unionSupportsOrderAndLimit() {
2392
		return true; // True for almost every DB supported
2393
	}
2394
2395
	public function unionQueries( $sqls, $all ) {
2396
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2397
2398
		return '(' . implode( $glue, $sqls ) . ')';
2399
	}
2400
2401
	public function conditional( $cond, $trueVal, $falseVal ) {
2402
		if ( is_array( $cond ) ) {
2403
			$cond = $this->makeList( $cond, LIST_AND );
2404
		}
2405
2406
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2407
	}
2408
2409
	public function strreplace( $orig, $old, $new ) {
2410
		return "REPLACE({$orig}, {$old}, {$new})";
2411
	}
2412
2413
	public function getServerUptime() {
2414
		return 0;
2415
	}
2416
2417
	public function wasDeadlock() {
2418
		return false;
2419
	}
2420
2421
	public function wasLockTimeout() {
2422
		return false;
2423
	}
2424
2425
	public function wasErrorReissuable() {
2426
		return false;
2427
	}
2428
2429
	public function wasReadOnlyError() {
2430
		return false;
2431
	}
2432
2433
	/**
2434
	 * Determines if the given query error was a connection drop
2435
	 * STUB
2436
	 *
2437
	 * @param integer|string $errno
2438
	 * @return bool
2439
	 */
2440
	public function wasConnectionError( $errno ) {
2441
		return false;
2442
	}
2443
2444
	/**
2445
	 * Perform a deadlock-prone transaction.
2446
	 *
2447
	 * This function invokes a callback function to perform a set of write
2448
	 * queries. If a deadlock occurs during the processing, the transaction
2449
	 * will be rolled back and the callback function will be called again.
2450
	 *
2451
	 * Avoid using this method outside of Job or Maintenance classes.
2452
	 *
2453
	 * Usage:
2454
	 *   $dbw->deadlockLoop( callback, ... );
2455
	 *
2456
	 * Extra arguments are passed through to the specified callback function.
2457
	 * This method requires that no transactions are already active to avoid
2458
	 * causing premature commits or exceptions.
2459
	 *
2460
	 * Returns whatever the callback function returned on its successful,
2461
	 * iteration, or false on error, for example if the retry limit was
2462
	 * reached.
2463
	 *
2464
	 * @return mixed
2465
	 * @throws DBUnexpectedError
2466
	 * @throws Exception
2467
	 */
2468
	public function deadlockLoop() {
2469
		$args = func_get_args();
2470
		$function = array_shift( $args );
2471
		$tries = self::DEADLOCK_TRIES;
2472
2473
		$this->begin( __METHOD__ );
2474
2475
		$retVal = null;
2476
		/** @var Exception $e */
2477
		$e = null;
2478
		do {
2479
			try {
2480
				$retVal = call_user_func_array( $function, $args );
2481
				break;
2482
			} catch ( DBQueryError $e ) {
2483
				if ( $this->wasDeadlock() ) {
2484
					// Retry after a randomized delay
2485
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2486
				} else {
2487
					// Throw the error back up
2488
					throw $e;
2489
				}
2490
			}
2491
		} while ( --$tries > 0 );
2492
2493
		if ( $tries <= 0 ) {
2494
			// Too many deadlocks; give up
2495
			$this->rollback( __METHOD__ );
2496
			throw $e;
2497
		} else {
2498
			$this->commit( __METHOD__ );
2499
2500
			return $retVal;
2501
		}
2502
	}
2503
2504
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2505
		# Real waits are implemented in the subclass.
2506
		return 0;
2507
	}
2508
2509
	public function getSlavePos() {
2510
		# Stub
2511
		return false;
2512
	}
2513
2514
	public function getMasterPos() {
2515
		# Stub
2516
		return false;
2517
	}
2518
2519
	public function serverIsReadOnly() {
2520
		return false;
2521
	}
2522
2523
	final public function onTransactionResolution( callable $callback ) {
2524
		if ( !$this->mTrxLevel ) {
2525
			throw new DBUnexpectedError( $this, "No transaction is active." );
2526
		}
2527
		$this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
2528
	}
2529
2530
	final public function onTransactionIdle( callable $callback ) {
2531
		$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2532
		if ( !$this->mTrxLevel ) {
2533
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2534
		}
2535
	}
2536
2537
	final public function onTransactionPreCommitOrIdle( callable $callback ) {
2538
		if ( $this->mTrxLevel ) {
2539
			$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2540
		} else {
2541
			// If no transaction is active, then make one for this callback
2542
			$this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
2543
			try {
2544
				call_user_func( $callback );
2545
				$this->commit( __METHOD__ );
2546
			} catch ( Exception $e ) {
2547
				$this->rollback( __METHOD__ );
2548
				throw $e;
2549
			}
2550
		}
2551
	}
2552
2553
	/**
2554
	 * Whether to disable running of post-commit callbacks
2555
	 *
2556
	 * This method should not be used outside of Database/LoadBalancer
2557
	 *
2558
	 * @param bool $suppress
2559
	 * @since 1.28
2560
	 */
2561
	final public function setPostCommitCallbackSupression( $suppress ) {
2562
		$this->suppressPostCommitCallbacks = $suppress;
2563
	}
2564
2565
	/**
2566
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2567
	 *
2568
	 * This method should not be used outside of Database/LoadBalancer
2569
	 *
2570
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2571
	 * @since 1.20
2572
	 * @throws Exception
2573
	 */
2574
	public function runOnTransactionIdleCallbacks( $trigger ) {
2575
		if ( $this->suppressPostCommitCallbacks ) {
2576
			return;
2577
		}
2578
2579
		$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2580
		/** @var Exception $e */
2581
		$e = null; // first exception
2582
		do { // callbacks may add callbacks :)
2583
			$callbacks = array_merge(
2584
				$this->mTrxIdleCallbacks,
2585
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2586
			);
2587
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2588
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2589
			foreach ( $callbacks as $callback ) {
2590
				try {
2591
					list( $phpCallback ) = $callback;
2592
					$this->clearFlag( DBO_TRX ); // make each query its own transaction
2593
					call_user_func_array( $phpCallback, [ $trigger ] );
2594
					if ( $autoTrx ) {
2595
						$this->setFlag( DBO_TRX ); // restore automatic begin()
2596
					} else {
2597
						$this->clearFlag( DBO_TRX ); // restore auto-commit
2598
					}
2599
				} catch ( Exception $ex ) {
2600
					MWExceptionHandler::logException( $ex );
2601
					$e = $e ?: $ex;
2602
					// Some callbacks may use startAtomic/endAtomic, so make sure
2603
					// their transactions are ended so other callbacks don't fail
2604
					if ( $this->trxLevel() ) {
2605
						$this->rollback( __METHOD__ );
2606
					}
2607
				}
2608
			}
2609
		} while ( count( $this->mTrxIdleCallbacks ) );
2610
2611
		if ( $e instanceof Exception ) {
2612
			throw $e; // re-throw any first exception
2613
		}
2614
	}
2615
2616
	/**
2617
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2618
	 *
2619
	 * This method should not be used outside of Database/LoadBalancer
2620
	 *
2621
	 * @since 1.22
2622
	 * @throws Exception
2623
	 */
2624
	public function runOnTransactionPreCommitCallbacks() {
2625
		$e = null; // first exception
2626
		do { // callbacks may add callbacks :)
2627
			$callbacks = $this->mTrxPreCommitCallbacks;
2628
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2629
			foreach ( $callbacks as $callback ) {
2630
				try {
2631
					list( $phpCallback ) = $callback;
2632
					call_user_func( $phpCallback );
2633
				} catch ( Exception $ex ) {
2634
					MWExceptionHandler::logException( $ex );
2635
					$e = $e ?: $ex;
2636
				}
2637
			}
2638
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2639
2640
		if ( $e instanceof Exception ) {
2641
			throw $e; // re-throw any first exception
2642
		}
2643
	}
2644
2645
	final public function startAtomic( $fname = __METHOD__ ) {
2646
		if ( !$this->mTrxLevel ) {
2647
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2648
			$this->mTrxAutomatic = true;
2649
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2650
			// in all changes being in one transaction to keep requests transactional.
2651
			if ( !$this->getFlag( DBO_TRX ) ) {
2652
				$this->mTrxAutomaticAtomic = true;
2653
			}
2654
		}
2655
2656
		$this->mTrxAtomicLevels[] = $fname;
2657
	}
2658
2659
	final public function endAtomic( $fname = __METHOD__ ) {
2660
		if ( !$this->mTrxLevel ) {
2661
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2662
		}
2663
		if ( !$this->mTrxAtomicLevels ||
2664
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2665
		) {
2666
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2667
		}
2668
2669
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2670
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2671
		}
2672
	}
2673
2674
	final public function doAtomicSection( $fname, callable $callback ) {
2675
		$this->startAtomic( $fname );
2676
		try {
2677
			$res = call_user_func_array( $callback, [ $this, $fname ] );
2678
		} catch ( Exception $e ) {
2679
			$this->rollback( $fname );
2680
			throw $e;
2681
		}
2682
		$this->endAtomic( $fname );
2683
2684
		return $res;
2685
	}
2686
2687
	final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2688
		// Protect against mismatched atomic section, transaction nesting, and snapshot loss
2689
		if ( $this->mTrxLevel ) {
2690
			if ( $this->mTrxAtomicLevels ) {
2691
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2692
				$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2693
				throw new DBUnexpectedError( $this, $msg );
2694
			} elseif ( !$this->mTrxAutomatic ) {
2695
				$msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2696
				throw new DBUnexpectedError( $this, $msg );
2697
			} else {
2698
				// @TODO: make this an exception at some point
2699
				$msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2700
				wfLogDBError( $msg );
2701
				return; // join the main transaction set
2702
			}
2703
		} elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2704
			// @TODO: make this an exception at some point
2705
			wfLogDBError( "$fname: Implicit transaction expected (DBO_TRX set)." );
2706
			return; // let any writes be in the main transaction
2707
		}
2708
2709
		// Avoid fatals if close() was called
2710
		$this->assertOpen();
2711
2712
		$this->doBegin( $fname );
2713
		$this->mTrxTimestamp = microtime( true );
2714
		$this->mTrxFname = $fname;
2715
		$this->mTrxDoneWrites = false;
2716
		$this->mTrxAutomatic = false;
2717
		$this->mTrxAutomaticAtomic = false;
2718
		$this->mTrxAtomicLevels = [];
2719
		$this->mTrxShortId = wfRandomString( 12 );
2720
		$this->mTrxWriteDuration = 0.0;
2721
		$this->mTrxWriteCallers = [];
2722
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2723
		// Get an estimate of the slave lag before then, treating estimate staleness
2724
		// as lag itself just to be safe
2725
		$status = $this->getApproximateLagStatus();
2726
		$this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
0 ignored issues
show
Documentation Bug introduced by
It seems like $status['lag'] + (microt...ue) - $status['since']) can also be of type integer. However, the property $mTrxSlaveLag is declared as type double. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2727
	}
2728
2729
	/**
2730
	 * Issues the BEGIN command to the database server.
2731
	 *
2732
	 * @see DatabaseBase::begin()
2733
	 * @param string $fname
2734
	 */
2735
	protected function doBegin( $fname ) {
2736
		$this->query( 'BEGIN', $fname );
2737
		$this->mTrxLevel = 1;
2738
	}
2739
2740
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2741
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2742
			// There are still atomic sections open. This cannot be ignored
2743
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2744
			throw new DBUnexpectedError(
2745
				$this,
2746
				"$fname: Got COMMIT while atomic sections $levels are still open."
2747
			);
2748
		}
2749
2750
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2751
			if ( !$this->mTrxLevel ) {
2752
				return; // nothing to do
2753
			} elseif ( !$this->mTrxAutomatic ) {
2754
				throw new DBUnexpectedError(
2755
					$this,
2756
					"$fname: Flushing an explicit transaction, getting out of sync."
2757
				);
2758
			}
2759
		} else {
2760
			if ( !$this->mTrxLevel ) {
2761
				wfWarn( "$fname: No transaction to commit, something got out of sync." );
2762
				return; // nothing to do
2763
			} elseif ( $this->mTrxAutomatic ) {
2764
				// @TODO: make this an exception at some point
2765
				wfLogDBError( "$fname: Explicit commit of implicit transaction." );
2766
				return; // wait for the main transaction set commit round
2767
			}
2768
		}
2769
2770
		// Avoid fatals if close() was called
2771
		$this->assertOpen();
2772
2773
		$this->runOnTransactionPreCommitCallbacks();
2774
		$writeTime = $this->pendingWriteQueryDuration();
2775
		$this->doCommit( $fname );
2776
		if ( $this->mTrxDoneWrites ) {
2777
			$this->mDoneWrites = microtime( true );
0 ignored issues
show
Documentation Bug introduced by
The property $mDoneWrites was declared of type boolean, but microtime(true) is of type double. Maybe add a type cast?

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

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

$answer = 42;

$correct = false;

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

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

Consider the follow example

<?php

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

    return false;
}

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

Loading history...
2780
		}
2781
2782
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2783
	}
2784
2785
	/**
2786
	 * Issues the COMMIT command to the database server.
2787
	 *
2788
	 * @see DatabaseBase::commit()
2789
	 * @param string $fname
2790
	 */
2791
	protected function doCommit( $fname ) {
2792
		if ( $this->mTrxLevel ) {
2793
			$this->query( 'COMMIT', $fname );
2794
			$this->mTrxLevel = 0;
2795
		}
2796
	}
2797
2798
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
2799
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2800
			if ( !$this->mTrxLevel ) {
2801
				return; // nothing to do
2802
			}
2803
		} else {
2804
			if ( !$this->mTrxLevel ) {
2805
				wfWarn( "$fname: No transaction to rollback, something got out of sync." );
2806
				return; // nothing to do
2807
			} elseif ( $this->getFlag( DBO_TRX ) ) {
2808
				throw new DBUnexpectedError(
2809
					$this,
2810
					"$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
2811
				);
2812
			}
2813
		}
2814
2815
		// Avoid fatals if close() was called
2816
		$this->assertOpen();
2817
2818
		$this->doRollback( $fname );
2819
		$this->mTrxAtomicLevels = [];
2820
		if ( $this->mTrxDoneWrites ) {
2821
			$this->getTransactionProfiler()->transactionWritingOut(
2822
				$this->mServer, $this->mDBname, $this->mTrxShortId );
2823
		}
2824
2825
		$this->mTrxIdleCallbacks = []; // clear
2826
		$this->mTrxPreCommitCallbacks = []; // clear
2827
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2828
	}
2829
2830
	/**
2831
	 * Issues the ROLLBACK command to the database server.
2832
	 *
2833
	 * @see DatabaseBase::rollback()
2834
	 * @param string $fname
2835
	 */
2836
	protected function doRollback( $fname ) {
2837
		if ( $this->mTrxLevel ) {
2838
			# Disconnects cause rollback anyway, so ignore those errors
2839
			$ignoreErrors = true;
2840
			$this->query( 'ROLLBACK', $fname, $ignoreErrors );
2841
			$this->mTrxLevel = 0;
2842
		}
2843
	}
2844
2845
	public function explicitTrxActive() {
2846
		return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
2847
	}
2848
2849
	/**
2850
	 * Creates a new table with structure copied from existing table
2851
	 * Note that unlike most database abstraction functions, this function does not
2852
	 * automatically append database prefix, because it works at a lower
2853
	 * abstraction level.
2854
	 * The table names passed to this function shall not be quoted (this
2855
	 * function calls addIdentifierQuotes when needed).
2856
	 *
2857
	 * @param string $oldName Name of table whose structure should be copied
2858
	 * @param string $newName Name of table to be created
2859
	 * @param bool $temporary Whether the new table should be temporary
2860
	 * @param string $fname Calling function name
2861
	 * @throws MWException
2862
	 * @return bool True if operation was successful
2863
	 */
2864
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
2865
		$fname = __METHOD__
2866
	) {
2867
		throw new MWException(
2868
			'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
2869
	}
2870
2871
	function listTables( $prefix = null, $fname = __METHOD__ ) {
2872
		throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
2873
	}
2874
2875
	/**
2876
	 * Reset the views process cache set by listViews()
2877
	 * @since 1.22
2878
	 */
2879
	final public function clearViewsCache() {
2880
		$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...
2881
	}
2882
2883
	/**
2884
	 * Lists all the VIEWs in the database
2885
	 *
2886
	 * For caching purposes the list of all views should be stored in
2887
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
2888
	 *
2889
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
2890
	 * @param string $fname Name of calling function
2891
	 * @throws MWException
2892
	 * @return array
2893
	 * @since 1.22
2894
	 */
2895
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
2896
		throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
2897
	}
2898
2899
	/**
2900
	 * Differentiates between a TABLE and a VIEW
2901
	 *
2902
	 * @param string $name Name of the database-structure to test.
2903
	 * @throws MWException
2904
	 * @return bool
2905
	 * @since 1.22
2906
	 */
2907
	public function isView( $name ) {
2908
		throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
2909
	}
2910
2911
	public function timestamp( $ts = 0 ) {
2912
		return wfTimestamp( TS_MW, $ts );
2913
	}
2914
2915
	public function timestampOrNull( $ts = null ) {
2916
		if ( is_null( $ts ) ) {
2917
			return null;
2918
		} else {
2919
			return $this->timestamp( $ts );
2920
		}
2921
	}
2922
2923
	/**
2924
	 * Take the result from a query, and wrap it in a ResultWrapper if
2925
	 * necessary. Boolean values are passed through as is, to indicate success
2926
	 * of write queries or failure.
2927
	 *
2928
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
2929
	 * resource, and it was necessary to call this function to convert it to
2930
	 * a wrapper. Nowadays, raw database objects are never exposed to external
2931
	 * callers, so this is unnecessary in external code.
2932
	 *
2933
	 * @param bool|ResultWrapper|resource|object $result
2934
	 * @return bool|ResultWrapper
2935
	 */
2936
	protected function resultObject( $result ) {
2937
		if ( !$result ) {
2938
			return false;
2939
		} elseif ( $result instanceof ResultWrapper ) {
2940
			return $result;
2941
		} elseif ( $result === true ) {
2942
			// Successful write query
2943
			return $result;
2944
		} else {
2945
			return new ResultWrapper( $this, $result );
2946
		}
2947
	}
2948
2949
	public function ping() {
2950
		if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
2951
			return true;
2952
		}
2953
		try {
2954
			// This will reconnect if possible, or error out if not
2955
			$this->query( "SELECT 1 AS ping", __METHOD__ );
2956
			return true;
2957
		} catch ( DBError $e ) {
2958
			return false;
2959
		}
2960
	}
2961
2962
	/**
2963
	 * @return bool
2964
	 */
2965
	protected function reconnect() {
2966
		$this->closeConnection();
2967
		$this->mOpened = false;
2968
		$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...
2969
		try {
2970
			$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
2971
			$this->lastPing = microtime( true );
2972
			$ok = true;
2973
		} catch ( DBConnectionError $e ) {
2974
			$ok = false;
2975
		}
2976
2977
		return $ok;
2978
	}
2979
2980
	public function getSessionLagStatus() {
2981
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
2982
	}
2983
2984
	/**
2985
	 * Get the slave lag when the current transaction started
2986
	 *
2987
	 * This is useful when transactions might use snapshot isolation
2988
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
2989
	 * is this lag plus transaction duration. If they don't, it is still
2990
	 * safe to be pessimistic. This returns null if there is no transaction.
2991
	 *
2992
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
2993
	 * @since 1.27
2994
	 */
2995
	public function getTransactionLagStatus() {
2996
		return $this->mTrxLevel
2997
			? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
2998
			: null;
2999
	}
3000
3001
	/**
3002
	 * Get a slave lag estimate for this server
3003
	 *
3004
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
3005
	 * @since 1.27
3006
	 */
3007
	public function getApproximateLagStatus() {
3008
		return [
3009
			'lag'   => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
3010
			'since' => microtime( true )
3011
		];
3012
	}
3013
3014
	/**
3015
	 * Merge the result of getSessionLagStatus() for several DBs
3016
	 * using the most pessimistic values to estimate the lag of
3017
	 * any data derived from them in combination
3018
	 *
3019
	 * This is information is useful for caching modules
3020
	 *
3021
	 * @see WANObjectCache::set()
3022
	 * @see WANObjectCache::getWithSetCallback()
3023
	 *
3024
	 * @param IDatabase $db1
3025
	 * @param IDatabase ...
3026
	 * @return array Map of values:
3027
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3028
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
3029
	 *   - pending: whether any of the DBs have uncommitted changes
3030
	 * @since 1.27
3031
	 */
3032
	public static function getCacheSetOptions( IDatabase $db1 ) {
3033
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3034
		foreach ( func_get_args() as $db ) {
3035
			/** @var IDatabase $db */
3036
			$status = $db->getSessionLagStatus();
3037
			if ( $status['lag'] === false ) {
3038
				$res['lag'] = false;
3039
			} elseif ( $res['lag'] !== false ) {
3040
				$res['lag'] = max( $res['lag'], $status['lag'] );
3041
			}
3042
			$res['since'] = min( $res['since'], $status['since'] );
3043
			$res['pending'] = $res['pending'] ?: $db->writesPending();
3044
		}
3045
3046
		return $res;
3047
	}
3048
3049
	public function getLag() {
3050
		return 0;
3051
	}
3052
3053
	function maxListLen() {
3054
		return 0;
3055
	}
3056
3057
	public function encodeBlob( $b ) {
3058
		return $b;
3059
	}
3060
3061
	public function decodeBlob( $b ) {
3062
		if ( $b instanceof Blob ) {
3063
			$b = $b->fetch();
3064
		}
3065
		return $b;
3066
	}
3067
3068
	public function setSessionOptions( array $options ) {
3069
	}
3070
3071
	/**
3072
	 * Read and execute SQL commands from a file.
3073
	 *
3074
	 * Returns true on success, error string or exception on failure (depending
3075
	 * on object's error ignore settings).
3076
	 *
3077
	 * @param string $filename File name to open
3078
	 * @param bool|callable $lineCallback Optional function called before reading each line
3079
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3080
	 * @param bool|string $fname Calling function name or false if name should be
3081
	 *   generated dynamically using $filename
3082
	 * @param bool|callable $inputCallback Optional function called for each
3083
	 *   complete line sent
3084
	 * @throws Exception|MWException
3085
	 * @return bool|string
3086
	 */
3087
	public function sourceFile(
3088
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3089
	) {
3090
		MediaWiki\suppressWarnings();
3091
		$fp = fopen( $filename, 'r' );
3092
		MediaWiki\restoreWarnings();
3093
3094
		if ( false === $fp ) {
3095
			throw new MWException( "Could not open \"{$filename}\".\n" );
3096
		}
3097
3098
		if ( !$fname ) {
3099
			$fname = __METHOD__ . "( $filename )";
3100
		}
3101
3102
		try {
3103
			$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 3088 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...
3104
		} catch ( Exception $e ) {
3105
			fclose( $fp );
3106
			throw $e;
3107
		}
3108
3109
		fclose( $fp );
3110
3111
		return $error;
3112
	}
3113
3114
	/**
3115
	 * Get the full path of a patch file. Originally based on archive()
3116
	 * from updaters.inc. Keep in mind this always returns a patch, as
3117
	 * it fails back to MySQL if no DB-specific patch can be found
3118
	 *
3119
	 * @param string $patch The name of the patch, like patch-something.sql
3120
	 * @return string Full path to patch file
3121
	 */
3122 View Code Duplication
	public function patchPath( $patch ) {
3123
		global $IP;
3124
3125
		$dbType = $this->getType();
3126
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
3127
			return "$IP/maintenance/$dbType/archives/$patch";
3128
		} else {
3129
			return "$IP/maintenance/archives/$patch";
3130
		}
3131
	}
3132
3133
	public function setSchemaVars( $vars ) {
3134
		$this->mSchemaVars = $vars;
3135
	}
3136
3137
	/**
3138
	 * Read and execute commands from an open file handle.
3139
	 *
3140
	 * Returns true on success, error string or exception on failure (depending
3141
	 * on object's error ignore settings).
3142
	 *
3143
	 * @param resource $fp File handle
3144
	 * @param bool|callable $lineCallback Optional function called before reading each query
3145
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3146
	 * @param string $fname Calling function name
3147
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3148
	 * @return bool|string
3149
	 */
3150
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3151
		$fname = __METHOD__, $inputCallback = false
3152
	) {
3153
		$cmd = '';
3154
3155
		while ( !feof( $fp ) ) {
3156
			if ( $lineCallback ) {
3157
				call_user_func( $lineCallback );
3158
			}
3159
3160
			$line = trim( fgets( $fp ) );
3161
3162
			if ( $line == '' ) {
3163
				continue;
3164
			}
3165
3166
			if ( '-' == $line[0] && '-' == $line[1] ) {
3167
				continue;
3168
			}
3169
3170
			if ( $cmd != '' ) {
3171
				$cmd .= ' ';
3172
			}
3173
3174
			$done = $this->streamStatementEnd( $cmd, $line );
3175
3176
			$cmd .= "$line\n";
3177
3178
			if ( $done || feof( $fp ) ) {
3179
				$cmd = $this->replaceVars( $cmd );
3180
3181
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3182
					$res = $this->query( $cmd, $fname );
3183
3184
					if ( $resultCallback ) {
3185
						call_user_func( $resultCallback, $res, $this );
3186
					}
3187
3188
					if ( false === $res ) {
3189
						$err = $this->lastError();
3190
3191
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3192
					}
3193
				}
3194
				$cmd = '';
3195
			}
3196
		}
3197
3198
		return true;
3199
	}
3200
3201
	/**
3202
	 * Called by sourceStream() to check if we've reached a statement end
3203
	 *
3204
	 * @param string $sql SQL assembled so far
3205
	 * @param string $newLine New line about to be added to $sql
3206
	 * @return bool Whether $newLine contains end of the statement
3207
	 */
3208
	public function streamStatementEnd( &$sql, &$newLine ) {
3209
		if ( $this->delimiter ) {
3210
			$prev = $newLine;
3211
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3212
			if ( $newLine != $prev ) {
3213
				return true;
3214
			}
3215
		}
3216
3217
		return false;
3218
	}
3219
3220
	/**
3221
	 * Database independent variable replacement. Replaces a set of variables
3222
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3223
	 *
3224
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3225
	 *
3226
	 * - '{$var}' should be used for text and is passed through the database's
3227
	 *   addQuotes method.
3228
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3229
	 *   It is passed through the database's addIdentifierQuotes method which
3230
	 *   can be overridden if the database uses something other than backticks.
3231
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3232
	 *   database's tableName method.
3233
	 * - / *i* / passes the name that follows through the database's indexName method.
3234
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3235
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3236
	 *
3237
	 * @param string $ins SQL statement to replace variables in
3238
	 * @return string The new SQL statement with variables replaced
3239
	 */
3240
	protected function replaceVars( $ins ) {
3241
		$vars = $this->getSchemaVars();
3242
		return preg_replace_callback(
3243
			'!
3244
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3245
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3246
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3247
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3248
			!x',
3249
			function ( $m ) use ( $vars ) {
3250
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3251
				// check for both nonexistent keys *and* the empty string.
3252
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3253
					if ( $m[1] === 'i' ) {
3254
						return $this->indexName( $m[2] );
3255
					} else {
3256
						return $this->tableName( $m[2] );
3257
					}
3258 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3259
					return $this->addQuotes( $vars[$m[3]] );
3260
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3261
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3262 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3263
					return $vars[$m[5]];
3264
				} else {
3265
					return $m[0];
3266
				}
3267
			},
3268
			$ins
3269
		);
3270
	}
3271
3272
	/**
3273
	 * Get schema variables. If none have been set via setSchemaVars(), then
3274
	 * use some defaults from the current object.
3275
	 *
3276
	 * @return array
3277
	 */
3278
	protected function getSchemaVars() {
3279
		if ( $this->mSchemaVars ) {
3280
			return $this->mSchemaVars;
3281
		} else {
3282
			return $this->getDefaultSchemaVars();
3283
		}
3284
	}
3285
3286
	/**
3287
	 * Get schema variables to use if none have been set via setSchemaVars().
3288
	 *
3289
	 * Override this in derived classes to provide variables for tables.sql
3290
	 * and SQL patch files.
3291
	 *
3292
	 * @return array
3293
	 */
3294
	protected function getDefaultSchemaVars() {
3295
		return [];
3296
	}
3297
3298
	public function lockIsFree( $lockName, $method ) {
3299
		return true;
3300
	}
3301
3302
	public function lock( $lockName, $method, $timeout = 5 ) {
3303
		$this->mNamedLocksHeld[$lockName] = 1;
3304
3305
		return true;
3306
	}
3307
3308
	public function unlock( $lockName, $method ) {
3309
		unset( $this->mNamedLocksHeld[$lockName] );
3310
3311
		return true;
3312
	}
3313
3314
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3315
		if ( $this->writesOrCallbacksPending() ) {
3316
			// This only flushes transactions to clear snapshots, not to write data
3317
			throw new DBUnexpectedError(
3318
				$this,
3319
				"$fname: Cannot COMMIT to clear snapshot because writes are pending."
3320
			);
3321
		}
3322
3323
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3324
			return null;
3325
		}
3326
3327
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3328
			if ( $this->trxLevel() ) {
3329
				// There is a good chance an exception was thrown, causing any early return
3330
				// from the caller. Let any error handler get a chance to issue rollback().
3331
				// If there isn't one, let the error bubble up and trigger server-side rollback.
3332
				$this->onTransactionResolution( function () use ( $lockKey, $fname ) {
3333
					$this->unlock( $lockKey, $fname );
3334
				} );
3335
			} else {
3336
				$this->unlock( $lockKey, $fname );
3337
			}
3338
		} );
3339
3340
		$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
3341
3342
		return $unlocker;
3343
	}
3344
3345
	public function namedLocksEnqueue() {
3346
		return false;
3347
	}
3348
3349
	/**
3350
	 * Lock specific tables
3351
	 *
3352
	 * @param array $read Array of tables to lock for read access
3353
	 * @param array $write Array of tables to lock for write access
3354
	 * @param string $method Name of caller
3355
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3356
	 * @return bool
3357
	 */
3358
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3359
		return true;
3360
	}
3361
3362
	/**
3363
	 * Unlock specific tables
3364
	 *
3365
	 * @param string $method The caller
3366
	 * @return bool
3367
	 */
3368
	public function unlockTables( $method ) {
3369
		return true;
3370
	}
3371
3372
	/**
3373
	 * Delete a table
3374
	 * @param string $tableName
3375
	 * @param string $fName
3376
	 * @return bool|ResultWrapper
3377
	 * @since 1.18
3378
	 */
3379 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3380
		if ( !$this->tableExists( $tableName, $fName ) ) {
3381
			return false;
3382
		}
3383
		$sql = "DROP TABLE " . $this->tableName( $tableName );
3384
		if ( $this->cascadingDeletes() ) {
3385
			$sql .= " CASCADE";
3386
		}
3387
3388
		return $this->query( $sql, $fName );
3389
	}
3390
3391
	/**
3392
	 * Get search engine class. All subclasses of this need to implement this
3393
	 * if they wish to use searching.
3394
	 *
3395
	 * @return string
3396
	 */
3397
	public function getSearchEngine() {
3398
		return 'SearchEngineDummy';
3399
	}
3400
3401
	public function getInfinity() {
3402
		return 'infinity';
3403
	}
3404
3405
	public function encodeExpiry( $expiry ) {
3406
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3407
			? $this->getInfinity()
3408
			: $this->timestamp( $expiry );
3409
	}
3410
3411
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3412
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3413
			? 'infinity'
3414
			: wfTimestamp( $format, $expiry );
3415
	}
3416
3417
	public function setBigSelects( $value = true ) {
3418
		// no-op
3419
	}
3420
3421
	public function isReadOnly() {
3422
		return ( $this->getReadOnlyReason() !== false );
3423
	}
3424
3425
	/**
3426
	 * @return string|bool Reason this DB is read-only or false if it is not
3427
	 */
3428
	protected function getReadOnlyReason() {
3429
		$reason = $this->getLBInfo( 'readOnlyReason' );
3430
3431
		return is_string( $reason ) ? $reason : false;
3432
	}
3433
3434
	/**
3435
	 * @since 1.19
3436
	 * @return string
3437
	 */
3438
	public function __toString() {
3439
		return (string)$this->mConn;
3440
	}
3441
3442
	/**
3443
	 * Run a few simple sanity checks
3444
	 */
3445
	public function __destruct() {
3446
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3447
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3448
		}
3449
		$danglingCallbacks = array_merge(
3450
			$this->mTrxIdleCallbacks,
3451
			$this->mTrxPreCommitCallbacks,
3452
			$this->mTrxEndCallbacks
3453
		);
3454
		if ( $danglingCallbacks ) {
3455
			$callers = [];
3456
			foreach ( $danglingCallbacks as $callbackInfo ) {
3457
				$callers[] = $callbackInfo[1];
3458
			}
3459
			$callers = implode( ', ', $callers );
3460
			trigger_error( "DB transaction callbacks still pending (from $callers)." );
3461
		}
3462
	}
3463
}
3464
3465
/**
3466
 * @since 1.27
3467
 */
3468
abstract class Database extends DatabaseBase {
3469
	// B/C until nothing type hints for DatabaseBase
3470
	// @TODO: finish renaming DatabaseBase => Database
3471
}
3472