interact with ApiParse and ApiExpandTemplates.
* (T139565) SECURITY: API: Generate head items in the context of the given title
* (T115333) SECURITY: Check read permission when loading page content in ApiParse
+* ApiBase::getResultData() was removed (deprecated since 1.25)
* ApiBase::makeHelpArrayToString() was removed (deprecated since 1.25)
* ApiBase::makeHelpMsgParameters() was removed (deprecated since 1.25)
* ApiBase::makeHelpMsg() was removed (deprecated since 1.25)
return $this->getService( 'TitleParser' );
}
+ /**
+ * @since 1.28
+ * @return \BagOStuff
+ */
+ public function getMainObjectStash() {
+ return $this->getService( 'MainObjectStash' );
+ }
+
+ /**
+ * @since 1.28
+ * @return \WANObjectCache
+ */
+ public function getMainWANObjectCache() {
+ return $this->getService( 'MainWANObjectCache' );
+ }
+
+ /**
+ * @since 1.28
+ * @return \BagOStuff
+ */
+ public function getLocalServerObjectCache() {
+ return $this->getService( 'LocalServerObjectCache' );
+ }
+
/**
* @since 1.28
* @return VirtualRESTServiceClient
return $services->getService( '_MediaWikiTitleCodec' );
},
+ 'MainObjectStash' => function( MediaWikiServices $services ) {
+ $mainConfig = $services->getMainConfig();
+
+ $id = $mainConfig->get( 'MainStash' );
+ if ( !isset( $mainConfig->get( 'ObjectCaches' )[$id] ) ) {
+ throw new UnexpectedValueException(
+ "Cache type \"$id\" is not present in \$wgObjectCaches." );
+ }
+
+ return \ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] );
+ },
+
+ 'MainWANObjectCache' => function( MediaWikiServices $services ) {
+ $mainConfig = $services->getMainConfig();
+
+ $id = $mainConfig->get( 'MainWANCache' );
+ if ( !isset( $mainConfig->get( 'WANObjectCaches' )[$id] ) ) {
+ throw new UnexpectedValueException(
+ "WAN cache type \"$id\" is not present in \$wgWANObjectCaches." );
+ }
+
+ $params = $mainConfig->get( 'WANObjectCaches' )[$id];
+ $objectCacheId = $params['cacheId'];
+ if ( !isset( $mainConfig->get( 'ObjectCaches' )[$objectCacheId] ) ) {
+ throw new UnexpectedValueException(
+ "Cache type \"$objectCacheId\" is not present in \$wgObjectCaches." );
+ }
+ $params['store'] = $mainConfig->get( 'ObjectCaches' )[$objectCacheId];
+
+ return \ObjectCache::newWANCacheFromParams( $params );
+ },
+
+ 'LocalServerObjectCache' => function( MediaWikiServices $services ) {
+ $mainConfig = $services->getMainConfig();
+
+ if ( function_exists( 'apc_fetch' ) ) {
+ $id = 'apc';
+ } elseif ( function_exists( 'apcu_fetch' ) ) {
+ $id = 'apcu';
+ } elseif ( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
+ $id = 'xcache';
+ } elseif ( function_exists( 'wincache_ucache_get' ) ) {
+ $id = 'wincache';
+ } else {
+ $id = CACHE_NONE;
+ }
+
+ if ( !isset( $mainConfig->get( 'ObjectCaches' )[$id] ) ) {
+ throw new UnexpectedValueException(
+ "Cache type \"$id\" is not present in \$wgObjectCaches." );
+ }
+
+ return \ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] );
+ },
+
'VirtualRESTServiceClient' => function( MediaWikiServices $services ) {
$config = $services->getMainConfig()->get( 'VirtualRestConfig' );
return 0;
}
- /**
- * Get the result data array (read-only)
- * @deprecated since 1.25, use $this->getResult() methods instead
- * @return array
- */
- public function getResultData() {
- wfDeprecated( __METHOD__, '1.25' );
- return $this->getResult()->getData();
- }
-
/**
* Call wfTransactionalTimeLimit() if this request was POSTed
* @since 1.26
$out->getModuleScripts(),
$out->getModuleStyles()
) ) ),
+ 'continue' => $this->getResult()->getResultData( 'continue' ),
'time' => round( $time * 1000 ),
],
false, FormatJson::ALL_OK
if ( count( $modules ) === 1 && $m === $modules[0] &&
!( !empty( $options['submodules'] ) && $m->getModuleManager() )
) {
- $link = Html::element( 'b', null, $name );
+ $link = Html::element( 'b', [ 'dir' => 'ltr', 'lang' => 'en' ], $name );
} else {
$link = SpecialPage::getTitleFor( 'ApiHelp', $m->getModulePath() )->getLocalURL();
$link = Html::element( 'a',
- [ 'href' => $link, 'class' => 'apihelp-linktrail' ],
+ [ 'href' => $link, 'class' => 'apihelp-linktrail', 'dir' => 'ltr', 'lang' => 'en' ],
$name
);
$any = true;
if ( isset( $sourceInfo['namemsg'] ) ) {
$extname = $context->msg( $sourceInfo['namemsg'] )->text();
} else {
- $extname = $sourceInfo['name'];
+ // Probably English, so wrap it.
+ $extname = Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $sourceInfo['name'] );
}
$help['flags'] .= Html::rawElement( 'li', null,
self::wrap(
$link = SpecialPage::getTitleFor( 'Version', 'License/' . $sourceInfo['name'] );
if ( isset( $sourceInfo['license-name'] ) ) {
- $msg = $context->msg( 'api-help-license', $link, $sourceInfo['license-name'] );
+ $msg = $context->msg( 'api-help-license', $link,
+ Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $sourceInfo['license-name'] )
+ );
} elseif ( SpecialVersion::getExtLicenseFileName( dirname( $sourceInfo['path'] ) ) ) {
$msg = $context->msg( 'api-help-license-noname', $link );
} else {
$help['help-urls'] .= Html::openElement( 'ul' );
foreach ( $urls as $url ) {
$help['help-urls'] .= Html::rawElement( 'li', null,
- Html::element( 'a', [ 'href' => $url ], $url )
+ Html::element( 'a', [ 'href' => $url, 'dir' => 'ltr' ], $url )
);
}
$help['help-urls'] .= Html::closeElement( 'ul' );
$settings = [ ApiBase::PARAM_DFLT => $settings ];
}
- $help['parameters'] .= Html::element( 'dt', null,
- $module->encodeParamName( $name ) );
+ $help['parameters'] .= Html::rawElement( 'dt', null,
+ Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $module->encodeParamName( $name ) )
+ );
// Add description
$description = [];
$links = isset( $settings[ApiBase::PARAM_VALUE_LINKS] )
? $settings[ApiBase::PARAM_VALUE_LINKS]
: [];
- $type = array_map( function ( $v ) use ( $links ) {
- $ret = wfEscapeWikiText( $v );
+ $values = array_map( function ( $v ) use ( $links ) {
+ // We can't know whether this contains LTR or RTL text.
+ $ret = $v === '' ? $v : Html::element( 'span', [ 'dir' => 'auto' ], $v );
if ( isset( $links[$v] ) ) {
$ret = "[[{$links[$v]}|$ret]]";
}
}, $type );
$i = array_search( '', $type, true );
if ( $i === false ) {
- $type = $context->getLanguage()->commaList( $type );
+ $values = $context->getLanguage()->commaList( $values );
} else {
- unset( $type[$i] );
- $type = $context->msg( 'api-help-param-list-can-be-empty' )
- ->numParams( count( $type ) )
- ->params( $context->getLanguage()->commaList( $type ) )
+ unset( $values[$i] );
+ $values = $context->msg( 'api-help-param-list-can-be-empty' )
+ ->numParams( count( $values ) )
+ ->params( $context->getLanguage()->commaList( $values ) )
->parse();
}
$info[] = $context->msg( 'api-help-param-list' )
->params( $multi ? 2 : 1 )
- ->params( $type )
+ ->params( $values )
->parse();
$hintPipeSeparated = false;
} else {
$prefix = $module->isMain()
? '' : ( $module->getModulePath() . '+' );
$submodules = array_map( function ( $name ) use ( $prefix ) {
- return "[[Special:ApiHelp/{$prefix}{$name}|{$name}]]";
+ $text = Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $name );
+ return "[[Special:ApiHelp/{$prefix}{$name}|{$text}]]";
}, $submodules );
}
$count = count( $submodules );
$info[] = $context->msg( 'api-help-param-default-empty' )
->parse();
} elseif ( $default !== null && $default !== false ) {
+ // We can't know whether this contains LTR or RTL text.
$info[] = $context->msg( 'api-help-param-default' )
- ->params( wfEscapeWikiText( $default ) )
+ ->params( Html::element( 'span', [ 'dir' => 'auto' ], $default ) )
->parse();
}
$sandbox = SpecialPage::getTitleFor( 'ApiSandbox' )->getLocalURL() . '#' . $qs;
$help['examples'] .= Html::rawElement( 'dt', null, $msg->parse() );
$help['examples'] .= Html::rawElement( 'dd', null,
- Html::element( 'a', [ 'href' => $link ], "api.php?$qs" ) . ' ' .
+ Html::element( 'a', [ 'href' => $link, 'dir' => 'ltr' ], "api.php?$qs" ) . ' ' .
Html::rawElement( 'a', [ 'href' => $sandbox ],
$context->msg( 'api-help-open-in-apisandbox' )->parse() )
);
*/
public function fetchMessage() {
if ( $this->message === null ) {
- $this->message = ";{$this->paramValue}:" . parent::fetchMessage();
+ $this->message = ";<span dir=\"ltr\" lang=\"en\">{$this->paramValue}</span>:"
+ . parent::fetchMessage();
}
return $this->message;
}
* @file
* @ingroup Cache
*/
+use MediaWiki\MediaWikiServices;
/**
* MediaWiki message cache structure version.
$this->mExpiry = $expiry;
if ( $wgUseLocalMessageCache ) {
- $this->localCache = ObjectCache::getLocalServerInstance( CACHE_NONE );
+ $this->localCache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
} else {
- $this->localCache = wfGetCache( CACHE_NONE );
+ $this->localCache = new EmptyBagOStuff();
}
$this->wanCache = ObjectCache::getMainWANInstance();
*/
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
/**
* MediaWiki-specific class for generating database load balancers
}
// Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
- $sCache = ObjectCache::getLocalServerInstance();
+ $sCache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) {
$lbConf['srvCache'] = $sCache;
}
if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
$lbConf['memCache'] = $cCache;
}
- $wCache = ObjectCache::getMainWANInstance();
+ $wCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
$lbConf['wanCache'] = $wCache;
}
* @author Aaron Schulz
*/
use \MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
/**
* Class to handle file backend registration
'streamMimeFunc' => [ 'StreamFile', 'contentTypeFromPath' ],
'tmpDirectory' => wfTempDir(),
'statusWrapper' => [ 'Status', 'wrap' ],
- 'wanCache' => ObjectCache::getMainWANInstance(),
+ 'wanCache' => MediaWikiServices::getInstance()->getMainWANObjectCache(),
'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ),
'logger' => LoggerFactory::getInstance( 'FileOperation' ),
'profiler' => Profiler::instance()
}
/**
- * @param callable $callback
+ * @param callable $callback A function with one parameter, the success status, which will be
+ * false if the job failed or it succeeded but the DB changes could not be committed or
+ * any deferred updates threw an exception. (This parameter was added in 1.28.)
* @since 1.27
*/
protected function addTeardownCallback( $callback ) {
/**
* Do any final cleanup after run(), deferred updates, and all DB commits happen
- *
+ * @param bool $status Whether the job, its deferred updates, and DB commit all succeeded
* @since 1.27
*/
- public function teardown() {
+ public function teardown( $status ) {
foreach ( $this->teardownCallbacks as $callback ) {
- call_user_func( $callback );
+ call_user_func( $callback, $status );
}
}
}
// Always attempt to call teardown() even if Job throws exception.
try {
- $job->teardown();
+ $job->teardown( $status );
} catch ( Exception $e ) {
MWExceptionHandler::logException( $e );
}
/** @var int ERR_* constant for the "last error" registry */
protected $lastRelayError = self::ERR_NONE;
+ /** @var mixed[] Temporary warm-up cache */
+ private $warmupCache = [];
+
/** Max time expected to pass between delete() and DB commit finishing */
const MAX_COMMIT_DELAY = 3;
/** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */
}
// Fetch all of the raw values
- $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeysFlat ) );
+ $keysGet = array_merge( $valueKeys, $checkKeysFlat );
+ if ( $this->warmupCache ) {
+ $wrappedValues = array_intersect_key( $this->warmupCache, array_flip( $keysGet ) );
+ $keysGet = array_diff( $keysGet, array_keys( $wrappedValues ) ); // keys left to fetch
+ } else {
+ $wrappedValues = [];
+ }
+ $wrappedValues += $this->cache->getMulti( $keysGet );
// Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
$now = microtime( true );
return $value;
}
+ /**
+ * Method to fetch/regenerate multiple cache keys at once
+ *
+ * This works the same as getWithSetCallback() except:
+ * - a) The $keys argument expects the result of WANObjectCache::makeMultiKeys()
+ * - b) The $callback argument expects a callback taking the following arguments:
+ * - $id: ID of an entity to query
+ * - $oldValue : the prior cache value or false if none was present
+ * - &$ttl : a reference to the new value TTL in seconds
+ * - &$setOpts : a reference to options for set() which can be altered
+ * - $oldAsOf : generation UNIX timestamp of $oldValue or null if not present
+ * Aside from the additional $id argument, the other arguments function the same
+ * way they do in getWithSetCallback().
+ * - c) The return value is a map of (cache key => value) in the order of $keyedIds
+ *
+ * @see WANObjectCache::getWithSetCallback()
+ *
+ * Example usage:
+ * @code
+ * $rows = $cache->getMultiWithSetCallback(
+ * // Map of cache keys to entitiy IDs
+ * $cache->makeMultiKeys(
+ * $this->fileVersionIds(),
+ * function ( $id, WANObjectCache $cache ) {
+ * return $cache->makeKey( 'file-version', $id );
+ * }
+ * ),
+ * // Time-to-live (in seconds)
+ * $cache::TTL_DAY,
+ * // Function that derives the new key value
+ * return function ( $id, $oldValue, &$ttl, array &$setOpts ) {
+ * $dbr = wfGetDB( DB_REPLICA );
+ * // Account for any snapshot/replica DB lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
+ *
+ * // Load the row for this file
+ * $row = $dbr->selectRow( 'file', '*', [ 'id' => $id ], __METHOD__ );
+ *
+ * return $row ? (array)$row : false;
+ * },
+ * [
+ * // Process cache for 30 seconds
+ * 'pcTTL' => 30,
+ * // Use a dedicated 500 item cache (initialized on-the-fly)
+ * 'pcGroup' => 'file-versions:500'
+ * ]
+ * );
+ * $files = array_map( [ __CLASS__, 'newFromRow' ], $rows );
+ * @endcode
+ *
+ * @param ArrayIterator $keyedIds Result of WANObjectCache::makeMultiKeys()
+ * @param integer $ttl Seconds to live for key updates
+ * @param callable $callback Callback the yields entity regeneration callbacks
+ * @param array $opts Options map
+ * @return array Map of (cache key => value) in the same order as $keyedIds
+ * @since 1.28
+ */
+ final public function getMultiWithSetCallback(
+ ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
+ ) {
+ $keysWarmUp = iterator_to_array( $keyedIds, true );
+ $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
+ foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
+ if ( is_int( $i ) ) {
+ $keysWarmUp[] = $checkKeyOrKeys;
+ } else {
+ $keysWarmUp = array_merge( $keysWarmUp, $checkKeyOrKeys );
+ }
+ }
+
+ $this->warmupCache = $this->cache->getMulti( $keysWarmUp );
+ $this->warmupCache += array_fill_keys( $keysWarmUp, false );
+
+ // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
+ $id = null;
+ $func = function ( $oldValue, &$ttl, array $setOpts, $oldAsOf ) use ( $callback, &$id ) {
+ return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
+ };
+
+ $values = [];
+ foreach ( $keyedIds as $key => $id ) {
+ $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
+ }
+
+ $this->warmupCache = [];
+
+ return $values;
+ }
+
/**
* @see BagOStuff::makeKey()
* @param string ... Key component
return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
}
+ /**
+ * @param array $entities List of entity IDs
+ * @param callable $keyFunc Callback yielding a key from (entity ID, this WANObjectCache)
+ * @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order
+ * @since 1.28
+ */
+ public function makeMultiKeys( array $entities, callable $keyFunc ) {
+ $map = [];
+ foreach ( $entities as $entity ) {
+ $map[$keyFunc( $entity, $this )] = $entity;
+ }
+
+ return new ArrayIterator( $map );
+ }
+
/**
* Get the "last error" registered; clearLastError() should be called manually
* @return int ERR_* class constant for the "last error" registry
/** @var float Moving average ratio (e.g. 0.1 for 10% weight to new weight) */
private $movingAveRatio;
+ const VERSION = 1;
+
public function __construct(
ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache, array $options = []
) {
// Lag is per-server, not per-DB, so key on the master DB name
return $this->srvCache->makeGlobalKey(
'lag-times',
+ self::VERSION,
$this->parent->getServerName( $this->parent->getWriterIndex() )
);
}
* @file
* @ingroup Media
*/
+use MediaWiki\MediaWikiServices;
/**
* Handler for images that need to be transformed
* @return string|bool Representing the IM version; false on error
*/
protected function getMagickVersion() {
- $cache = ObjectCache::getLocalServerInstance( CACHE_NONE );
+ $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
return $cache->getWithSetCallback(
'imagemagick-version',
$cache::TTL_HOUR,
* @since 1.27
*/
public static function getLocalServerInstance( $fallback = CACHE_NONE ) {
- if ( function_exists( 'apc_fetch' ) ) {
- $id = 'apc';
- } elseif ( function_exists( 'apcu_fetch' ) ) {
- $id = 'apcu';
- } elseif ( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
- $id = 'xcache';
- } elseif ( function_exists( 'wincache_ucache_get' ) ) {
- $id = 'wincache';
- } else {
+ $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
+ if ( $cache instanceof EmptyBagOStuff ) {
if ( is_array( $fallback ) ) {
- $id = isset( $fallback['fallback'] ) ? $fallback['fallback'] : CACHE_NONE;
- } else {
- $id = $fallback;
+ $fallback = isset( $fallback['fallback'] ) ? $fallback['fallback'] : CACHE_NONE;
}
+ $cache = self::getInstance( $fallback );
}
- return self::getInstance( $id );
+ return $cache;
}
/**
*
* @since 1.26
* @return WANObjectCache
+ * @deprecated Since 1.28 Use MediaWikiServices::getMainWANCache()
*/
public static function getMainWANInstance() {
- global $wgMainWANCache;
-
- return self::getWANInstance( $wgMainWANCache );
+ return MediaWikiServices::getInstance()->getMainWANObjectCache();
}
/**
*
* @return BagOStuff
* @since 1.26
+ * @deprecated Since 1.28 Use MediaWikiServices::getMainObjectStash
*/
public static function getMainStashInstance() {
- global $wgMainStash;
-
- return self::getInstance( $wgMainStash );
+ return MediaWikiServices::getInstance()->getMainObjectStash();
}
/**
return $this->mPage->getLastPurgeTimestamp();
}
- /**
- * Call to WikiPage function for backwards compatibility.
- * @see WikiPage::doQuickEditContent
- */
- public function doQuickEditContent(
- Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
- ) {
- return $this->mPage->doQuickEditContent(
- $content, $user, $comment, $minor, $serialFormat
- );
- }
-
/**
* Call to WikiPage function for backwards compatibility.
* @see WikiPage::doViewUpdates
}
}
- /**
- * Edit an article without doing all that other stuff
- * The article must already exist; link tables etc
- * are not updated, caches are not flushed.
- *
- * @param Content $content Content submitted
- * @param User $user The relevant user
- * @param string $comment Comment submitted
- * @param bool $minor Whereas it's a minor modification
- * @param string $serialFormat Format for storing the content in the database
- */
- public function doQuickEditContent(
- Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
- ) {
-
- $serialized = $content->serialize( $serialFormat );
-
- $dbw = wfGetDB( DB_MASTER );
- $revision = new Revision( [
- 'title' => $this->getTitle(), // for determining the default content model
- 'page' => $this->getId(),
- 'user_text' => $user->getName(),
- 'user' => $user->getId(),
- 'text' => $serialized,
- 'length' => $content->getSize(),
- 'comment' => $comment,
- 'minor_edit' => $minor ? 1 : 0,
- ] ); // XXX: set the content object?
- $revision->insertOn( $dbw );
- $this->updateRevisionOn( $dbw, $revision );
-
- Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
-
- }
-
/**
* Update the article's restriction field, and leave a log entry.
* This works for protection both existing and non-existing pages.
<?php
+use MediaWiki\MediaWikiServices;
+
/**
* ExtensionRegistry class
*
// we don't want to fail here if $wgObjectCaches is not configured
// properly for APC setup
try {
- $this->cache = ObjectCache::getLocalServerInstance();
+ $this->cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
} catch ( MWException $e ) {
$this->cache = new EmptyBagOStuff();
}
}
public function isExpensive() {
- return true;
+ return false;
}
function isSyndicated() {
* @author Chris Steipp
* @file
*/
+use MediaWiki\MediaWikiServices;
class MWCryptHKDF {
* @throws MWException
*/
protected static function singleton() {
- global $wgHKDFAlgorithm, $wgHKDFSecret, $wgSecretKey, $wgMainCacheType;
+ global $wgHKDFAlgorithm, $wgHKDFSecret, $wgSecretKey;
$secret = $wgHKDFSecret ?: $wgSecretKey;
if ( !$secret ) {
$context[] = getmypid();
$context[] = gethostname();
- // Setup salt cache. Use APC, or fallback to the main cache if it isn't setup
- $cache = ObjectCache::getLocalServerInstance( $wgMainCacheType );
+ // Setup salt cache
+ $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
+ if ( $cache instanceof EmptyBagOStuff ) {
+ // Use APC, or fallback to the main cache if it isn't setup
+ $cache = ObjectCache::getLocalClusterInstance();
+ }
if ( is_null( self::$singleton ) ) {
self::$singleton = new self( $secret, $wgHKDFAlgorithm, $cache, $context );
* @author Aaron Schulz
*/
use Wikimedia\Assert\Assert;
+use MediaWiki\MediaWikiServices;
/**
* Class for getting statistically unique IDs
// Counter values would not survive accross script instances in CLI mode.
$cache = null;
if ( ( $flags & self::QUICK_VOLATILE ) && PHP_SAPI !== 'cli' ) {
- $cache = ObjectCache::getLocalServerInstance();
+ $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
}
if ( $cache ) {
$counter = $cache->incrWithInit( $bucket, $cache::TTL_INDEFINITE, $count, $count );
"apisandbox-results-fixtoken-fail": "Failed to fetch \"$1\" token.",
"apisandbox-alert-page": "Fields on this page are not valid.",
"apisandbox-alert-field": "The value of this field is not valid.",
+ "apisandbox-continue": "Continue",
+ "apisandbox-continue-clear": "Clear",
+ "apisandbox-continue-help": "{{int:apisandbox-continue}} will [https://www.mediawiki.org/wiki/API:Query#Continuing_queries continue] the last request; {{int:apisandbox-continue-clear}} will clear continuation-related parameters.",
"booksources": "Book sources",
"booksources-summary": "",
"booksources-search-legend": "Search for book sources",
"apisandbox-results-fixtoken-fail": "Displayed as an error message from JavaScript when a CSRF token could not be fetched.\n\nParameters:\n* $1 - Token type",
"apisandbox-alert-page": "Tooltip for the alert icon on a module's page tab when the page contains fields with issues.",
"apisandbox-alert-field": "Tooltip for the alert icon on a field when the field has issues.",
+ "apisandbox-continue": "Button text for sending another request using query continuation.",
+ "apisandbox-continue-clear": "Button text for clearing query continuation parameters.",
+ "apisandbox-continue-help": "Help text for the continue and clear buttons.",
"booksources": "{{doc-special|BookSources}}\n\n'''This message shouldn't be changed unless it has serious mistakes.'''\n\nIt's used as the page name of the configuration page of [[Special:BookSources]]. Changing it breaks existing sites using the default version of this message.\n\nSee also:\n* {{msg-mw|Booksources|title}}\n* {{msg-mw|Booksources-text|text}}",
"booksources-summary": "{{doc-specialpagesummary|booksources}}",
"booksources-search-legend": "Box heading on [[Special:BookSources|book sources]] special page. The box is for searching for places where a particular book can be bought or viewed.",
'apisandbox-results-fixtoken-fail',
'apisandbox-alert-page',
'apisandbox-alert-field',
+ 'apisandbox-continue',
+ 'apisandbox-continue-clear',
+ 'apisandbox-continue-help',
'blanknamespace',
],
],
suppressErrors = true,
updatingBooklet = false,
pages = {},
- moduleInfoCache = {};
+ moduleInfoCache = {},
+ baseRequestParams;
WidgetMethods = {
textInputWidget: {
/**
* Submit button handler
+ *
+ * @param {Object} [params] Use this set of params instead of those in the form fields.
+ * The form fields will be updated to match.
*/
- sendRequest: function () {
+ sendRequest: function ( params ) {
var page, subpages, i, query, $result, $focus,
progress, $progressText, progressLoading,
deferreds = [],
- params = {},
+ paramsAreForced = !!params,
displayParams = {},
checkPages = [ pages.main ];
suppressErrors = false;
+ // save widget state in params (or load from it if we are forced)
+ if ( paramsAreForced ) {
+ ApiSandbox.updateUI( params );
+ }
+ params = {};
while ( checkPages.length ) {
page = checkPages.shift();
deferreds.push( page.apiCheckValid() );
}
}
+ if ( !paramsAreForced ) {
+ // forced params means we are continuing a query; the base query should be preserved
+ baseRequestParams = $.extend( {}, params );
+ }
+
$.when.apply( $, deferreds ).done( function () {
if ( $.inArray( false, arguments ) !== -1 ) {
windowManager.openWindow( 'errorAlert', {
);
} )
.done( function ( data, jqXHR ) {
- var m, loadTime, button,
+ var m, loadTime, button, clear,
ct = jqXHR.getResponseHeader( 'Content-Type' );
$result.empty();
.text( data )
.appendTo( $result );
}
+ if ( paramsAreForced || data[ 'continue' ] ) {
+ $result.append(
+ $( '<div>' ).append(
+ new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-continue' ).text()
+ } ).on( 'click', function () {
+ ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
+ } ).setDisabled( !data[ 'continue' ] ).$element,
+ ( clear = new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-continue-clear' ).text()
+ } ).on( 'click', function () {
+ ApiSandbox.updateUI( baseRequestParams );
+ clear.setDisabled( true );
+ booklet.setPage( '|results|' );
+ } ).setDisabled( !paramsAreForced ) ).$element,
+ new OO.ui.PopupButtonWidget( {
+ framed: false,
+ icon: 'info',
+ popup: {
+ $content: $( '<div>' ).append( mw.message( 'apisandbox-continue-help' ).parse() ),
+ padded: true
+ }
+ } ).$element
+ )
+ );
+ }
if ( typeof loadTime === 'number' ) {
$result.append(
$( '<div>' ).append(
// Don't grey out the label when the field is disabled,
// it makes it too hard to read and our "disabled"
// isn't really disabled.
+ widgetField.onFieldDisable( false );
widgetField.onFieldDisable = doNothing;
if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
}
.searchresult {
font-size: 95%;
- width: 38em;
+ max-width: 38em;
}
.mw-search-results {
margin-left: 0;
* @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
* @cfg {boolean} [showImages] Show page images
* @cfg {boolean} [showDescriptions] Show page descriptions
+ * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions
* @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true,
* the widget will marks itself red for invalid inputs, including an empty query).
* @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
this.showRedlink = !!config.showRedlink;
this.showImages = !!config.showImages;
this.showDescriptions = !!config.showDescriptions;
+ this.excludeCurrentPage = !!config.excludeCurrentPage;
this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true;
this.cache = config.cache;
*/
mw.widgets.TitleWidget.prototype.getOptionsFromData = function ( data ) {
var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
+ currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(),
items = [],
titles = [],
titleObj = mw.Title.newFromText( this.getQueryValue() ),
for ( index in data.pages ) {
suggestionPage = data.pages[ index ];
+ // When excludeCurrentPage is set, don't list the current page unless the user has type the full title
+ if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {
+ continue;
+ }
pageData[ suggestionPage.title ] = {
missing: suggestionPage.missing !== undefined,
redirect: suggestionPage.redirect !== undefined,
pendingRequests.push( function () {
if ( moduleName && hasOwn.call( registry, moduleName ) ) {
+ // Emulate runScript() part of execute()
window.require = mw.loader.require;
window.module = registry[ moduleName ].module;
}
addScript( src ).always( function () {
- // Clear environment
- delete window.require;
+ // 'module.exports' should not persist after the file is executed to
+ // avoid leakage to unrelated code. 'require' should be kept, however,
+ // as asynchronous access to 'require' is allowed and expected. (T144879)
delete window.module;
r.resolve();
*/
private $dbClone;
- /**
- * @var DjVuSupport
- */
- private $djVuSupport;
-
/**
* @var TidySupport
*/
$this->runDisabled = !empty( $options['run-disabled'] );
$this->runParsoid = !empty( $options['run-parsoid'] );
- $this->djVuSupport = new DjVuSupport();
$this->tidySupport = new TidySupport( !empty( $options['use-tidy-config'] ) );
if ( !$this->tidySupport->isEnabled() ) {
$this->recorder->warning(
}
$this->setupDone[$funcName] = true;
return function () use ( $funcName ) {
- wfDebug( "markSetupDone unmarked $funcName" );
$this->setupDone[$funcName] = false;
};
}
$user = $context->getUser();
$options = ParserOptions::newFromContext( $context );
- if ( isset( $opts['djvu'] ) ) {
- if ( !$this->djVuSupport->isEnabled() ) {
- $this->recorder->skipped( $test,
- 'djvu binaries do not exist or are not executable' );
- return false;
- }
- }
-
if ( isset( $opts['tidy'] ) ) {
if ( !$this->tidySupport->isEnabled() ) {
$this->recorder->skipped( $test, 'tidy extension is not installed' );
};
// Set content language. This invalidates the magic word cache and title services
- wfDebug( "Setting up language $langCode" );
$lang = Language::factory( $langCode );
$setup['wgContLang'] = $lang;
$reset = function () {
JobQueueGroup::destroySingletons();
ObjectCache::clear();
+ $services = MediaWikiServices::getInstance();
+ $services->resetServiceForTesting( 'MainObjectStash' );
+ $services->resetServiceForTesting( 'LocalServerObjectCache' );
+ $services->getMainWANObjectCache()->clearProcessCache();
FileBackendGroup::destroySingleton();
// TODO: move global state into MediaWikiServices
'_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ],
'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ],
'TitleParser' => [ 'TitleParser', TitleParser::class ],
- 'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ],
- 'ProxyLookup' => [ 'ProxyLookup', ProxyLookup::class ]
+ 'ProxyLookup' => [ 'ProxyLookup', ProxyLookup::class ],
+ 'MainObjectStash' => [ 'MainObjectStash', BagOStuff::class ],
+ 'MainWANObjectCache' => [ 'MainWANObjectCache', WANObjectCache::class ],
+ 'LocalServerObjectCache' => [ 'LocalServerObjectCache', BagOStuff::class ],
+ 'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ]
];
}
}
/**
+ * @covers ComposerJson::__construct
* @covers ComposerJson::getRequiredDependencies
*/
public function testGetRequiredDependencies() {
}
/**
+ * @covers ComposerLock::__construct
* @covers ComposerLock::getInstalledDependencies
*/
public function testGetInstalledDependencies() {
}
$wanCache = TestingAccessWrapper::newFromObject( $this->cache );
+ /** @noinspection PhpUndefinedFieldInspection */
$this->internalCache = $wanCache->cache;
}
* @dataProvider provideSetAndGet
* @covers WANObjectCache::set()
* @covers WANObjectCache::get()
+ * @covers WANObjectCache::makeKey()
* @param mixed $value
* @param integer $ttl
*/
public function testSetAndGet( $value, $ttl ) {
$curTTL = null;
$asOf = null;
- $key = wfRandomString();
+ $key = $this->cache->makeKey( 'x', wfRandomString() );
$this->cache->get( $key, $curTTL, [], $asOf );
$this->assertNull( $curTTL, "Current TTL is null" );
/**
* @covers WANObjectCache::get()
+ * @covers WANObjectCache::makeGlobalKey()
*/
public function testGetNotExists() {
- $key = wfRandomString();
+ $key = $this->cache->makeGlobalKey( 'y', wfRandomString(), 'p' );
$curTTL = null;
$value = $this->cache->get( $key, $curTTL );
$priorAsOf = null;
$wasSet = 0;
$func = function( $old, &$ttl, &$opts, $asOf )
- use ( &$wasSet, &$priorValue, &$priorAsOf, $value )
+ use ( &$wasSet, &$priorValue, &$priorAsOf, $value )
{
++$wasSet;
$priorValue = $old;
$wasSet = 0;
$v = $cache->getWithSetCallback( $key, 30, $func, [
- 'lowTTL' => 0,
- 'lockTSE' => 5,
- ] + $extOpts );
+ 'lowTTL' => 0,
+ 'lockTSE' => 5,
+ ] + $extOpts );
$this->assertEquals( $value, $v, "Value returned" );
$this->assertEquals( 0, $wasSet, "Value not regenerated" );
];
}
+ /**
+ * @dataProvider getMultiWithSetCallback_provider
+ * @covers WANObjectCache::geMultitWithSetCallback()
+ * @covers WANObjectCache::makeMultiKeys()
+ * @param array $extOpts
+ * @param bool $versioned
+ */
+ public function testGetMultiWithSetCallback( array $extOpts, $versioned ) {
+ $cache = $this->cache;
+
+ $keyA = wfRandomString();
+ $keyB = wfRandomString();
+ $keyC = wfRandomString();
+ $cKey1 = wfRandomString();
+ $cKey2 = wfRandomString();
+
+ $priorValue = null;
+ $priorAsOf = null;
+ $wasSet = 0;
+ $genFunc = function ( $id, $old, &$ttl, &$opts, $asOf ) use (
+ &$wasSet, &$priorValue, &$priorAsOf
+ ) {
+ ++$wasSet;
+ $priorValue = $old;
+ $priorAsOf = $asOf;
+ $ttl = 20; // override with another value
+ return "@$id$";
+ };
+
+ $wasSet = 0;
+ $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
+ $value = "@3353$";
+ $v = $cache->getMultiWithSetCallback(
+ $keyedIds, 30, $genFunc, [ 'lockTSE' => 5 ] + $extOpts );
+ $this->assertEquals( $value, $v[$keyA], "Value returned" );
+ $this->assertEquals( 1, $wasSet, "Value regenerated" );
+ $this->assertFalse( $priorValue, "No prior value" );
+ $this->assertNull( $priorAsOf, "No prior value" );
+
+ $curTTL = null;
+ $cache->get( $keyA, $curTTL );
+ $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
+ $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
+
+ $wasSet = 0;
+ $value = "@efef$";
+ $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
+ $v = $cache->getMultiWithSetCallback(
+ $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+ $this->assertEquals( $value, $v[$keyB], "Value returned" );
+ $this->assertEquals( 1, $wasSet, "Value regenerated" );
+ $v = $cache->getMultiWithSetCallback(
+ $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+ $this->assertEquals( $value, $v[$keyB], "Value returned" );
+ $this->assertEquals( 1, $wasSet, "Value not regenerated" );
+
+ $priorTime = microtime( true );
+ usleep( 1 );
+ $wasSet = 0;
+ $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
+ $v = $cache->getMultiWithSetCallback(
+ $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+ );
+ $this->assertEquals( $value, $v[$keyB], "Value returned" );
+ $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
+ $this->assertEquals( $value, $priorValue, "Has prior value" );
+ $this->assertType( 'float', $priorAsOf, "Has prior value" );
+ $t1 = $cache->getCheckKeyTime( $cKey1 );
+ $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
+ $t2 = $cache->getCheckKeyTime( $cKey2 );
+ $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
+
+ $priorTime = microtime( true );
+ $value = "@43636$";
+ $wasSet = 0;
+ $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
+ $v = $cache->getMultiWithSetCallback(
+ $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+ );
+ $this->assertEquals( $value, $v[$keyC], "Value returned" );
+ $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
+ $t1 = $cache->getCheckKeyTime( $cKey1 );
+ $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
+ $t2 = $cache->getCheckKeyTime( $cKey2 );
+ $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
+
+ $curTTL = null;
+ $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
+ if ( $versioned ) {
+ $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
+ } else {
+ $this->assertEquals( $value, $v, "Value returned" );
+ }
+ $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
+
+ $wasSet = 0;
+ $key = wfRandomString();
+ $keyedIds = new ArrayIterator( [ $key => 242424 ] );
+ $v = $cache->getMultiWithSetCallback(
+ $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
+ $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
+ $cache->delete( $key );
+ $keyedIds = new ArrayIterator( [ $key => 242424 ] );
+ $v = $cache->getMultiWithSetCallback(
+ $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
+ $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
+ $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
+
+ $calls = 0;
+ $ids = [ 1, 2, 3, 4, 5, 6 ];
+ $keyFunc = function ( $id, WANObjectCache $wanCache ) {
+ return $wanCache->makeKey( 'test', $id );
+ };
+ $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
+ $genFunc = function ( $id, $oldValue, &$ttl, array &$setops ) use ( &$calls ) {
+ ++$calls;
+
+ return "val-{$id}";
+ };
+ $values = $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
+
+ $this->assertEquals(
+ [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
+ array_values( $values ),
+ "Correct values in correct order"
+ );
+ $this->assertEquals(
+ array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
+ array_keys( $values ),
+ "Correct keys in correct order"
+ );
+ $this->assertEquals( count( $ids ), $calls );
+
+ $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
+ $this->assertEquals( count( $ids ), $calls, "Values cached" );
+ }
+
+ public static function getMultiWithSetCallback_provider() {
+ return [
+ [ [], false ],
+ [ [ 'version' => 1 ], true ]
+ ];
+ }
+
/**
* @covers WANObjectCache::getWithSetCallback()
* @covers WANObjectCache::doGetWithSetCallback()
/**
* @dataProvider provideAdaptiveTTL
* @covers WANObjectCache::adaptiveTTL()
+ * @param float|int $ago
+ * @param int $maxTTL
+ * @param int $minTTL
+ * @param float $factor
+ * @param int $adaptiveTTL
*/
public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
- $mtime = is_int( $ago ) ? time() - $ago : $ago;
+ $mtime = $ago ? time() - $ago : $ago;
$margin = 5;
$ttl = $this->cache->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
/**
* @covers WikiPage::doEditContent
+ * @covers WikiPage::doModify
+ * @covers WikiPage::doCreate
+ * @covers WikiPage::doEditUpdates
*/
public function testDoEditContent() {
$page = $this->newPage( "WikiPageTest_testDoEditContent" );
$this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
}
- /**
- * @covers WikiPage::doQuickEditContent
- */
- public function testDoQuickEditContent() {
- global $wgUser;
-
- $page = $this->createPage(
- "WikiPageTest_testDoQuickEditContent",
- "original text",
- CONTENT_MODEL_WIKITEXT
- );
-
- $content = ContentHandler::makeContent(
- "quick text",
- $page->getTitle(),
- CONTENT_MODEL_WIKITEXT
- );
- $page->doQuickEditContent( $content, $wgUser, "testing q" );
-
- # ---------------------
- $page = new WikiPage( $page->getTitle() );
- $this->assertTrue( $content->equals( $page->getContent() ) );
- }
-
/**
* @covers WikiPage::doDeleteArticle
*/
*/
class MockDjVuHandler extends DjVuHandler {
+ function isEnabled() {
+ return true;
+ }
+
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
if ( !$this->normaliseParams( $image, $params ) ) {
return new TransformParameterError( $params );
-module.exports = 'Define worked.';
+module.exports = 'Defined.';
-var x = require( 'test.require.define' );
-module.exports = 'Require worked.' + x;
+module.exports = {
+ immediate: require( 'test.require.define' ),
+ later: function () {
+ return require( 'test.require.define' );
+ }
+};
} );
} );
- QUnit.test( 'require() in debug mode', 1, function ( assert ) {
+ QUnit.test( 'require() in debug mode', function ( assert ) {
var path = mw.config.get( 'wgScriptPath' );
mw.loader.register( [
[ 'test.require.define', '0' ],
mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
return mw.loader.using( 'test.require.callback' ).then( function ( require ) {
- var exported = require( 'test.require.callback' );
- assert.strictEqual( exported, 'Require worked.Define worked.',
- 'module.exports worked in debug mode' );
+ var cb = require( 'test.require.callback' );
+ assert.strictEqual( cb.immediate, 'Defined.', 'module.exports and require work in debug mode' );
+ // Must use try-catch because cb.later() will throw if require is undefined,
+ // which doesn't work well inside Deferred.then() when using jQuery 1.x with QUnit
+ try {
+ assert.strictEqual( cb.later(), 'Defined.', 'require works asynchrously in debug mode' );
+ } catch ( e ) {
+ assert.equal( null, String( e ), 'require works asynchrously in debug mode' );
+ }
}, function () {
assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
} );