Completed
Branch master (dc3656)
by
unknown
30:14
created

DatabaseBase::setPostCommitCallbackSupression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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