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/libs/objectcache/BagOStuff.php (1 issue)

Labels
Severity

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
 * Copyright © 2003-2004 Brion Vibber <[email protected]>
4
 * https://www.mediawiki.org/
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 2 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along
17
 * with this program; if not, write to the Free Software Foundation, Inc.,
18
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
 * http://www.gnu.org/copyleft/gpl.html
20
 *
21
 * @file
22
 * @ingroup Cache
23
 */
24
25
/**
26
 * @defgroup Cache Cache
27
 */
28
29
use Psr\Log\LoggerAwareInterface;
30
use Psr\Log\LoggerInterface;
31
use Psr\Log\NullLogger;
32
use Wikimedia\ScopedCallback;
0 ignored issues
show
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
33
use Wikimedia\WaitConditionLoop;
34
35
/**
36
 * interface is intended to be more or less compatible with
37
 * the PHP memcached client.
38
 *
39
 * backends for local hash array and SQL table included:
40
 * @code
41
 *   $bag = new HashBagOStuff();
42
 *   $bag = new SqlBagOStuff(); # connect to db first
43
 * @endcode
44
 *
45
 * @ingroup Cache
46
 */
47
abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
48
	/** @var array[] Lock tracking */
49
	protected $locks = [];
50
	/** @var integer ERR_* class constant */
51
	protected $lastError = self::ERR_NONE;
52
	/** @var string */
53
	protected $keyspace = 'local';
54
	/** @var LoggerInterface */
55
	protected $logger;
56
	/** @var callback|null */
57
	protected $asyncHandler;
58
	/** @var integer Seconds */
59
	protected $syncTimeout;
60
61
	/** @var bool */
62
	private $debugMode = false;
63
	/** @var array */
64
	private $duplicateKeyLookups = [];
65
	/** @var bool */
66
	private $reportDupes = false;
67
	/** @var bool */
68
	private $dupeTrackScheduled = false;
69
70
	/** @var callable[] */
71
	protected $busyCallbacks = [];
72
73
	/** @var integer[] Map of (ATTR_* class constant => QOS_* class constant) */
74
	protected $attrMap = [];
75
76
	/** Possible values for getLastError() */
77
	const ERR_NONE = 0; // no error
78
	const ERR_NO_RESPONSE = 1; // no response
79
	const ERR_UNREACHABLE = 2; // can't connect
80
	const ERR_UNEXPECTED = 3; // response gave some error
81
82
	/** Bitfield constants for get()/getMulti() */
83
	const READ_LATEST = 1; // use latest data for replicated stores
84
	const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
85
	/** Bitfield constants for set()/merge() */
86
	const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
87
	const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
88
89
	/**
90
	 * $params include:
91
	 *   - logger: Psr\Log\LoggerInterface instance
92
	 *   - keyspace: Default keyspace for $this->makeKey()
93
	 *   - asyncHandler: Callable to use for scheduling tasks after the web request ends.
94
	 *      In CLI mode, it should run the task immediately.
95
	 *   - reportDupes: Whether to emit warning log messages for all keys that were
96
	 *      requested more than once (requires an asyncHandler).
97
	 *   - syncTimeout: How long to wait with WRITE_SYNC in seconds.
98
	 * @param array $params
99
	 */
100
	public function __construct( array $params = [] ) {
101
		if ( isset( $params['logger'] ) ) {
102
			$this->setLogger( $params['logger'] );
103
		} else {
104
			$this->setLogger( new NullLogger() );
105
		}
106
107
		if ( isset( $params['keyspace'] ) ) {
108
			$this->keyspace = $params['keyspace'];
109
		}
110
111
		$this->asyncHandler = isset( $params['asyncHandler'] )
112
			? $params['asyncHandler']
113
			: null;
114
115
		if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
116
			$this->reportDupes = true;
117
		}
118
119
		$this->syncTimeout = isset( $params['syncTimeout'] ) ? $params['syncTimeout'] : 3;
120
	}
121
122
	/**
123
	 * @param LoggerInterface $logger
124
	 * @return null
125
	 */
126
	public function setLogger( LoggerInterface $logger ) {
127
		$this->logger = $logger;
128
	}
129
130
	/**
131
	 * @param bool $bool
132
	 */
133
	public function setDebug( $bool ) {
134
		$this->debugMode = $bool;
135
	}
136
137
	/**
138
	 * Get an item with the given key, regenerating and setting it if not found
139
	 *
140
	 * If the callback returns false, then nothing is stored.
141
	 *
142
	 * @param string $key
143
	 * @param int $ttl Time-to-live (seconds)
144
	 * @param callable $callback Callback that derives the new value
145
	 * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
146
	 * @return mixed The cached value if found or the result of $callback otherwise
147
	 * @since 1.27
148
	 */
149
	final public function getWithSetCallback( $key, $ttl, $callback, $flags = 0 ) {
150
		$value = $this->get( $key, $flags );
151
152
		if ( $value === false ) {
153
			if ( !is_callable( $callback ) ) {
154
				throw new InvalidArgumentException( "Invalid cache miss callback provided." );
155
			}
156
			$value = call_user_func( $callback );
157
			if ( $value !== false ) {
158
				$this->set( $key, $value, $ttl );
159
			}
160
		}
161
162
		return $value;
163
	}
164
165
	/**
166
	 * Get an item with the given key
167
	 *
168
	 * If the key includes a determistic input hash (e.g. the key can only have
169
	 * the correct value) or complete staleness checks are handled by the caller
170
	 * (e.g. nothing relies on the TTL), then the READ_VERIFIED flag should be set.
171
	 * This lets tiered backends know they can safely upgrade a cached value to
172
	 * higher tiers using standard TTLs.
173
	 *
174
	 * @param string $key
175
	 * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
176
	 * @param integer $oldFlags [unused]
177
	 * @return mixed Returns false on failure and if the item does not exist
178
	 */
179
	public function get( $key, $flags = 0, $oldFlags = null ) {
180
		// B/C for ( $key, &$casToken = null, $flags = 0 )
181
		$flags = is_int( $oldFlags ) ? $oldFlags : $flags;
182
183
		$this->trackDuplicateKeys( $key );
184
185
		return $this->doGet( $key, $flags );
186
	}
187
188
	/**
189
	 * Track the number of times that a given key has been used.
190
	 * @param string $key
191
	 */
192
	private function trackDuplicateKeys( $key ) {
193
		if ( !$this->reportDupes ) {
194
			return;
195
		}
196
197
		if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
198
			// Track that we have seen this key. This N-1 counting style allows
199
			// easy filtering with array_filter() later.
200
			$this->duplicateKeyLookups[$key] = 0;
201
		} else {
202
			$this->duplicateKeyLookups[$key] += 1;
203
204
			if ( $this->dupeTrackScheduled === false ) {
205
				$this->dupeTrackScheduled = true;
206
				// Schedule a callback that logs keys processed more than once by get().
207
				call_user_func( $this->asyncHandler, function () {
208
					$dups = array_filter( $this->duplicateKeyLookups );
209
					foreach ( $dups as $key => $count ) {
210
						$this->logger->warning(
211
							'Duplicate get(): "{key}" fetched {count} times',
212
							// Count is N-1 of the actual lookup count
213
							[ 'key' => $key, 'count' => $count + 1, ]
214
						);
215
					}
216
				} );
217
			}
218
		}
219
	}
220
221
	/**
222
	 * @param string $key
223
	 * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
224
	 * @return mixed Returns false on failure and if the item does not exist
225
	 */
226
	abstract protected function doGet( $key, $flags = 0 );
227
228
	/**
229
	 * @note: This method is only needed if merge() uses mergeViaCas()
230
	 *
231
	 * @param string $key
232
	 * @param mixed $casToken
233
	 * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
234
	 * @return mixed Returns false on failure and if the item does not exist
235
	 * @throws Exception
236
	 */
237
	protected function getWithToken( $key, &$casToken, $flags = 0 ) {
238
		throw new Exception( __METHOD__ . ' not implemented.' );
239
	}
240
241
	/**
242
	 * Set an item
243
	 *
244
	 * @param string $key
245
	 * @param mixed $value
246
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
247
	 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
248
	 * @return bool Success
249
	 */
250
	abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
251
252
	/**
253
	 * Delete an item
254
	 *
255
	 * @param string $key
256
	 * @return bool True if the item was deleted or not found, false on failure
257
	 */
258
	abstract public function delete( $key );
259
260
	/**
261
	 * Merge changes into the existing cache value (possibly creating a new one)
262
	 *
263
	 * The callback function returns the new value given the current value
264
	 * (which will be false if not present), and takes the arguments:
265
	 * (this BagOStuff, cache key, current value, TTL).
266
	 * The TTL parameter is reference set to $exptime. It can be overriden in the callback.
267
	 *
268
	 * @param string $key
269
	 * @param callable $callback Callback method to be executed
270
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
271
	 * @param int $attempts The amount of times to attempt a merge in case of failure
272
	 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
273
	 * @return bool Success
274
	 * @throws InvalidArgumentException
275
	 */
276
	public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
277
		return $this->mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
278
	}
279
280
	/**
281
	 * @see BagOStuff::merge()
282
	 *
283
	 * @param string $key
284
	 * @param callable $callback Callback method to be executed
285
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
286
	 * @param int $attempts The amount of times to attempt a merge in case of failure
287
	 * @return bool Success
288
	 */
289
	protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
290
		do {
291
			$this->clearLastError();
292
			$reportDupes = $this->reportDupes;
293
			$this->reportDupes = false;
294
			$casToken = null; // passed by reference
295
			$currentValue = $this->getWithToken( $key, $casToken, self::READ_LATEST );
296
			$this->reportDupes = $reportDupes;
297
298
			if ( $this->getLastError() ) {
299
				return false; // don't spam retries (retry only on races)
300
			}
301
302
			// Derive the new value from the old value
303
			$value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
304
305
			$this->clearLastError();
306 View Code Duplication
			if ( $value === false ) {
307
				$success = true; // do nothing
308
			} elseif ( $currentValue === false ) {
309
				// Try to create the key, failing if it gets created in the meantime
310
				$success = $this->add( $key, $value, $exptime );
311
			} else {
312
				// Try to update the key, failing if it gets changed in the meantime
313
				$success = $this->cas( $casToken, $key, $value, $exptime );
314
			}
315
			if ( $this->getLastError() ) {
316
				return false; // IO error; don't spam retries
317
			}
318
		} while ( !$success && --$attempts );
319
320
		return $success;
321
	}
322
323
	/**
324
	 * Check and set an item
325
	 *
326
	 * @param mixed $casToken
327
	 * @param string $key
328
	 * @param mixed $value
329
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
330
	 * @return bool Success
331
	 * @throws Exception
332
	 */
333
	protected function cas( $casToken, $key, $value, $exptime = 0 ) {
334
		throw new Exception( "CAS is not implemented in " . __CLASS__ );
335
	}
336
337
	/**
338
	 * @see BagOStuff::merge()
339
	 *
340
	 * @param string $key
341
	 * @param callable $callback Callback method to be executed
342
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
343
	 * @param int $attempts The amount of times to attempt a merge in case of failure
344
	 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
345
	 * @return bool Success
346
	 */
347
	protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
348
		if ( !$this->lock( $key, 6 ) ) {
349
			return false;
350
		}
351
352
		$this->clearLastError();
353
		$reportDupes = $this->reportDupes;
354
		$this->reportDupes = false;
355
		$currentValue = $this->get( $key, self::READ_LATEST );
356
		$this->reportDupes = $reportDupes;
357
358
		if ( $this->getLastError() ) {
359
			$success = false;
360 View Code Duplication
		} else {
361
			// Derive the new value from the old value
362
			$value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
363
			if ( $value === false ) {
364
				$success = true; // do nothing
365
			} else {
366
				$success = $this->set( $key, $value, $exptime, $flags ); // set the new value
367
			}
368
		}
369
370
		if ( !$this->unlock( $key ) ) {
371
			// this should never happen
372
			trigger_error( "Could not release lock for key '$key'." );
373
		}
374
375
		return $success;
376
	}
377
378
	/**
379
	 * Reset the TTL on a key if it exists
380
	 *
381
	 * @param string $key
382
	 * @param int $expiry
383
	 * @return bool Success Returns false if there is no key
384
	 * @since 1.28
385
	 */
386
	public function changeTTL( $key, $expiry = 0 ) {
387
		$value = $this->get( $key );
388
389
		return ( $value === false ) ? false : $this->set( $key, $value, $expiry );
390
	}
391
392
	/**
393
	 * Acquire an advisory lock on a key string
394
	 *
395
	 * Note that if reentry is enabled, duplicate calls ignore $expiry
396
	 *
397
	 * @param string $key
398
	 * @param int $timeout Lock wait timeout; 0 for non-blocking [optional]
399
	 * @param int $expiry Lock expiry [optional]; 1 day maximum
400
	 * @param string $rclass Allow reentry if set and the current lock used this value
401
	 * @return bool Success
402
	 */
403
	public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
404
		// Avoid deadlocks and allow lock reentry if specified
405
		if ( isset( $this->locks[$key] ) ) {
406
			if ( $rclass != '' && $this->locks[$key]['class'] === $rclass ) {
407
				++$this->locks[$key]['depth'];
408
				return true;
409
			} else {
410
				return false;
411
			}
412
		}
413
414
		$expiry = min( $expiry ?: INF, self::TTL_DAY );
415
		$loop = new WaitConditionLoop(
416
			function () use ( $key, $timeout, $expiry ) {
417
				$this->clearLastError();
418
				if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
419
					return true; // locked!
420
				} elseif ( $this->getLastError() ) {
421
					return WaitConditionLoop::CONDITION_ABORTED; // network partition?
422
				}
423
424
				return WaitConditionLoop::CONDITION_CONTINUE;
425
			},
426
			$timeout
427
		);
428
429
		$locked = ( $loop->invoke() === $loop::CONDITION_REACHED );
430
		if ( $locked ) {
431
			$this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
432
		}
433
434
		return $locked;
435
	}
436
437
	/**
438
	 * Release an advisory lock on a key string
439
	 *
440
	 * @param string $key
441
	 * @return bool Success
442
	 */
443
	public function unlock( $key ) {
444
		if ( isset( $this->locks[$key] ) && --$this->locks[$key]['depth'] <= 0 ) {
445
			unset( $this->locks[$key] );
446
447
			return $this->delete( "{$key}:lock" );
448
		}
449
450
		return true;
451
	}
452
453
	/**
454
	 * Get a lightweight exclusive self-unlocking lock
455
	 *
456
	 * Note that the same lock cannot be acquired twice.
457
	 *
458
	 * This is useful for task de-duplication or to avoid obtrusive
459
	 * (though non-corrupting) DB errors like INSERT key conflicts
460
	 * or deadlocks when using LOCK IN SHARE MODE.
461
	 *
462
	 * @param string $key
463
	 * @param int $timeout Lock wait timeout; 0 for non-blocking [optional]
464
	 * @param int $expiry Lock expiry [optional]; 1 day maximum
465
	 * @param string $rclass Allow reentry if set and the current lock used this value
466
	 * @return ScopedCallback|null Returns null on failure
467
	 * @since 1.26
468
	 */
469
	final public function getScopedLock( $key, $timeout = 6, $expiry = 30, $rclass = '' ) {
470
		$expiry = min( $expiry ?: INF, self::TTL_DAY );
471
472
		if ( !$this->lock( $key, $timeout, $expiry, $rclass ) ) {
473
			return null;
474
		}
475
476
		$lSince = microtime( true ); // lock timestamp
477
478
		return new ScopedCallback( function() use ( $key, $lSince, $expiry ) {
479
			$latency = .050; // latency skew (err towards keeping lock present)
480
			$age = ( microtime( true ) - $lSince + $latency );
481
			if ( ( $age + $latency ) >= $expiry ) {
482
				$this->logger->warning( "Lock for $key held too long ($age sec)." );
483
				return; // expired; it's not "safe" to delete the key
484
			}
485
			$this->unlock( $key );
486
		} );
487
	}
488
489
	/**
490
	 * Delete all objects expiring before a certain date.
491
	 * @param string $date The reference date in MW format
492
	 * @param callable|bool $progressCallback Optional, a function which will be called
493
	 *     regularly during long-running operations with the percentage progress
494
	 *     as the first parameter.
495
	 *
496
	 * @return bool Success, false if unimplemented
497
	 */
498
	public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) {
499
		// stub
500
		return false;
501
	}
502
503
	/**
504
	 * Get an associative array containing the item for each of the keys that have items.
505
	 * @param array $keys List of strings
506
	 * @param integer $flags Bitfield; supports READ_LATEST [optional]
507
	 * @return array
508
	 */
509
	public function getMulti( array $keys, $flags = 0 ) {
510
		$res = [];
511
		foreach ( $keys as $key ) {
512
			$val = $this->get( $key );
513
			if ( $val !== false ) {
514
				$res[$key] = $val;
515
			}
516
		}
517
		return $res;
518
	}
519
520
	/**
521
	 * Batch insertion
522
	 * @param array $data $key => $value assoc array
523
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
524
	 * @return bool Success
525
	 * @since 1.24
526
	 */
527
	public function setMulti( array $data, $exptime = 0 ) {
528
		$res = true;
529
		foreach ( $data as $key => $value ) {
530
			if ( !$this->set( $key, $value, $exptime ) ) {
531
				$res = false;
532
			}
533
		}
534
		return $res;
535
	}
536
537
	/**
538
	 * @param string $key
539
	 * @param mixed $value
540
	 * @param int $exptime
541
	 * @return bool Success
542
	 */
543
	public function add( $key, $value, $exptime = 0 ) {
544
		if ( $this->get( $key ) === false ) {
545
			return $this->set( $key, $value, $exptime );
546
		}
547
		return false; // key already set
548
	}
549
550
	/**
551
	 * Increase stored value of $key by $value while preserving its TTL
552
	 * @param string $key Key to increase
553
	 * @param int $value Value to add to $key (Default 1)
554
	 * @return int|bool New value or false on failure
555
	 */
556
	public function incr( $key, $value = 1 ) {
557
		if ( !$this->lock( $key ) ) {
558
			return false;
559
		}
560
		$n = $this->get( $key );
561
		if ( $this->isInteger( $n ) ) { // key exists?
562
			$n += intval( $value );
563
			$this->set( $key, max( 0, $n ) ); // exptime?
564
		} else {
565
			$n = false;
566
		}
567
		$this->unlock( $key );
568
569
		return $n;
570
	}
571
572
	/**
573
	 * Decrease stored value of $key by $value while preserving its TTL
574
	 * @param string $key
575
	 * @param int $value
576
	 * @return int|bool New value or false on failure
577
	 */
578
	public function decr( $key, $value = 1 ) {
579
		return $this->incr( $key, - $value );
580
	}
581
582
	/**
583
	 * Increase stored value of $key by $value while preserving its TTL
584
	 *
585
	 * This will create the key with value $init and TTL $ttl instead if not present
586
	 *
587
	 * @param string $key
588
	 * @param int $ttl
589
	 * @param int $value
590
	 * @param int $init
591
	 * @return int|bool New value or false on failure
592
	 * @since 1.24
593
	 */
594
	public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
595
		$newValue = $this->incr( $key, $value );
596
		if ( $newValue === false ) {
597
			// No key set; initialize
598
			$newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false;
599
		}
600
		if ( $newValue === false ) {
601
			// Raced out initializing; increment
602
			$newValue = $this->incr( $key, $value );
603
		}
604
605
		return $newValue;
606
	}
607
608
	/**
609
	 * Get the "last error" registered; clearLastError() should be called manually
610
	 * @return int ERR_* constant for the "last error" registry
611
	 * @since 1.23
612
	 */
613
	public function getLastError() {
614
		return $this->lastError;
615
	}
616
617
	/**
618
	 * Clear the "last error" registry
619
	 * @since 1.23
620
	 */
621
	public function clearLastError() {
622
		$this->lastError = self::ERR_NONE;
623
	}
624
625
	/**
626
	 * Set the "last error" registry
627
	 * @param int $err ERR_* constant
628
	 * @since 1.23
629
	 */
630
	protected function setLastError( $err ) {
631
		$this->lastError = $err;
632
	}
633
634
	/**
635
	 * Let a callback be run to avoid wasting time on special blocking calls
636
	 *
637
	 * The callbacks may or may not be called ever, in any particular order.
638
	 * They are likely to be invoked when something WRITE_SYNC is used used.
639
	 * They should follow a caching pattern as shown below, so that any code
640
	 * using the word will get it's result no matter what happens.
641
	 * @code
642
	 *     $result = null;
643
	 *     $workCallback = function () use ( &$result ) {
644
	 *         if ( !$result ) {
645
	 *             $result = ....
646
	 *         }
647
	 *         return $result;
648
	 *     }
649
	 * @endcode
650
	 *
651
	 * @param callable $workCallback
652
	 * @since 1.28
653
	 */
654
	public function addBusyCallback( callable $workCallback ) {
655
		$this->busyCallbacks[] = $workCallback;
656
	}
657
658
	/**
659
	 * Modify a cache update operation array for EventRelayer::notify()
660
	 *
661
	 * This is used for relayed writes, e.g. for broadcasting a change
662
	 * to multiple data-centers. If the array contains a 'val' field
663
	 * then the command involves setting a key to that value. Note that
664
	 * for simplicity, 'val' is always a simple scalar value. This method
665
	 * is used to possibly serialize the value and add any cache-specific
666
	 * key/values needed for the relayer daemon (e.g. memcached flags).
667
	 *
668
	 * @param array $event
669
	 * @return array
670
	 * @since 1.26
671
	 */
672
	public function modifySimpleRelayEvent( array $event ) {
673
		return $event;
674
	}
675
676
	/**
677
	 * @param string $text
678
	 */
679
	protected function debug( $text ) {
680
		if ( $this->debugMode ) {
681
			$this->logger->debug( "{class} debug: $text", [
682
				'class' => get_class( $this ),
683
			] );
684
		}
685
	}
686
687
	/**
688
	 * Convert an optionally relative time to an absolute time
689
	 * @param int $exptime
690
	 * @return int
691
	 */
692
	protected function convertExpiry( $exptime ) {
693
		if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
694
			return time() + $exptime;
695
		} else {
696
			return $exptime;
697
		}
698
	}
699
700
	/**
701
	 * Convert an optionally absolute expiry time to a relative time. If an
702
	 * absolute time is specified which is in the past, use a short expiry time.
703
	 *
704
	 * @param int $exptime
705
	 * @return int
706
	 */
707
	protected function convertToRelative( $exptime ) {
708
		if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
709
			$exptime -= time();
710
			if ( $exptime <= 0 ) {
711
				$exptime = 1;
712
			}
713
			return $exptime;
714
		} else {
715
			return $exptime;
716
		}
717
	}
718
719
	/**
720
	 * Check if a value is an integer
721
	 *
722
	 * @param mixed $value
723
	 * @return bool
724
	 */
725
	protected function isInteger( $value ) {
726
		return ( is_int( $value ) || ctype_digit( $value ) );
727
	}
728
729
	/**
730
	 * Construct a cache key.
731
	 *
732
	 * @since 1.27
733
	 * @param string $keyspace
734
	 * @param array $args
735
	 * @return string
736
	 */
737
	public function makeKeyInternal( $keyspace, $args ) {
738
		$key = $keyspace;
739
		foreach ( $args as $arg ) {
740
			$arg = str_replace( ':', '%3A', $arg );
741
			$key = $key . ':' . $arg;
742
		}
743
		return strtr( $key, ' ', '_' );
744
	}
745
746
	/**
747
	 * Make a global cache key.
748
	 *
749
	 * @since 1.27
750
	 * @param string ... Key component (variadic)
751
	 * @return string
752
	 */
753
	public function makeGlobalKey() {
754
		return $this->makeKeyInternal( 'global', func_get_args() );
755
	}
756
757
	/**
758
	 * Make a cache key, scoped to this instance's keyspace.
759
	 *
760
	 * @since 1.27
761
	 * @param string ... Key component (variadic)
762
	 * @return string
763
	 */
764
	public function makeKey() {
765
		return $this->makeKeyInternal( $this->keyspace, func_get_args() );
766
	}
767
768
	/**
769
	 * @param integer $flag ATTR_* class constant
770
	 * @return integer QOS_* class constant
771
	 * @since 1.28
772
	 */
773
	public function getQoS( $flag ) {
774
		return isset( $this->attrMap[$flag] ) ? $this->attrMap[$flag] : self::QOS_UNKNOWN;
775
	}
776
777
	/**
778
	 * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map
779
	 *
780
	 * @param BagOStuff[] $bags
781
	 * @return integer[] Resulting flag map (class ATTR_* constant => class QOS_* constant)
782
	 */
783
	protected function mergeFlagMaps( array $bags ) {
784
		$map = [];
785
		foreach ( $bags as $bag ) {
786
			foreach ( $bag->attrMap as $attr => $rank ) {
787 View Code Duplication
				if ( isset( $map[$attr] ) ) {
788
					$map[$attr] = min( $map[$attr], $rank );
789
				} else {
790
					$map[$attr] = $rank;
791
				}
792
			}
793
		}
794
795
		return $map;
796
	}
797
}
798