* The following response properties from action=login, deprecated in 1.27, are
now removed: lgtoken, cookieprefix, sessionid. Clients should handle cookies
to properly manage session state.
+* Submitting the lgtoken and lgpassword parameters in the query string to
+ action=login is now deprecated and outputs a warning. They should be submitted
+ in the POST body instead.
+* Submitting sensitive authentication request parameters to action=clientlogin,
+ action=createaccount, action=linkaccount, and action=changeauthenticationdata
+ in the query string is now deprecated and outputs a warning. They should be
+ submitted in the POST body instead.
=== Action API internal changes in 1.28 ===
* Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
// Collect the fields for all the requests
$fields = [];
+ $sensitive = [];
foreach ( $reqs as $req ) {
- $fields += (array)$req->getFieldInfo();
+ $info = (array)$req->getFieldInfo();
+ $fields += $info;
+ $sensitive += array_filter( $info, function ( $opts ) {
+ return !empty( $opts['sensitive'] );
+ } );
}
// Extract the request data for the fields and mark those request
$data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
$this->module->getMain()->markParamsUsed( array_keys( $data ) );
+ if ( $sensitive ) {
+ try {
+ $this->module->requirePostedParameters( array_keys( $sensitive ), 'noprefix' );
+ } catch ( UsageException $ex ) {
+ // Make this a warning for now, upgrade to an error in 1.29.
+ $this->module->setWarning( $ex->getMessage() );
+ $this->module->logFeatureUsage( $this->module->getModuleName() . '-params-in-query-string' );
+ }
+ }
+
return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
}
}
}
+ /**
+ * Die if any of the specified parameters were found in the query part of
+ * the URL rather than the post body.
+ * @since 1.28
+ * @param string[] $params Parameters to check
+ * @param string $prefix Set to 'noprefix' to skip calling $this->encodeParamName()
+ */
+ public function requirePostedParameters( $params, $prefix = 'prefix' ) {
+ // Skip if $wgDebugAPI is set or we're in internal mode
+ if ( $this->getConfig()->get( 'DebugAPI' ) || $this->getMain()->isInternalMode() ) {
+ return;
+ }
+
+ $queryValues = $this->getRequest()->getQueryValues();
+ $badParams = [];
+ foreach ( $params as $param ) {
+ if ( $prefix !== 'noprefix' ) {
+ $param = $this->encodeParamName( $param );
+ }
+ if ( array_key_exists( $param, $queryValues ) ) {
+ $badParams[] = $param;
+ }
+ }
+
+ if ( $badParams ) {
+ $this->dieUsage(
+ 'The following parameters were found in the query string, but must be in the POST body: '
+ . join( ', ', $badParams ),
+ 'mustpostparams'
+ );
+ }
+ }
+
/**
* Callback function used in requireOnlyOneParameter to check whether required parameters are set
*
* analysis.
* @param string $feature Feature being used.
*/
- protected function logFeatureUsage( $feature ) {
+ public function logFeatureUsage( $feature ) {
$request = $this->getRequest();
$s = '"' . addslashes( $feature ) . '"' .
' "' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . '"' .
return;
}
+ try {
+ $this->requirePostedParameters( [ 'password', 'token' ] );
+ } catch ( UsageException $ex ) {
+ // Make this a warning for now, upgrade to an error in 1.29.
+ $this->setWarning( $ex->getMessage() );
+ $this->logFeatureUsage( 'login-params-in-query-string' );
+ }
+
$params = $this->extractRequestParams();
$result = [];
$this->dieUsageMsg( [ 'missingparam', 'token' ] );
}
- if ( !$this->getConfig()->get( 'DebugAPI' ) &&
- array_key_exists(
- $module->encodeParamName( 'token' ),
- $this->getRequest()->getQueryValues()
- )
- ) {
- $this->dieUsage(
- "The '{$module->encodeParamName( 'token' )}' parameter was " .
- 'found in the query string, but must be in the POST body',
- 'mustposttoken'
- );
- }
+ $module->requirePostedParameters( [ 'token' ] );
if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
$this->dieUsageMsg( 'sessionfailure' );
$this->dieStatusWithCode( $status, 'stashfailed' );
}
+ // We can only get warnings like 'duplicate' after concatenating the chunks
+ $warnings = $this->getApiWarnings();
+ if ( $warnings ) {
+ $result['warnings'] = $warnings;
+ }
+
// The fully concatenated file has a new filekey. So remove
// the old filekey and fetch the new one.
UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
$this->mUpload->stash->removeFile( $filekey );
- $filekey = $this->mUpload->getLocalFile()->getFileKey();
+ $filekey = $this->mUpload->getStashFile()->getFileKey();
$result['result'] = 'Success';
}
if ( isset( $progress['status']->value['verification'] ) ) {
$this->checkVerification( $progress['status']->value['verification'] );
}
+ if ( isset( $progress['status']->value['warnings'] ) ) {
+ $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
+ if ( $warnings ) {
+ $progress['warnings'] = $warnings;
+ }
+ }
unset( $progress['status'] ); // remove Status object
$this->getResult()->addValue( null, $this->getModuleName(), $progress );
$this->mParams['text'] = $this->mParams['comment'];
}
- /** @var $file File */
+ /** @var $file LocalFile */
$file = $this->mUpload->getLocalFile();
// For preferences mode, we want to watch if 'watchdefault' is set,
return false;
}
+ // We can only get warnings like 'duplicate' after concatenating the chunks
+ $status = Status::newGood();
+ $status->value = [ 'warnings' => $upload->checkWarnings() ];
+
// We have a new filekey for the fully concatenated file
- $newFileKey = $upload->getLocalFile()->getFileKey();
+ $newFileKey = $upload->getStashFile()->getFileKey();
// Remove the old stash file row and first chunk file
$upload->stash->removeFileNoAuth( $this->params['filekey'] );
'stage' => 'assembling',
'filekey' => $newFileKey,
'imageinfo' => $imageInfo,
- 'status' => Status::newGood()
+ 'status' => $status
]
);
} catch ( Exception $e ) {
return false;
}
+ public function add( $key, $value, $exp = 0 ) {
+ return true;
+ }
+
public function set( $key, $value, $exp = 0, $flags = 0 ) {
return true;
}
* This class is intended for caching data from primary stores.
* If the get() method does not return a value, then the caller
* should query the new value and backfill the cache using set().
+ * The preferred way to do this logic is through getWithSetCallback().
* When querying the store on cache miss, the closest DB replica
* should be used. Try to avoid heavyweight DB master or quorum reads.
* When the source data changes, a purge method should be called.
*
* The simplest purge method is delete().
*
- * Instances of this class must be configured to point to a valid
- * PubSub endpoint, and there must be listeners on the cache servers
- * that subscribe to the endpoint and update the caches.
+ * There are two supported ways to handle broadcasted operations:
+ * - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint
+ * that has subscribed listeners on the cache servers applying the cache updates.
+ * - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
+ * and set up mcrouter as the underlying cache backend, using one of the memcached
+ * BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
+ * to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
+ * configure other operations to go to the local DC via PoolRoute (for reference,
+ * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
*
- * Broadcasted operations like delete() and touchCheckKey() are done
- * synchronously in the local datacenter, but are relayed asynchronously.
- * This means that callers in other datacenters will see older values
- * for however many milliseconds the datacenters are apart. As with
- * any cache, this should not be relied on for cases where reads are
- * used to determine writes to source (e.g. non-cache) data stores.
+ * Broadcasted operations like delete() and touchCheckKey() are done asynchronously
+ * in all datacenters this way, though the local one should likely be near immediate.
+ *
+ * This means that callers in all datacenters may see older values for however many
+ * milliseconds that the purge took to reach that datacenter. As with any cache, this
+ * should not be relied on for cases where reads are used to determine writes to source
+ * (e.g. non-cache) data stores, except when reading immutable data.
*
* All values are wrapped in metadata arrays. Keys use a "WANCache:" prefix
* to avoid collisions with keys that are not wrapped as metadata arrays. The
* - a) "WANCache:v" : used for regular value keys
* - b) "WANCache:i" : used for temporarily storing values of tombstoned keys
* - c) "WANCache:t" : used for storing timestamp "check" keys
+ * - d) "WANCache:m" : used for temporary mutex keys to avoid cache stampedes
*
* @ingroup Cache
* @since 1.26
const VALUE_KEY_PREFIX = 'WANCache:v:';
const INTERIM_KEY_PREFIX = 'WANCache:i:';
const TIME_KEY_PREFIX = 'WANCache:t:';
+ const MUTEX_KEY_PREFIX = 'WANCache:m:';
const PURGE_VAL_PREFIX = 'PURGED:';
*
* When using potentially long-running ACID transactions, a good pattern is
* to use a pre-commit hook to issue the delete. This means that immediately
- * after commit, callers will see the tombstone in cache in the local datacenter
- * and in the others upon relay. It also avoids the following race condition:
+ * after commit, callers will see the tombstone in cache upon purge relay.
+ * It also avoids the following race condition:
* - a) T1 begins, changes a row, and calls delete()
* - b) The HOLDOFF_TTL passes, expiring the delete() tombstone
* - c) T2 starts, reads the row and calls set() due to a cache miss
$key = self::VALUE_KEY_PREFIX . $key;
if ( $ttl <= 0 ) {
- // Update the local datacenter immediately
- $ok = $this->cache->delete( $key );
// Publish the purge to all datacenters
- $ok = $this->relayDelete( $key ) && $ok;
+ $ok = $this->relayDelete( $key );
} else {
- // Update the local datacenter immediately
- $ok = $this->cache->set( $key,
- $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
- $ttl
- );
// Publish the purge to all datacenters
- $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE ) && $ok;
+ $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE );
}
return $ok;
* keys, the relevant "check" keys must be supplied for this to work.
*
* The "check" key essentially represents a last-modified field.
- * When touched, keys using it via get(), getMulti(), or getWithSetCallback()
- * will be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
+ * When touched, the field will be updated on all cache servers.
+ * Keys using it via get(), getMulti(), or getWithSetCallback() will
+ * be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
* by those methods to avoid race conditions where dependent keys get updated
* with stale values (e.g. from a DB slave).
*
* When a few important keys get a large number of hits, a high cache
* time is usually desired as well as "lockTSE" logic. The resetCheckKey()
* method is less appropriate in such cases since the "time since expiry"
- * cannot be inferred.
+ * cannot be inferred, causing any get() after the reset to treat the key
+ * as being "hot", resulting in more stale value usage.
*
* Note that "check" keys won't collide with other regular keys.
*
* @return bool True if the item was purged or not found, false on failure
*/
final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
- $key = self::TIME_KEY_PREFIX . $key;
- // Update the local datacenter immediately
- $ok = $this->cache->set( $key,
- $this->makePurgeValue( microtime( true ), $holdoff ),
- self::CHECK_KEY_TTL
- );
// Publish the purge to all datacenters
- return $this->relayPurge( $key, self::CHECK_KEY_TTL, $holdoff ) && $ok;
+ return $this->relayPurge( self::TIME_KEY_PREFIX . $key, self::CHECK_KEY_TTL, $holdoff );
}
/**
*
* This is similar to touchCheckKey() in that keys using it via get(), getMulti(),
* or getWithSetCallback() will be invalidated. The differences are:
- * - a) The timestamp will be deleted from all caches and lazily
+ * - a) The "check" key will be deleted from all caches and lazily
* re-initialized when accessed (rather than set everywhere)
* - b) Thus, dependent keys will be known to be invalid, but not
* for how long (they are treated as "just" purged), which
* effects any lockTSE logic in getWithSetCallback()
+ * - c) Since "check" keys are initialized only on the server the key hashes
+ * to, any temporary ejection of that server will cause the value to be
+ * seen as purged as a new server will initialize the "check" key.
*
* The advantage is that this does not place high TTL keys on every cache
* server, making it better for code that will cache many different keys
* @return bool True if the item was purged or not found, false on failure
*/
final public function resetCheckKey( $key ) {
- $key = self::TIME_KEY_PREFIX . $key;
- // Update the local datacenter immediately
- $ok = $this->cache->delete( $key );
// Publish the purge to all datacenters
- return $this->relayDelete( $key ) && $ok;
+ return $this->relayDelete( self::TIME_KEY_PREFIX . $key );
}
/**
$lockAcquired = false;
if ( $useMutex ) {
// Acquire a datacenter-local non-blocking lock
- if ( $this->cache->lock( $key, 0, self::LOCK_TTL ) ) {
+ if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
// Lock acquired; this thread should update the key
$lockAcquired = true;
} elseif ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
$tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
$wrapped = $this->wrap( $value, $tempTTL, $asOf );
- $this->cache->set( self::INTERIM_KEY_PREFIX . $key, $wrapped, $tempTTL );
- }
-
- if ( $lockAcquired ) {
- $this->cache->unlock( $key );
+ // Avoid using set() to avoid pointless mcrouter broadcasting
+ $this->cache->merge(
+ self::INTERIM_KEY_PREFIX . $key,
+ function () use ( $wrapped ) {
+ return $wrapped;
+ },
+ $tempTTL,
+ 1
+ );
}
if ( $value !== false && $ttl >= 0 ) {
$this->set( $key, $value, $ttl, $setOpts );
}
+ if ( $lockAcquired ) {
+ // Avoid using delete() to avoid pointless mcrouter broadcasting
+ $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, 1 );
+ }
+
return $value;
}
* @return bool Success
*/
protected function relayPurge( $key, $ttl, $holdoff ) {
- $event = $this->cache->modifySimpleRelayEvent( [
- 'cmd' => 'set',
- 'key' => $key,
- 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
- 'ttl' => max( $ttl, 1 ),
- 'sbt' => true, // substitute $UNIXTIME$ with actual microtime
- ] );
-
- $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
- if ( !$ok ) {
- $this->lastRelayError = self::ERR_RELAY;
+ if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ // This handles the mcrouter and the single-DC case
+ $ok = $this->cache->set( $key,
+ $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
+ $ttl
+ );
+ } else {
+ $event = $this->cache->modifySimpleRelayEvent( [
+ 'cmd' => 'set',
+ 'key' => $key,
+ 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
+ 'ttl' => max( $ttl, 1 ),
+ 'sbt' => true, // substitute $UNIXTIME$ with actual microtime
+ ] );
+
+ $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
+ if ( !$ok ) {
+ $this->lastRelayError = self::ERR_RELAY;
+ }
}
return $ok;
* @return bool Success
*/
protected function relayDelete( $key ) {
- $event = $this->cache->modifySimpleRelayEvent( [
- 'cmd' => 'delete',
- 'key' => $key,
- ] );
-
- $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
- if ( !$ok ) {
- $this->lastRelayError = self::ERR_RELAY;
+ if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ // This handles the mcrouter and the single-DC case
+ $ok = $this->cache->delete( $key );
+ } else {
+ $event = $this->cache->modifySimpleRelayEvent( [
+ 'cmd' => 'delete',
+ 'key' => $key,
+ ] );
+
+ $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
+ if ( !$ok ) {
+ $this->lastRelayError = self::ERR_RELAY;
+ }
}
return $ok;
protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
protected $mTitle = false, $mTitleError = 0;
protected $mFilteredName, $mFinalExtension;
- protected $mLocalFile, $mFileSize, $mFileProps;
+ protected $mLocalFile, $mStashFile, $mFileSize, $mFileProps;
protected $mBlackListedExtensions;
protected $mJavaDetected, $mSVGNSError;
/**
* Return the local file and initializes if necessary.
*
- * @return LocalFile|UploadStashFile|null
+ * @return LocalFile|null
*/
public function getLocalFile() {
if ( is_null( $this->mLocalFile ) ) {
return $this->mLocalFile;
}
+ /**
+ * @return UploadStashFile|null
+ */
+ public function getStashFile() {
+ return $this->mStashFile;
+ }
+
/**
* Like stashFile(), but respects extensions' wishes to prevent the stashing. verifyUpload() must
* be called before calling this method (unless $isPartial is true).
protected function doStashFile( User $user = null ) {
$stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $user );
$file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
- $this->mLocalFile = $file;
+ $this->mStashFile = $file;
return $file;
}
* @return array Image info
*/
public function getImageInfo( $result ) {
- $file = $this->getLocalFile();
- /** @todo This cries out for refactoring.
- * We really want to say $file->getAllInfo(); here.
- * Perhaps "info" methods should be moved into files, and the API should
- * just wrap them in queries.
- */
- if ( $file instanceof UploadStashFile ) {
+ $localFile = $this->getLocalFile();
+ $stashFile = $this->getStashFile();
+ // Calling a different API module depending on whether the file was stashed is less than optimal.
+ // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
+ if ( $stashFile ) {
$imParam = ApiQueryStashImageInfo::getPropertyNames();
- $info = ApiQueryStashImageInfo::getInfo( $file, array_flip( $imParam ), $result );
+ $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_flip( $imParam ), $result );
} else {
$imParam = ApiQueryImageInfo::getPropertyNames();
- $info = ApiQueryImageInfo::getInfo( $file, array_flip( $imParam ), $result );
+ $info = ApiQueryImageInfo::getInfo( $localFile, array_flip( $imParam ), $result );
}
return $info;
$this->verifyChunk();
// Create a local stash target
- $this->mLocalFile = parent::doStashFile( $user );
+ $this->mStashFile = parent::doStashFile( $user );
// Update the initial file offset (based on file size)
- $this->mOffset = $this->mLocalFile->getSize();
- $this->mFileKey = $this->mLocalFile->getFileKey();
+ $this->mOffset = $this->mStashFile->getSize();
+ $this->mFileKey = $this->mStashFile->getFileKey();
// Output a copy of this first to chunk 0 location:
- $this->outputChunk( $this->mLocalFile->getPath() );
+ $this->outputChunk( $this->mStashFile->getPath() );
// Update db table to reflect initial "chunk" state
$this->updateChunkStatus();
- return $this->mLocalFile;
+ return $this->mStashFile;
}
/**
return $status;
}
- // Update the mTempPath and mLocalFile
+ // Update the mTempPath and mStashFile
// (for FileUpload or normal Stash to take over)
$tStart = microtime( true );
// This is a re-implementation of UploadBase::tryStashFile(), we can't call it because we
return $status;
}
try {
- $this->mLocalFile = parent::doStashFile( $this->user );
+ $this->mStashFile = parent::doStashFile( $this->user );
} catch ( UploadStashException $e ) {
$status->fatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
return $status;
}
$tAmount = microtime( true ) - $tStart;
- $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
+ $this->mStashFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." );
return $status;
$value = wfRandomString();
$calls = 0;
- $func = function() use ( &$calls, $value ) {
+ $func = function() use ( &$calls, $value, $cache, $key ) {
++$calls;
+ // Immediately kill any mutex rather than waiting a second
+ $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
return $value;
};
$this->assertEquals( 1, $calls, 'Value was populated' );
// Acquire a lock to verify that getWithSetCallback uses lockTSE properly
- $this->internalCache->lock( $key, 0 );
+ $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
$checkKeys = [ wfRandomString() ]; // new check keys => force misses
$ret = $cache->getWithSetCallback( $key, 30, $func,
$value = wfRandomString();
$calls = 0;
- $func = function( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value ) {
+ $func = function( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $cache, $key ) {
++$calls;
$setOpts['since'] = microtime( true ) - 10;
+ // Immediately kill any mutex rather than waiting a second
+ $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
return $value;
};
$this->assertEquals( 1, $calls, 'Value was generated' );
// Acquire a lock to verify that getWithSetCallback uses lockTSE properly
- $this->internalCache->lock( $key, 0 );
+ $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
$ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
$this->assertEquals( $value, $ret );
$this->assertEquals( 1, $calls, 'Callback was not used' );
$busyValue = wfRandomString();
$calls = 0;
- $func = function() use ( &$calls, $value ) {
+ $func = function() use ( &$calls, $value, $cache, $key ) {
++$calls;
+ // Immediately kill any mutex rather than waiting a second
+ $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
return $value;
};
$this->assertEquals( 1, $calls, 'Value was populated' );
// Acquire a lock to verify that getWithSetCallback uses busyValue properly
- $this->internalCache->lock( $key, 0 );
+ $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
$checkKeys = [ wfRandomString() ]; // new check keys => force misses
$ret = $cache->getWithSetCallback( $key, 30, $func,
$this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
$this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
- $this->internalCache->unlock( $key );
+ $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
$ret = $cache->getWithSetCallback( $key, 30, $func,
[ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
$this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
$this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
- $this->internalCache->lock( $key, 0 );
+ $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
$ret = $cache->getWithSetCallback( $key, 30, $func,
[ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
$this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
$this->cache->set( $key, $value, 30, $opts );
$this->assertEquals( false, $this->cache->get( $key ), "Pending value not written." );
}
+
+ public function testMcRouterSupport() {
+ $localBag = $this->getMock( 'EmptyBagOStuff', [ 'set', 'delete' ] );
+ $localBag->expects( $this->never() )->method( 'set' );
+ $localBag->expects( $this->never() )->method( 'delete' );
+ $wanCache = new WANObjectCache( [
+ 'cache' => $localBag,
+ 'pool' => 'testcache-hash',
+ 'relayer' => new EventRelayerNull( [] )
+ ] );
+ $valFunc = function () {
+ return 1;
+ };
+
+ // None of these should use broadcasting commands (e.g. SET, DELETE)
+ $wanCache->get( 'x' );
+ $wanCache->get( 'x', $ctl, [ 'check1' ] );
+ $wanCache->getMulti( [ 'x', 'y' ] );
+ $wanCache->getMulti( [ 'x', 'y' ], $ctls, [ 'check2' ] );
+ $wanCache->getWithSetCallback( 'p', 30, $valFunc );
+ $wanCache->getCheckKeyTime( 'zzz' );
+ }
}