Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/objectcache/SqlBagOStuff.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Object caching using a SQL database.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Cache
22
 */
23
24
use \MediaWiki\MediaWikiServices;
25
26
/**
27
 * Class to store objects in the database
28
 *
29
 * @ingroup Cache
30
 */
31
class SqlBagOStuff extends BagOStuff {
32
	/** @var array[] (server index => server config) */
33
	protected $serverInfos;
34
	/** @var string[] (server index => tag/host name) */
35
	protected $serverTags;
36
	/** @var int */
37
	protected $numServers;
38
	/** @var int */
39
	protected $lastExpireAll = 0;
40
	/** @var int */
41
	protected $purgePeriod = 100;
42
	/** @var int */
43
	protected $shards = 1;
44
	/** @var string */
45
	protected $tableName = 'objectcache';
46
	/** @var bool */
47
	protected $replicaOnly = false;
48
	/** @var int */
49
	protected $syncTimeout = 3;
50
51
	/** @var LoadBalancer|null */
52
	protected $separateMainLB;
53
	/** @var array */
54
	protected $conns;
55
	/** @var array UNIX timestamps */
56
	protected $connFailureTimes = [];
57
	/** @var array Exceptions */
58
	protected $connFailureErrors = [];
59
60
	/**
61
	 * Constructor. Parameters are:
62
	 *   - server:      A server info structure in the format required by each
63
	 *                  element in $wgDBServers.
64
	 *
65
	 *   - servers:     An array of server info structures describing a set of database servers
66
	 *                  to distribute keys to. If this is specified, the "server" option will be
67
	 *                  ignored. If string keys are used, then they will be used for consistent
68
	 *                  hashing *instead* of the host name (from the server config). This is useful
69
	 *                  when a cluster is replicated to another site (with different host names)
70
	 *                  but each server has a corresponding replica in the other cluster.
71
	 *
72
	 *   - purgePeriod: The average number of object cache requests in between
73
	 *                  garbage collection operations, where expired entries
74
	 *                  are removed from the database. Or in other words, the
75
	 *                  reciprocal of the probability of purging on any given
76
	 *                  request. If this is set to zero, purging will never be
77
	 *                  done.
78
	 *
79
	 *   - tableName:   The table name to use, default is "objectcache".
80
	 *
81
	 *   - shards:      The number of tables to use for data storage on each server.
82
	 *                  If this is more than 1, table names will be formed in the style
83
	 *                  objectcacheNNN where NNN is the shard index, between 0 and
84
	 *                  shards-1. The number of digits will be the minimum number
85
	 *                  required to hold the largest shard index. Data will be
86
	 *                  distributed across all tables by key hash. This is for
87
	 *                  MySQL bugs 61735 and 61736.
88
	 *   - slaveOnly:   Whether to only use replica DBs and avoid triggering
89
	 *                  garbage collection logic of expired items. This only
90
	 *                  makes sense if the primary DB is used and only if get()
91
	 *                  calls will be used. This is used by ReplicatedBagOStuff.
92
	 *   - syncTimeout: Max seconds to wait for replica DBs to catch up for WRITE_SYNC.
93
	 *
94
	 * @param array $params
95
	 */
96
	public function __construct( $params ) {
97
		parent::__construct( $params );
98
99
		$this->attrMap[self::ATTR_EMULATION] = self::QOS_EMULATION_SQL;
100
		$this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_NONE;
101
102
		if ( isset( $params['servers'] ) ) {
103
			$this->serverInfos = [];
104
			$this->serverTags = [];
105
			$this->numServers = count( $params['servers'] );
106
			$index = 0;
107
			foreach ( $params['servers'] as $tag => $info ) {
108
				$this->serverInfos[$index] = $info;
109
				if ( is_string( $tag ) ) {
110
					$this->serverTags[$index] = $tag;
111
				} else {
112
					$this->serverTags[$index] = isset( $info['host'] ) ? $info['host'] : "#$index";
113
				}
114
				++$index;
115
			}
116
		} elseif ( isset( $params['server'] ) ) {
117
			$this->serverInfos = [ $params['server'] ];
118
			$this->numServers = count( $this->serverInfos );
119
		} else {
120
			// Default to using the main wiki's database servers
121
			$this->serverInfos = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type array<integer,array> of property $serverInfos.

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...
122
			$this->numServers = 1;
123
			$this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE;
124
		}
125
		if ( isset( $params['purgePeriod'] ) ) {
126
			$this->purgePeriod = intval( $params['purgePeriod'] );
127
		}
128
		if ( isset( $params['tableName'] ) ) {
129
			$this->tableName = $params['tableName'];
130
		}
131
		if ( isset( $params['shards'] ) ) {
132
			$this->shards = intval( $params['shards'] );
133
		}
134
		if ( isset( $params['syncTimeout'] ) ) {
135
			$this->syncTimeout = $params['syncTimeout'];
136
		}
137
		$this->replicaOnly = !empty( $params['slaveOnly'] );
138
	}
139
140
	protected function getSeparateMainLB() {
141
		global $wgDBtype;
142
143
		if ( $wgDBtype === 'mysql' && $this->usesMainDB() ) {
144
			if ( !$this->separateMainLB ) {
145
				// We must keep a separate connection to MySQL in order to avoid deadlocks
146
				$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
147
				$this->separateMainLB = $lbFactory->newMainLB();
148
			}
149
			return $this->separateMainLB;
150
		} else {
151
			// However, SQLite has an opposite behavior. And PostgreSQL needs to know
152
			// if we are in transaction or not (@TODO: find some PostgreSQL work-around).
153
			return null;
154
		}
155
	}
156
157
	/**
158
	 * Get a connection to the specified database
159
	 *
160
	 * @param int $serverIndex
161
	 * @return IDatabase
162
	 * @throws MWException
163
	 */
164
	protected function getDB( $serverIndex ) {
165
		if ( !isset( $this->conns[$serverIndex] ) ) {
166
			if ( $serverIndex >= $this->numServers ) {
167
				throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
168
			}
169
170
			# Don't keep timing out trying to connect for each call if the DB is down
171
			if ( isset( $this->connFailureErrors[$serverIndex] )
172
				&& ( time() - $this->connFailureTimes[$serverIndex] ) < 60
173
			) {
174
				throw $this->connFailureErrors[$serverIndex];
175
			}
176
177
			# If server connection info was given, use that
178
			if ( $this->serverInfos ) {
179
				$info = $this->serverInfos[$serverIndex];
180
				$type = isset( $info['type'] ) ? $info['type'] : 'mysql';
181
				$host = isset( $info['host'] ) ? $info['host'] : '[unknown]';
182
				$this->logger->debug( __CLASS__ . ": connecting to $host" );
183
				// Use a blank trx profiler to ignore expections as this is a cache
184
				$info['trxProfiler'] = new TransactionProfiler();
185
				$db = Database::factory( $type, $info );
186
				$db->clearFlag( DBO_TRX );
187
			} else {
188
				$index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
189
				if ( $this->getSeparateMainLB() ) {
190
					$db = $this->getSeparateMainLB()->getConnection( $index );
191
					$db->clearFlag( DBO_TRX ); // auto-commit mode
192
				} else {
193
					$db = wfGetDB( $index );
194
					// Can't mess with transaction rounds (DBO_TRX) :(
195
				}
196
			}
197
			$this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) );
198
			$this->conns[$serverIndex] = $db;
199
		}
200
201
		return $this->conns[$serverIndex];
202
	}
203
204
	/**
205
	 * Get the server index and table name for a given key
206
	 * @param string $key
207
	 * @return array Server index and table name
208
	 */
209
	protected function getTableByKey( $key ) {
210
		if ( $this->shards > 1 ) {
211
			$hash = hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
212
			$tableIndex = $hash % $this->shards;
213
		} else {
214
			$tableIndex = 0;
215
		}
216
		if ( $this->numServers > 1 ) {
217
			$sortedServers = $this->serverTags;
218
			ArrayUtils::consistentHashSort( $sortedServers, $key );
219
			reset( $sortedServers );
220
			$serverIndex = key( $sortedServers );
221
		} else {
222
			$serverIndex = 0;
223
		}
224
		return [ $serverIndex, $this->getTableNameByShard( $tableIndex ) ];
225
	}
226
227
	/**
228
	 * Get the table name for a given shard index
229
	 * @param int $index
230
	 * @return string
231
	 */
232
	protected function getTableNameByShard( $index ) {
233
		if ( $this->shards > 1 ) {
234
			$decimals = strlen( $this->shards - 1 );
235
			return $this->tableName .
236
				sprintf( "%0{$decimals}d", $index );
237
		} else {
238
			return $this->tableName;
239
		}
240
	}
241
242
	protected function doGet( $key, $flags = 0 ) {
243
		$casToken = null;
244
245
		return $this->getWithToken( $key, $casToken, $flags );
246
	}
247
248
	protected function getWithToken( $key, &$casToken, $flags = 0 ) {
249
		$values = $this->getMulti( [ $key ] );
250
		if ( array_key_exists( $key, $values ) ) {
251
			$casToken = $values[$key];
252
			return $values[$key];
253
		}
254
		return false;
255
	}
256
257
	public function getMulti( array $keys, $flags = 0 ) {
258
		$values = []; // array of (key => value)
259
260
		$keysByTable = [];
261 View Code Duplication
		foreach ( $keys as $key ) {
262
			list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
263
			$keysByTable[$serverIndex][$tableName][] = $key;
264
		}
265
266
		$this->garbageCollect(); // expire old entries if any
267
268
		$dataRows = [];
269
		foreach ( $keysByTable as $serverIndex => $serverKeys ) {
270
			try {
271
				$db = $this->getDB( $serverIndex );
272
				foreach ( $serverKeys as $tableName => $tableKeys ) {
273
					$res = $db->select( $tableName,
274
						[ 'keyname', 'value', 'exptime' ],
275
						[ 'keyname' => $tableKeys ],
276
						__METHOD__,
277
						// Approximate write-on-the-fly BagOStuff API via blocking.
278
						// This approximation fails if a ROLLBACK happens (which is rare).
279
						// We do not want to flush the TRX as that can break callers.
280
						$db->trxLevel() ? [ 'LOCK IN SHARE MODE' ] : []
281
					);
282
					if ( $res === false ) {
283
						continue;
284
					}
285
					foreach ( $res as $row ) {
0 ignored issues
show
The expression $res of type object<ResultWrapper>|boolean 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...
286
						$row->serverIndex = $serverIndex;
287
						$row->tableName = $tableName;
288
						$dataRows[$row->keyname] = $row;
289
					}
290
				}
291
			} catch ( DBError $e ) {
292
				$this->handleReadError( $e, $serverIndex );
293
			}
294
		}
295
296
		foreach ( $keys as $key ) {
297
			if ( isset( $dataRows[$key] ) ) { // HIT?
298
				$row = $dataRows[$key];
299
				$this->debug( "get: retrieved data; expiry time is " . $row->exptime );
300
				$db = null;
301
				try {
302
					$db = $this->getDB( $row->serverIndex );
303
					if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
304
						$this->debug( "get: key has expired" );
305
					} else { // HIT
306
						$values[$key] = $this->unserialize( $db->decodeBlob( $row->value ) );
307
					}
308
				} catch ( DBQueryError $e ) {
309
					$this->handleWriteError( $e, $db, $row->serverIndex );
310
				}
311
			} else { // MISS
312
				$this->debug( 'get: no matching rows' );
313
			}
314
		}
315
316
		return $values;
317
	}
318
319
	public function setMulti( array $data, $expiry = 0 ) {
320
		$keysByTable = [];
321 View Code Duplication
		foreach ( $data as $key => $value ) {
322
			list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
323
			$keysByTable[$serverIndex][$tableName][] = $key;
324
		}
325
326
		$this->garbageCollect(); // expire old entries if any
327
328
		$result = true;
329
		$exptime = (int)$expiry;
330
		foreach ( $keysByTable as $serverIndex => $serverKeys ) {
331
			$db = null;
332
			try {
333
				$db = $this->getDB( $serverIndex );
334
			} catch ( DBError $e ) {
335
				$this->handleWriteError( $e, $db, $serverIndex );
336
				$result = false;
337
				continue;
338
			}
339
340
			if ( $exptime < 0 ) {
341
				$exptime = 0;
342
			}
343
344 View Code Duplication
			if ( $exptime == 0 ) {
345
				$encExpiry = $this->getMaxDateTime( $db );
346
			} else {
347
				$exptime = $this->convertExpiry( $exptime );
348
				$encExpiry = $db->timestamp( $exptime );
349
			}
350
			foreach ( $serverKeys as $tableName => $tableKeys ) {
351
				$rows = [];
352
				foreach ( $tableKeys as $key ) {
353
					$rows[] = [
354
						'keyname' => $key,
355
						'value' => $db->encodeBlob( $this->serialize( $data[$key] ) ),
356
						'exptime' => $encExpiry,
357
					];
358
				}
359
360
				try {
361
					$db->replace(
362
						$tableName,
363
						[ 'keyname' ],
364
						$rows,
365
						__METHOD__
366
					);
367
				} catch ( DBError $e ) {
368
					$this->handleWriteError( $e, $db, $serverIndex );
369
					$result = false;
370
				}
371
372
			}
373
374
		}
375
376
		return $result;
377
	}
378
379 View Code Duplication
	public function set( $key, $value, $exptime = 0, $flags = 0 ) {
380
		$ok = $this->setMulti( [ $key => $value ], $exptime );
381
		if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
382
			$ok = $this->waitForReplication() && $ok;
383
		}
384
385
		return $ok;
386
	}
387
388
	protected function cas( $casToken, $key, $value, $exptime = 0 ) {
389
		list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
390
		$db = null;
391
		try {
392
			$db = $this->getDB( $serverIndex );
393
			$exptime = intval( $exptime );
394
395
			if ( $exptime < 0 ) {
396
				$exptime = 0;
397
			}
398
399 View Code Duplication
			if ( $exptime == 0 ) {
400
				$encExpiry = $this->getMaxDateTime( $db );
401
			} else {
402
				$exptime = $this->convertExpiry( $exptime );
403
				$encExpiry = $db->timestamp( $exptime );
404
			}
405
			// (bug 24425) use a replace if the db supports it instead of
406
			// delete/insert to avoid clashes with conflicting keynames
407
			$db->update(
408
				$tableName,
409
				[
410
					'keyname' => $key,
411
					'value' => $db->encodeBlob( $this->serialize( $value ) ),
412
					'exptime' => $encExpiry
413
				],
414
				[
415
					'keyname' => $key,
416
					'value' => $db->encodeBlob( $this->serialize( $casToken ) )
417
				],
418
				__METHOD__
419
			);
420
		} catch ( DBQueryError $e ) {
421
			$this->handleWriteError( $e, $db, $serverIndex );
422
423
			return false;
424
		}
425
426
		return (bool)$db->affectedRows();
427
	}
428
429
	public function delete( $key ) {
430
		list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
431
		$db = null;
432
		try {
433
			$db = $this->getDB( $serverIndex );
434
			$db->delete(
435
				$tableName,
436
				[ 'keyname' => $key ],
437
				__METHOD__ );
438
		} catch ( DBError $e ) {
439
			$this->handleWriteError( $e, $db, $serverIndex );
440
			return false;
441
		}
442
443
		return true;
444
	}
445
446
	public function incr( $key, $step = 1 ) {
447
		list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
448
		$db = null;
449
		try {
450
			$db = $this->getDB( $serverIndex );
451
			$step = intval( $step );
452
			$row = $db->selectRow(
453
				$tableName,
454
				[ 'value', 'exptime' ],
455
				[ 'keyname' => $key ],
456
				__METHOD__,
457
				[ 'FOR UPDATE' ] );
458
			if ( $row === false ) {
459
				// Missing
460
461
				return null;
462
			}
463
			$db->delete( $tableName, [ 'keyname' => $key ], __METHOD__ );
464
			if ( $this->isExpired( $db, $row->exptime ) ) {
465
				// Expired, do not reinsert
466
467
				return null;
468
			}
469
470
			$oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) );
471
			$newValue = $oldValue + $step;
472
			$db->insert( $tableName,
473
				[
474
					'keyname' => $key,
475
					'value' => $db->encodeBlob( $this->serialize( $newValue ) ),
476
					'exptime' => $row->exptime
477
				], __METHOD__, 'IGNORE' );
478
479
			if ( $db->affectedRows() == 0 ) {
480
				// Race condition. See bug 28611
481
				$newValue = null;
482
			}
483
		} catch ( DBError $e ) {
484
			$this->handleWriteError( $e, $db, $serverIndex );
485
			return null;
486
		}
487
488
		return $newValue;
489
	}
490
491 View Code Duplication
	public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
492
		$ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts );
493
		if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
494
			$ok = $this->waitForReplication() && $ok;
495
		}
496
497
		return $ok;
498
	}
499
500
	public function changeTTL( $key, $expiry = 0 ) {
501
		list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
502
		$db = null;
503
		try {
504
			$db = $this->getDB( $serverIndex );
505
			$db->update(
506
				$tableName,
507
				[ 'exptime' => $db->timestamp( $this->convertExpiry( $expiry ) ) ],
508
				[ 'keyname' => $key, 'exptime > ' . $db->addQuotes( $db->timestamp( time() ) ) ],
509
				__METHOD__
510
			);
511
			if ( $db->affectedRows() == 0 ) {
512
				return false;
513
			}
514
		} catch ( DBError $e ) {
515
			$this->handleWriteError( $e, $db, $serverIndex );
516
			return false;
517
		}
518
519
		return true;
520
	}
521
522
	/**
523
	 * @param IDatabase $db
524
	 * @param string $exptime
525
	 * @return bool
526
	 */
527
	protected function isExpired( $db, $exptime ) {
528
		return $exptime != $this->getMaxDateTime( $db ) && wfTimestamp( TS_UNIX, $exptime ) < time();
529
	}
530
531
	/**
532
	 * @param IDatabase $db
533
	 * @return string
534
	 */
535
	protected function getMaxDateTime( $db ) {
536
		if ( time() > 0x7fffffff ) {
537
			return $db->timestamp( 1 << 62 );
538
		} else {
539
			return $db->timestamp( 0x7fffffff );
540
		}
541
	}
542
543
	protected function garbageCollect() {
544
		if ( !$this->purgePeriod || $this->replicaOnly ) {
545
			// Disabled
546
			return;
547
		}
548
		// Only purge on one in every $this->purgePeriod requests.
549
		if ( $this->purgePeriod !== 1 && mt_rand( 0, $this->purgePeriod - 1 ) ) {
550
			return;
551
		}
552
		$now = time();
553
		// Avoid repeating the delete within a few seconds
554
		if ( $now > ( $this->lastExpireAll + 1 ) ) {
555
			$this->lastExpireAll = $now;
556
			$this->expireAll();
557
		}
558
	}
559
560
	public function expireAll() {
561
		$this->deleteObjectsExpiringBefore( wfTimestampNow() );
0 ignored issues
show
It seems like wfTimestampNow() can also be of type false; however, SqlBagOStuff::deleteObjectsExpiringBefore() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
562
	}
563
564
	/**
565
	 * Delete objects from the database which expire before a certain date.
566
	 * @param string $timestamp
567
	 * @param bool|callable $progressCallback
568
	 * @return bool
569
	 */
570
	public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) {
571
		for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
572
			$db = null;
573
			try {
574
				$db = $this->getDB( $serverIndex );
575
				$dbTimestamp = $db->timestamp( $timestamp );
576
				$totalSeconds = false;
577
				$baseConds = [ 'exptime < ' . $db->addQuotes( $dbTimestamp ) ];
578
				for ( $i = 0; $i < $this->shards; $i++ ) {
579
					$maxExpTime = false;
580
					while ( true ) {
581
						$conds = $baseConds;
582
						if ( $maxExpTime !== false ) {
583
							$conds[] = 'exptime > ' . $db->addQuotes( $maxExpTime );
584
						}
585
						$rows = $db->select(
586
							$this->getTableNameByShard( $i ),
587
							[ 'keyname', 'exptime' ],
588
							$conds,
589
							__METHOD__,
590
							[ 'LIMIT' => 100, 'ORDER BY' => 'exptime' ] );
591
						if ( $rows === false || !$rows->numRows() ) {
592
							break;
593
						}
594
						$keys = [];
595
						$row = $rows->current();
596
						$minExpTime = $row->exptime;
597
						if ( $totalSeconds === false ) {
598
							$totalSeconds = wfTimestamp( TS_UNIX, $timestamp )
599
								- wfTimestamp( TS_UNIX, $minExpTime );
600
						}
601
						foreach ( $rows as $row ) {
0 ignored issues
show
The expression $rows of type object<ResultWrapper>|boolean 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...
602
							$keys[] = $row->keyname;
603
							$maxExpTime = $row->exptime;
604
						}
605
606
						$db->delete(
607
							$this->getTableNameByShard( $i ),
608
							[
609
								'exptime >= ' . $db->addQuotes( $minExpTime ),
610
								'exptime < ' . $db->addQuotes( $dbTimestamp ),
611
								'keyname' => $keys
612
							],
613
							__METHOD__ );
614
615
						if ( $progressCallback ) {
616
							if ( intval( $totalSeconds ) === 0 ) {
617
								$percent = 0;
618
							} else {
619
								$remainingSeconds = wfTimestamp( TS_UNIX, $timestamp )
620
									- wfTimestamp( TS_UNIX, $maxExpTime );
621
								if ( $remainingSeconds > $totalSeconds ) {
622
									$totalSeconds = $remainingSeconds;
623
								}
624
								$processedSeconds = $totalSeconds - $remainingSeconds;
625
								$percent = ( $i + $processedSeconds / $totalSeconds )
626
									/ $this->shards * 100;
627
							}
628
							$percent = ( $percent / $this->numServers )
629
								+ ( $serverIndex / $this->numServers * 100 );
630
							call_user_func( $progressCallback, $percent );
631
						}
632
					}
633
				}
634
			} catch ( DBError $e ) {
635
				$this->handleWriteError( $e, $db, $serverIndex );
636
				return false;
637
			}
638
		}
639
		return true;
640
	}
641
642
	/**
643
	 * Delete content of shard tables in every server.
644
	 * Return true if the operation is successful, false otherwise.
645
	 * @return bool
646
	 */
647
	public function deleteAll() {
648
		for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
649
			$db = null;
650
			try {
651
				$db = $this->getDB( $serverIndex );
652
				for ( $i = 0; $i < $this->shards; $i++ ) {
653
					$db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
654
				}
655
			} catch ( DBError $e ) {
656
				$this->handleWriteError( $e, $db, $serverIndex );
657
				return false;
658
			}
659
		}
660
		return true;
661
	}
662
663
	/**
664
	 * Serialize an object and, if possible, compress the representation.
665
	 * On typical message and page data, this can provide a 3X decrease
666
	 * in storage requirements.
667
	 *
668
	 * @param mixed $data
669
	 * @return string
670
	 */
671
	protected function serialize( &$data ) {
672
		$serial = serialize( $data );
673
674
		if ( function_exists( 'gzdeflate' ) ) {
675
			return gzdeflate( $serial );
676
		} else {
677
			return $serial;
678
		}
679
	}
680
681
	/**
682
	 * Unserialize and, if necessary, decompress an object.
683
	 * @param string $serial
684
	 * @return mixed
685
	 */
686
	protected function unserialize( $serial ) {
687
		if ( function_exists( 'gzinflate' ) ) {
688
			MediaWiki\suppressWarnings();
689
			$decomp = gzinflate( $serial );
690
			MediaWiki\restoreWarnings();
691
692
			if ( false !== $decomp ) {
693
				$serial = $decomp;
694
			}
695
		}
696
697
		$ret = unserialize( $serial );
698
699
		return $ret;
700
	}
701
702
	/**
703
	 * Handle a DBError which occurred during a read operation.
704
	 *
705
	 * @param DBError $exception
706
	 * @param int $serverIndex
707
	 */
708
	protected function handleReadError( DBError $exception, $serverIndex ) {
709
		if ( $exception instanceof DBConnectionError ) {
710
			$this->markServerDown( $exception, $serverIndex );
711
		}
712
		$this->logger->error( "DBError: {$exception->getMessage()}" );
713 View Code Duplication
		if ( $exception instanceof DBConnectionError ) {
714
			$this->setLastError( BagOStuff::ERR_UNREACHABLE );
715
			$this->logger->debug( __METHOD__ . ": ignoring connection error" );
716
		} else {
717
			$this->setLastError( BagOStuff::ERR_UNEXPECTED );
718
			$this->logger->debug( __METHOD__ . ": ignoring query error" );
719
		}
720
	}
721
722
	/**
723
	 * Handle a DBQueryError which occurred during a write operation.
724
	 *
725
	 * @param DBError $exception
726
	 * @param IDatabase|null $db DB handle or null if connection failed
727
	 * @param int $serverIndex
728
	 * @throws Exception
729
	 */
730
	protected function handleWriteError( DBError $exception, IDatabase $db = null, $serverIndex ) {
731
		if ( !$db ) {
732
			$this->markServerDown( $exception, $serverIndex );
733
		} elseif ( $db->wasReadOnlyError() ) {
734
			if ( $db->trxLevel() && $this->usesMainDB() ) {
735
				// Errors like deadlocks and connection drops already cause rollback.
736
				// For consistency, we have no choice but to throw an error and trigger
737
				// complete rollback if the main DB is also being used as the cache DB.
738
				throw $exception;
739
			}
740
		}
741
742
		$this->logger->error( "DBError: {$exception->getMessage()}" );
743 View Code Duplication
		if ( $exception instanceof DBConnectionError ) {
744
			$this->setLastError( BagOStuff::ERR_UNREACHABLE );
745
			$this->logger->debug( __METHOD__ . ": ignoring connection error" );
746
		} else {
747
			$this->setLastError( BagOStuff::ERR_UNEXPECTED );
748
			$this->logger->debug( __METHOD__ . ": ignoring query error" );
749
		}
750
	}
751
752
	/**
753
	 * Mark a server down due to a DBConnectionError exception
754
	 *
755
	 * @param DBError $exception
756
	 * @param int $serverIndex
757
	 */
758
	protected function markServerDown( DBError $exception, $serverIndex ) {
759
		unset( $this->conns[$serverIndex] ); // bug T103435
760
761
		if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
762
			if ( time() - $this->connFailureTimes[$serverIndex] >= 60 ) {
763
				unset( $this->connFailureTimes[$serverIndex] );
764
				unset( $this->connFailureErrors[$serverIndex] );
765
			} else {
766
				$this->logger->debug( __METHOD__ . ": Server #$serverIndex already down" );
767
				return;
768
			}
769
		}
770
		$now = time();
771
		$this->logger->info( __METHOD__ . ": Server #$serverIndex down until " . ( $now + 60 ) );
772
		$this->connFailureTimes[$serverIndex] = $now;
773
		$this->connFailureErrors[$serverIndex] = $exception;
774
	}
775
776
	/**
777
	 * Create shard tables. For use from eval.php.
778
	 */
779
	public function createTables() {
780
		for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
781
			$db = $this->getDB( $serverIndex );
782
			if ( $db->getType() !== 'mysql' ) {
783
				throw new MWException( __METHOD__ . ' is not supported on this DB server' );
784
			}
785
786
			for ( $i = 0; $i < $this->shards; $i++ ) {
787
				$db->query(
788
					'CREATE TABLE ' . $db->tableName( $this->getTableNameByShard( $i ) ) .
789
					' LIKE ' . $db->tableName( 'objectcache' ),
790
					__METHOD__ );
791
			}
792
		}
793
	}
794
795
	/**
796
	 * @return bool Whether the main DB is used, e.g. wfGetDB( DB_MASTER )
797
	 */
798
	protected function usesMainDB() {
799
		return !$this->serverInfos;
800
	}
801
802
	protected function waitForReplication() {
803
		if ( !$this->usesMainDB() ) {
804
			// Custom DB server list; probably doesn't use replication
805
			return true;
806
		}
807
808
		$lb = $this->getSeparateMainLB()
809
			?: MediaWikiServices::getInstance()->getDBLoadBalancer();
810
811
		if ( $lb->getServerCount() <= 1 ) {
812
			return true; // no replica DBs
813
		}
814
815
		// Main LB is used; wait for any replica DBs to catch up
816
		$masterPos = $lb->getMasterPos();
817
818
		$loop = new WaitConditionLoop(
819
			function () use ( $lb, $masterPos ) {
820
				return $lb->waitForAll( $masterPos, 1 );
0 ignored issues
show
It seems like $masterPos defined by $lb->getMasterPos() on line 816 can also be of type boolean; however, LoadBalancer::waitForAll() does only seem to accept object<DBMasterPos>, 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...
821
			},
822
			$this->syncTimeout,
823
			$this->busyCallbacks
824
		);
825
826
		return ( $loop->invoke() === $loop::CONDITION_REACHED );
827
	}
828
}
829