specified, deprecated in 1.30, have been removed.
* BufferingStatsdDataFactory::getBuffer(), deprecated in 1.30, has been removed.
* The constant DB_SLAVE, deprecated in 1.28, has been removed. Use DB_REPLICA.
+* The constants NS_IMAGE and NS_IMAGE_TALK, deprecated in 1.14, have been
+ removed. Use NS_FILE and NS_FILE_TALK respectively.
* Replacer, DoubleReplacer, HashtableReplacer and RegexlikeReplacer
(deprecated in 1.32) have been removed. Closures should be used instead.
* OutputPage::addWikiText(), ::addWikiTextWithTitle(), ::addWikiTextTitleTidy(),
AuthChangeFormFields hook or security levels instead.
* WikiMap::getWikiIdFromDomain(), deprecated in 1.33, has been removed.
Use WikiMap::getWikiIdFromDbDomain() instead.
+* The config variables $wgHtml5, $wgJsMimeType, and $wgXhtmlDefaultNamespace,
+ which were deprecated and ignored by core since 1.22, are no longer set to any
+ value, and SkinTemplate no longer emits a 'jsmimetype' key. Any extensions not
+ updated since 2013 to cope with this deprecation may now break.
* …
=== Deprecations in 1.34 ===
"SkinOOUIThemes": {
"type": "object"
},
+ "OOUIThemePaths": {
+ "type": "object",
+ "description": "Map of custom OOUI theme names to paths to load them from. Same format as ResourceLoaderOOUIModule::$builtinThemePaths.",
+ "patternProperties": {
+ "^[A-Za-z]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "scripts": {
+ "type": "string",
+ "description": "Path to script file."
+ },
+ "styles": {
+ "type": "string",
+ "description": "Path to style files. '{module}' will be replaced with the module's name."
+ },
+ "images": {
+ "type": [ "string", "null" ],
+ "description": "Path to images (optional). '{module}' will be replaced with the module's name."
+ }
+ }
+ }
+ }
+ },
"PasswordPolicy": {
"type": "object",
"description": "Password policies"
"type": "object",
"description": "Map of skin names to OOUI themes to use. Same format as ResourceLoaderOOUIModule::$builtinSkinThemeMap."
},
+ "OOUIThemePaths": {
+ "type": "object",
+ "description": "Map of custom OOUI theme names to paths to load them from. Same format as ResourceLoaderOOUIModule::$builtinThemePaths.",
+ "patternProperties": {
+ "^[A-Za-z]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "scripts": {
+ "type": "string",
+ "description": "Path to script file."
+ },
+ "styles": {
+ "type": "string",
+ "description": "Path to style files. '{module}' will be replaced with the module's name."
+ },
+ "images": {
+ "type": [ "string", "null" ],
+ "description": "Path to images (optional). '{module}' will be replaced with the module's name."
+ }
+ }
+ }
+ }
+ },
"PasswordPolicy": {
"type": "object",
"description": "Password policies"
*/
$wgMimeType = 'text/html';
-/**
- * Previously used as content type in HTML script tags. This is now ignored since
- * HTML5 doesn't require a MIME type for script tags (javascript is the default).
- * It was also previously used by RawAction to determine the ctype query parameter
- * value that will result in a javascript response.
- * @deprecated since 1.22
- */
-$wgJsMimeType = null;
-
-/**
- * The default xmlns attribute. The option to define this has been removed.
- * The value of this variable is no longer used by core and is set to a fixed
- * value in Setup.php for compatibility with extensions that depend on the value
- * of this variable being set. Such a dependency however is deprecated.
- * @deprecated since 1.22
- */
-$wgXhtmlDefaultNamespace = null;
-
-/**
- * Previously used to determine if we should output an HTML5 doctype.
- * This is no longer used as we always output HTML5 now. For compatibility with
- * extensions that still check the value of this config it's value is now forced
- * to true by Setup.php.
- * @deprecated since 1.22
- */
-$wgHtml5 = true;
-
/**
* Defines the value of the version attribute in the <html> tag, if any.
*
define( 'NS_HELP_TALK', 13 );
define( 'NS_CATEGORY', 14 );
define( 'NS_CATEGORY_TALK', 15 );
-
-/**
- * NS_IMAGE and NS_IMAGE_TALK are the pre-v1.14 names for NS_FILE and
- * NS_FILE_TALK respectively, and are kept for compatibility.
- *
- * When writing code that should be compatible with older MediaWiki
- * versions, either stick to the old names or define the new constants
- * yourself, if they're not defined already.
- *
- * @deprecated since 1.14
- */
-define( 'NS_IMAGE', NS_FILE );
-/**
- * @deprecated since 1.14
- */
-define( 'NS_IMAGE_TALK', NS_FILE_TALK );
/**@}*/
/**@{
$wgDebugToolbar = false;
}
-// We always output HTML5 since 1.22, overriding these is no longer supported
-// we set them here for extensions that depend on its value.
-$wgHtml5 = true;
-$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml';
-$wgJsMimeType = 'text/javascript';
-
// Blacklisted file extensions shouldn't appear on the "allowed" list
$wgFileExtensions = array_values( array_diff( $wgFileExtensions, $wgFileBlacklist ) );
* Type: Measure (in milliseconds).
* Variable `kClass`: The first part of your cache key.
+#### `wanobjectcache.{kClass}.regen_walltime`
+
+Upon cache miss, this measures the time spent in `WANObjectCache::getWithSetCallback()`
+from the start of the callback to right after the new value has been computed.
+
+* Type: Measure (in milliseconds).
+* Variable `kClass`: The first part of your cache key.
+
#### `wanobjectcache.{kClass}.ck_touch.{result}`
Call counter from `WANObjectCache::touchCheckKey()`.
protected $asyncHandler;
/** @var float Unix timestamp of the oldest possible valid values */
protected $epoch;
+ /** @var string Stable secret used for hasing long strings into key components */
+ protected $secret;
/** @var int Callback stack depth for getWithSetCallback() */
private $callbackDepth = 0;
* is configured to interpret /<region>/<cluster>/ key prefixes as routes. This
* requires that "region" and "cluster" are both set above. [optional]
* - epoch: lowest UNIX timestamp a value/tombstone must have to be valid. [optional]
+ * - secret: stable secret used for hashing long strings into key components. [optional]
*/
public function __construct( array $params ) {
$this->cache = $params['cache'];
$this->cluster = $params['cluster'] ?? 'wan-main';
$this->mcrouterAware = !empty( $params['mcrouterAware'] );
$this->epoch = $params['epoch'] ?? self::EPOCH_UNIX_ONE_SECOND;
+ $this->secret = $params['secret'] ?? (string)$this->epoch;
$this->setLogger( $params['logger'] ?? new NullLogger() );
$this->stats = $params['stats'] ?? new NullStatsdDataFactory();
*
* @param string $key Cache key made from makeKey() or makeGlobalKey()
* @param mixed|null &$curTTL Approximate TTL left on the key if present/tombstoned [returned]
- * @param array $checkKeys List of "check" keys
+ * @param string[] $checkKeys The "check" keys used to validate the value
* @param mixed|null &$info Key info if WANObjectCache::PASS_BY_REF [returned]
* @return mixed Value of cache key or false on failure
*/
* Othwerwise, $info will transform into a map of (cache key => cached value timestamp).
* Only the cache keys listed in $keys that exists or are tombstoned will have an entry.
*
+ * $checkKeys holds the "check" keys used to validate values of applicable keys. The integer
+ * indexes hold "check" keys that apply to all of $keys while the string indexes hold "check"
+ * keys that only apply to the cache key with that name.
+ *
* @see WANObjectCache::get()
*
- * @param array $keys List of cache keys made from makeKey() or makeGlobalKey()
+ * @param string[] $keys List of cache keys made from makeKey() or makeGlobalKey()
* @param mixed|null &$curTTLs Map of (key => TTL left) for existing/tombstoned keys [returned]
- * @param array $checkKeys List of check keys to apply to all $keys. May also apply "check"
- * keys to specific cache keys only by using cache keys as keys in the $checkKeys array.
+ * @param string[]|string[][] $checkKeys Map of (integer or cache key => "check" key(s))
* @param mixed|null &$info Map of (key => info) if WANObjectCache::PASS_BY_REF [returned]
- * @return array Map of (key => value) for keys that exist and are not tombstoned
+ * @return mixed[] Map of (key => value) for existing values; order of $keys is preserved
*/
final public function getMulti(
array $keys,
/**
* @since 1.27
- * @param array $timeKeys List of prefixed time check keys
- * @param array $wrappedValues
+ * @param string[] $timeKeys List of prefixed time check keys
+ * @param mixed[] $wrappedValues
* @param float $now
- * @return array List of purge value arrays
+ * @return array[] List of purge value arrays
*/
private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
$purgeValues = [];
* @see WANObjectCache::getCheckKeyTime()
* @see WANObjectCache::getWithSetCallback()
*
- * @param array $keys
+ * @param string[] $keys
* @return float[] Map of (key => UNIX timestamp)
* @since 1.31
*/
) {
// How long it took to generate the value
$walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
+ $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
// If the key is write-holed then use the (volatile) interim key as an alternative
if ( $isKeyTombstoned ) {
$this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
* // Map of cache keys to entity IDs
* $cache->makeMultiKeys(
* $this->fileVersionIds(),
- * function ( $id, WANObjectCache $cache ) {
+ * function ( $id ) use ( $cache ) {
* return $cache->makeKey( 'file-version', $id );
* }
* ),
* @param int $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
+ * @return mixed[] 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 = []
) {
- $valueKeys = array_keys( $keyedIds->getArrayCopy() );
-
// Load required keys into process cache in one go
$this->warmupCache = $this->getRawKeysForWarmup(
- $this->getNonProcessCachedKeys( $valueKeys, $opts ),
+ $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
$opts['checkKeys'] ?? []
);
$this->warmupKeyMisses = 0;
* // Map of cache keys to entity IDs
* $cache->makeMultiKeys(
* $this->fileVersionIds(),
- * function ( $id, WANObjectCache $cache ) {
+ * function ( $id ) use ( $cache ) {
* return $cache->makeKey( 'file-version', $id );
* }
* ),
* @param int $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
+ * @return mixed[] Map of (cache key => value) in the same order as $keyedIds
* @since 1.30
*/
final public function getMultiWithUnionSetCallback(
ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
) {
- $idsByValueKey = $keyedIds->getArrayCopy();
- $valueKeys = array_keys( $idsByValueKey );
$checkKeys = $opts['checkKeys'] ?? [];
unset( $opts['lockTSE'] ); // incompatible
unset( $opts['busyValue'] ); // incompatible
// Load required keys into process cache in one go
- $keysGet = $this->getNonProcessCachedKeys( $valueKeys, $opts );
- $this->warmupCache = $this->getRawKeysForWarmup( $keysGet, $checkKeys );
+ $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
+ $this->warmupCache = $this->getRawKeysForWarmup( $keysByIdGet, $checkKeys );
$this->warmupKeyMisses = 0;
// IDs of entities known to be in need of regeneration
// Find out which keys are missing/deleted/stale
$curTTLs = [];
$asOfs = [];
- $curByKey = $this->getMulti( $keysGet, $curTTLs, $checkKeys, $asOfs );
- foreach ( $keysGet as $key ) {
+ $curByKey = $this->getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
+ foreach ( $keysByIdGet as $id => $key ) {
if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
- $idsRegen[] = $idsByValueKey[$key];
+ $idsRegen[] = $id;
}
}
// Run the cache-aside logic using warmupCache instead of persistent cache queries
$values = [];
- foreach ( $idsByValueKey as $key => $id ) { // preserve order
+ foreach ( $keyedIds as $key => $id ) { // preserve order
$values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
}
}
/**
- * @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
+ * Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey()
+ *
+ * @param string $component A raw component used in building a cache key
+ * @return string 64 character HMAC using a stable secret for public collision resistance
+ * @since 1.34
+ */
+ public function hash256( $component ) {
+ return hash_hmac( 'sha256', $component, $this->secret );
+ }
+
+ /**
+ * Get an iterator of (cache key => entity ID) for a list of entity IDs
+ *
+ * The callback takes an ID string and returns a key via makeKey()/makeGlobalKey().
+ * There should be no network nor filesystem I/O used in the callback. The entity
+ * ID/key mapping must be 1:1 or an exception will be thrown. If hashing is needed,
+ * then use the hash256() method.
+ *
+ * Example usage for the default keyspace:
+ * @code
+ * $keyedIds = $cache->makeMultiKeys(
+ * $modules,
+ * function ( $module ) use ( $cache ) {
+ * return $cache->makeKey( 'module-info', $module );
+ * }
+ * );
+ * @endcode
+ *
+ * Example usage for mixed default and global keyspace:
+ * @code
+ * $keyedIds = $cache->makeMultiKeys(
+ * $filters,
+ * function ( $filter ) use ( $cache ) {
+ * return ( strpos( $filter, 'central:' ) === 0 )
+ * ? $cache->makeGlobalKey( 'regex-filter', $filter )
+ * : $cache->makeKey( 'regex-filter', $filter )
+ * }
+ * );
+ * @endcode
+ *
+ * Example usage with hashing:
+ * @code
+ * $keyedIds = $cache->makeMultiKeys(
+ * $urls,
+ * function ( $url ) use ( $cache ) {
+ * return $cache->makeKey( 'url-info', $cache->hash256( $url ) );
+ * }
+ * );
+ * @endcode
+ *
+ * @see WANObjectCache::makeKey()
+ * @see WANObjectCache::makeGlobalKey()
+ * @see WANObjectCache::hash256()
+ *
+ * @param string[]|int[] $ids List of entity IDs
+ * @param callable $keyCallback Function returning makeKey()/makeGlobalKey() on the input ID
+ * @return ArrayIterator Iterator of (cache key => ID); order of $ids is preserved
+ * @throws UnexpectedValueException
* @since 1.28
*/
- final public function makeMultiKeys( array $entities, callable $keyFunc ) {
- $map = [];
- foreach ( $entities as $entity ) {
- $map[$keyFunc( $entity, $this )] = $entity;
+ final public function makeMultiKeys( array $ids, $keyCallback ) {
+ $idByKey = [];
+ foreach ( $ids as $id ) {
+ // Discourage triggering of automatic makeKey() hashing in some backends
+ if ( strlen( $id ) > 64 ) {
+ $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
+ }
+ $key = $keyCallback( $id, $this );
+ // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
+ if ( !isset( $idByKey[$key] ) ) {
+ $idByKey[$key] = $id;
+ } elseif ( (string)$id !== (string)$idByKey[$key] ) {
+ throw new UnexpectedValueException(
+ "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
+ );
+ }
+ }
+
+ return new ArrayIterator( $idByKey );
+ }
+
+ /**
+ * Get an (ID => value) map from (i) a non-unique list of entity IDs, and (ii) the list
+ * of corresponding entity values by first appearance of each ID in the entity ID list
+ *
+ * For use with getMultiWithSetCallback() and getMultiWithUnionSetCallback().
+ *
+ * *Only* use this method if the entity ID/key mapping is trivially 1:1 without exception.
+ * Key generation method must utitilize the *full* entity ID in the key (not a hash of it).
+ *
+ * Example usage:
+ * @code
+ * $poems = $cache->getMultiWithSetCallback(
+ * $cache->makeMultiKeys(
+ * $uuids,
+ * function ( $uuid ) use ( $cache ) {
+ * return $cache->makeKey( 'poem', $uuid );
+ * }
+ * ),
+ * $cache::TTL_DAY,
+ * function ( $uuid ) use ( $url ) {
+ * return $this->http->run( [ 'method' => 'GET', 'url' => "$url/$uuid" ] );
+ * }
+ * );
+ * $poemsByUUID = $cache->multiRemap( $uuids, $poems );
+ * @endcode
+ *
+ * @see WANObjectCache::makeMultiKeys()
+ * @see WANObjectCache::getMultiWithSetCallback()
+ * @see WANObjectCache::getMultiWithUnionSetCallback()
+ *
+ * @param string[]|int[] $ids Entity ID list makeMultiKeys()
+ * @param mixed[] $res Result of getMultiWithSetCallback()/getMultiWithUnionSetCallback()
+ * @return mixed[] Map of (ID => value); order of $ids is preserved
+ * @since 1.34
+ */
+ final public function multiRemap( array $ids, array $res ) {
+ if ( count( $ids ) !== count( $res ) ) {
+ // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
+ // ArrayIterator will have less entries due to "first appearance" de-duplication
+ $ids = array_keys( array_flip( $ids ) );
+ if ( count( $ids ) !== count( $res ) ) {
+ throw new UnexpectedValueException( "Multi-key result does not match ID list" );
+ }
}
- return new ArrayIterator( $map );
+ return array_combine( $ids, $res );
}
/**
}
/**
- * @param array $keys
+ * @param string[] $keys
* @param string $prefix
- * @return string[]
+ * @return string[] Prefix keys; the order of $keys is preserved
*/
protected static function prefixCacheKeys( array $keys, $prefix ) {
$res = [];
}
/**
- * @param array $keys
+ * @param ArrayIterator $keys
* @param array $opts
- * @return string[] List of keys
+ * @return string[] Map of (ID => cache key)
*/
- private function getNonProcessCachedKeys( array $keys, array $opts ) {
+ private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
$pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
- $keysFound = [];
+ $keysMissing = [];
if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
$version = $opts['version'] ?? null;
$pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
- foreach ( $keys as $key ) {
- if ( $pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
- $keysFound[] = $key;
+ foreach ( $keys as $key => $id ) {
+ if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
+ $keysMissing[$id] = $key;
}
}
}
- return array_diff( $keys, $keysFound );
+ return $keysMissing;
}
/**
- * @param array $keys
- * @param array $checkKeys
- * @return array Map of (cache key => mixed)
+ * @param string[] $keys
+ * @param string[]|string[][] $checkKeys
+ * @return string[] List of cache keys
*/
private function getRawKeysForWarmup( array $keys, array $checkKeys ) {
if ( !$keys ) {
* @throws UnexpectedValueException
*/
public static function newWANCacheFromParams( array $params ) {
- global $wgCommandLineMode;
+ global $wgCommandLineMode, $wgSecretKey;
$services = MediaWikiServices::getInstance();
$params['cache'] = self::newFromParams( $params['store'] );
// Let pre-emptive refreshes happen post-send on HTTP requests
$params['asyncHandler'] = [ DeferredUpdates::class, 'addCallableUpdate' ];
}
+ $params['secret'] = $params['secret'] ?? $wgSecretKey;
$class = $params['class'];
return new $class( $params );
'ResourceFileModulePaths',
'ResourceModules',
'ResourceModuleSkinStyles',
+ 'OOUIThemePaths',
'QUnitTestModule',
'ExtensionMessagesFiles',
'MessagesDirs',
}
}
- foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles' ] as $setting ) {
+ foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
if ( isset( $info[$setting] ) ) {
foreach ( $info[$setting] as $name => $data ) {
if ( isset( $data['localBasePath'] ) ) {
if ( $defaultPaths ) {
$data += $defaultPaths;
}
- $this->globals["wg$setting"][$name] = $data;
+ if ( $setting === 'OOUIThemePaths' ) {
+ $this->attributes[$setting][$name] = $data;
+ } else {
+ $this->globals["wg$setting"][$name] = $data;
+ }
}
}
}
case 'debugScripts':
case 'styles':
case 'packageFiles':
- $this->{$member} = (array)$option;
+ $this->{$member} = is_array( $option ) ? $option : [ $option ];
break;
case 'templates':
$hasTemplates = true;
- $this->{$member} = (array)$option;
+ $this->{$member} = is_array( $option ) ? $option : [ $option ];
break;
// Collated lists of file paths
case 'languageScripts':
"'$key' given, string expected."
);
}
- $this->{$member}[$key] = (array)$value;
+ $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
}
break;
case 'deprecated':
// Ensure relevant template compiler module gets loaded
foreach ( $this->templates as $alias => $templatePath ) {
if ( is_int( $alias ) ) {
- $alias = $templatePath;
+ $alias = $this->getPath( $templatePath );
}
$suffix = explode( '.', $alias );
$suffix = end( $suffix );
return $summary;
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getPath();
+ }
+
+ return $path;
+ }
+
/**
* @param string|ResourceLoaderFilePath $path
* @return string
foreach ( $this->templates as $alias => $templatePath ) {
// Alias is optional
if ( is_int( $alias ) ) {
- $alias = $templatePath;
+ $alias = $this->getPath( $templatePath );
}
$localPath = $this->getLocalPath( $templatePath );
if ( file_exists( $localPath ) ) {
return "{$this->remoteBasePath}/{$this->path}";
}
+ /**
+ * @return string
+ */
+ public function getLocalBasePath() {
+ return $this->localBasePath;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRemoteBasePath() {
+ return $this->remoteBasePath;
+ }
+
/**
* @return string
*/
// Ensure that all files have common extension.
$extensions = [];
- $descriptor = (array)$this->descriptor;
+ $descriptor = is_array( $this->descriptor ) ? $this->descriptor : [ $this->descriptor ];
array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) {
- $extensions[] = pathinfo( $path, PATHINFO_EXTENSION );
+ $extensions[] = pathinfo( $this->getLocalPath( $path ), PATHINFO_EXTENSION );
} );
$extensions = array_unique( $extensions );
if ( count( $extensions ) !== 1 ) {
*/
public function getPath( ResourceLoaderContext $context ) {
$desc = $this->descriptor;
- if ( is_string( $desc ) ) {
- return $this->basePath . '/' . $desc;
+ if ( !is_array( $desc ) ) {
+ return $this->getLocalPath( $desc );
}
if ( isset( $desc['lang'] ) ) {
$contextLang = $context->getLanguage();
if ( isset( $desc['lang'][$contextLang] ) ) {
- return $this->basePath . '/' . $desc['lang'][$contextLang];
+ return $this->getLocalPath( $desc['lang'][$contextLang] );
}
$fallbacks = Language::getFallbacksFor( $contextLang, Language::STRICT_FALLBACKS );
foreach ( $fallbacks as $lang ) {
if ( isset( $desc['lang'][$lang] ) ) {
- return $this->basePath . '/' . $desc['lang'][$lang];
+ return $this->getLocalPath( $desc['lang'][$lang] );
}
}
}
if ( isset( $desc[$context->getDirection()] ) ) {
- return $this->basePath . '/' . $desc[$context->getDirection()];
+ return $this->getLocalPath( $desc[$context->getDirection()] );
}
if ( isset( $desc['default'] ) ) {
- return $this->basePath . '/' . $desc['default'];
+ return $this->getLocalPath( $desc['default'] );
} else {
throw new MWException( 'No matching path found' );
}
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getLocalPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getLocalPath();
+ }
+
+ return "{$this->basePath}/$path";
+ }
+
/**
* Get the extension of the image.
*
$this->definition = null;
if ( isset( $options['data'] ) ) {
- $dataPath = $this->localBasePath . '/' . $options['data'];
+ $dataPath = $this->getLocalPath( $options['data'] );
$data = json_decode( file_get_contents( $dataPath ), true );
$options = array_merge( $data, $options );
}
$this->images[$skin] = $this->images['default'] ?? [];
}
foreach ( $this->images[$skin] as $name => $options ) {
- $fileDescriptor = is_string( $options ) ? $options : $options['file'];
+ $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
$allowedVariants = array_merge(
( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
return array_map( [ __CLASS__, 'safeFileHash' ], $files );
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getLocalPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getLocalPath();
+ }
+
+ return "{$this->localBasePath}/$path";
+ }
+
/**
* Extract a local base path from module definition information.
*
// Find the path to the JSON file which contains the actual image definitions for this theme
if ( $module ) {
$dataPath = $this->getThemeImagesPath( $theme, $module );
+ if ( !$dataPath ) {
+ return false;
+ }
} else {
// Backwards-compatibility for things that probably shouldn't have used this class...
$dataPath =
* @return array|false
*/
protected function readJSONFile( $dataPath ) {
- $localDataPath = $this->localBasePath . '/' . $dataPath;
+ $localDataPath = $this->getLocalPath( $dataPath );
if ( !file_exists( $localDataPath ) ) {
return false;
// Expand the paths to images (since they are relative to the JSON file that defines them, not
// our base directory)
$fixPath = function ( &$path ) use ( $dataPath ) {
- $path = dirname( $dataPath ) . '/' . $path;
+ if ( $dataPath instanceof ResourceLoaderFilePath ) {
+ $path = new ResourceLoaderFilePath(
+ dirname( $dataPath->getPath() ) . '/' . $path,
+ $dataPath->getLocalBasePath(),
+ $dataPath->getRemoteBasePath()
+ );
+ } else {
+ $path = dirname( $dataPath ) . '/' . $path;
+ }
};
array_walk( $data['images'], function ( &$value ) use ( $fixPath ) {
if ( is_string( $value['file'] ) ) {
* Return a map of theme names to lists of paths from which a given theme should be loaded.
*
* Keys are theme names, values are associative arrays. Keys of the inner array are 'scripts',
- * 'styles', or 'images', and values are string paths.
+ * 'styles', or 'images', and values are paths. Paths may be strings or ResourceLoaderFilePaths.
*
* Additionally, the string '{module}' in paths represents the name of the module to load.
*
*/
protected static function getThemePaths() {
$themePaths = self::$builtinThemePaths;
+ $themePaths += ExtensionRegistry::getInstance()->getAttribute( 'OOUIThemePaths' );
+
+ list( $defaultLocalBasePath, $defaultRemoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths();
+
+ // Allow custom themes' paths to be relative to the skin/extension that defines them,
+ // like with ResourceModuleSkinStyles
+ foreach ( $themePaths as $theme => &$paths ) {
+ list( $localBasePath, $remoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths( $paths );
+ if ( $localBasePath !== $defaultLocalBasePath || $remoteBasePath !== $defaultRemoteBasePath ) {
+ foreach ( $paths as &$path ) {
+ $path = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
+ }
+ }
+ }
+
return $themePaths;
}
/**
* Return a path to load given module of given theme from.
*
+ * The file at this path may not exist. This should be handled by the caller (throwing an error or
+ * falling back to default theme).
+ *
* @param string $theme OOUI theme name, for example 'WikimediaUI' or 'Apex'
* @param string $kind Kind of the module: 'scripts', 'styles', or 'images'
* @param string $module Module name, for valid values see $knownScriptsModules,
* $knownStylesModules, $knownImagesModules
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemePath( $theme, $kind, $module ) {
$paths = self::getThemePaths();
$path = $paths[$theme][$kind];
- $path = str_replace( '{module}', $module, $path );
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ $path = new ResourceLoaderFilePath(
+ str_replace( '{module}', $module, $path->getPath() ),
+ $path->getLocalBasePath(),
+ $path->getRemoteBasePath()
+ );
+ } else {
+ $path = str_replace( '{module}', $module, $path );
+ }
return $path;
}
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeScriptsPath( $theme, $module ) {
if ( !in_array( $module, self::$knownScriptsModules ) ) {
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeStylesPath( $theme, $module ) {
if ( !in_array( $module, self::$knownStylesModules ) ) {
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeImagesPath( $theme, $module ) {
if ( !in_array( $module, self::$knownImagesModules ) ) {
* @return QuickTemplate The template to be executed by outputPage
*/
protected function prepareQuickTemplate() {
- global $wgScript, $wgStylePath, $wgMimeType, $wgJsMimeType,
+ global $wgScript, $wgStylePath, $wgMimeType,
$wgSitename, $wgLogo, $wgMaxCredits,
$wgShowCreditsIfMax, $wgArticlePath,
$wgScriptPath, $wgServer;
}
$tpl->set( 'mimetype', $wgMimeType );
- $tpl->set( 'jsmimetype', $wgJsMimeType );
$tpl->set( 'charset', 'UTF-8' );
$tpl->set( 'wgScript', $wgScript );
$tpl->set( 'skinname', $this->skinname );
throw new PermissionsError( 'viewmyprivateinfo' );
}
- if ( $user->isBlockedFromEmailuser() ) {
- throw new UserBlockedError( $user->getBlock() );
- }
-
parent::checkExecutePermissions( $user );
}
@colorButtonTextHighlight: @colorGray4;
@colorButtonTextActive: @colorGray1;
@colorDisabledText: @colorGray7;
-@colorErrorText: #d33;
-@colorWarningText: #705000;
+
+// Messages
+// Messages: Error
+@backgroundColorError: #fee7e6;
+// Use only for inlined messages, boxed messages require `@colorTextEmphasized` for
+// minimum contrast ratio.
+@colorError: #d33;
+@borderColorError: @colorError;
+// Messages: Warning
+@backgroundColorWarning: #fef6e7;
+@colorWarning: @colorTextEmphasized;
+@borderColorWarning: #fc3;
+// Messages: Success
+@backgroundColorSuccess: #d5fdf4;
+@colorSuccess: #14866d;
+@borderColorSuccess: @colorSuccess;
+
+// FIXME: Remove after a few weeks, when extensions got updated
+@colorErrorText: @colorError;
+@colorWarningText: @colorWarning;
// UI colors
@backgroundColorInputBinaryChecked: @colorProgressive;
// Form input sizes, equal to OOUI at 14px base font-size
@sizeInputBinary: 1.5625em;
-
-// Messages
-@backgroundColorError: #fee7e6;
-@borderColorError: #d33;
-@backgroundColorWarning: #fef6e7;
-@borderColorWarning: #fc3;
/**
* @inheritdoc
+ * @param {string} subject Section title.
+ * @param {string} body Message body, as wikitext. Signature code will automatically be added unless the message already contains the string ~~~.
+ * @param {Object} [options] Message options:
+ * @param {string} [options.tags] [Change tags](https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tags) to add to the message's revision, pipe-separated.
*/
- WikitextMessagePoster.prototype.post = function ( subject, body ) {
- mw.messagePoster.WikitextMessagePoster.parent.prototype.post.call( this, subject, body );
+ WikitextMessagePoster.prototype.post = function ( subject, body, options ) {
+ var additionalParams;
+ options = options || {};
+ mw.messagePoster.WikitextMessagePoster.parent.prototype.post.call( this, subject, body, options );
// Add signature if needed
if ( body.indexOf( '~~~' ) === -1 ) {
body += '\n\n~~~~';
}
+ additionalParams = { redirect: true };
+ if ( options.tags !== undefined ) {
+ additionalParams.tags = options.tags;
+ }
return this.api.newSection(
this.title,
subject,
body,
- { redirect: true }
+ additionalParams
).then( function ( resp, jqXHR ) {
if ( resp.edit.result === 'Success' ) {
return $.Deferred().resolve( resp, jqXHR );
* @param {string} body Body, as wikitext. Signature code will automatically be added
* by MessagePosters that require one, unless the message already contains the string
* ~~~.
+ * @param {Object} [options] Message options. See MessagePoster implementations for details.
* @return {jQuery.Promise} Promise completing when the post succeeds or fails.
* For failure, will be rejected with three arguments:
*
$setup['wgNoFollowDomainExceptions'] = [ 'no-nofollow.org' ];
$setup['wgExternalLinkTarget'] = false;
$setup['wgLocaltimezone'] = 'UTC';
- $setup['wgHtml5'] = true;
$setup['wgDisableLangConversion'] = false;
$setup['wgDisableTitleConversion'] = false;
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>eye</title><path d="M10 7.5a2.5 2.5 0 1 0 2.5 2.5A2.5 2.5 0 0 0 10 7.5zm0 7a4.5 4.5 0 1 1 4.5-4.5 4.5 4.5 0 0 1-4.5 4.5zM10 3C3 3 0 10 0 10s3 7 10 7 10-7 10-7-3-7-10-7z"/></svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>flag</title><path d="M17 6L3 1v18h2v-6.87L17 6z"/></svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>flag</title><path d="M3 6l14-5v18h-2v-6.87L3 6z"/></svg>
\ No newline at end of file
--- /dev/null
+mw.test();
--- /dev/null
+body {
+ color: red;
+}
--- /dev/null
+body {
+ color: black;
+}
--- /dev/null
+<div></div>
* @covers WANObjectCache::getWarmupKeyMisses
* @covers WANObjectCache::prefixCacheKeys
* @covers WANObjectCache::getProcessCache
- * @covers WANObjectCache::getNonProcessCachedKeys
+ * @covers WANObjectCache::getNonProcessCachedMultiKeys
* @covers WANObjectCache::getRawKeysForWarmup
* @covers WANObjectCache::getInterimValue
* @covers WANObjectCache::setInterimValue
$cache->set( $key2, $value2, 10 );
$curTTLs = [];
- $this->assertEquals(
+ $this->assertSame(
[ $key1 => $value1, $key2 => $value2 ],
$cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
'Result array populated'
$mockWallClock += 1;
$curTTLs = [];
- $this->assertEquals(
+ $this->assertSame(
[ $key1 => $value1, $key2 => $value2 ],
$cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
"Result array populated even with new check keys"
'key2' => $check2,
'key3' => $check3,
] );
- $this->assertEquals(
+ $this->assertSame(
[ 'key1' => $value1, 'key2' => $value2 ],
$result,
'Initial values'
'key2' => $check2,
'key3' => $check3,
] );
- $this->assertEquals(
+ $this->assertSame(
[ 'key1' => $value1, 'key2' => $value2 ],
$result,
'key1 expired by check1, but value still provided'
$this->assertEquals( $class, $wanCache->determineKeyClassForStats( $key ) );
}
+
+ /**
+ * @covers WANObjectCache::makeMultiKeys
+ */
+ public function testMakeMultiKeys() {
+ $cache = $this->cache;
+
+ $ids = [ 1, 2, 3, 4, 4, 5, 6, 6, 7, 7 ];
+ $keyCallback = function ( $id, WANObjectCache $cache ) {
+ return $cache->makeKey( 'key', $id );
+ };
+ $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+ $expected = [
+ "local:key:1" => 1,
+ "local:key:2" => 2,
+ "local:key:3" => 3,
+ "local:key:4" => 4,
+ "local:key:5" => 5,
+ "local:key:6" => 6,
+ "local:key:7" => 7
+ ];
+ $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+
+ $ids = [ '1', '2', '3', '4', '4', '5', '6', '6', '7', '7' ];
+ $keyCallback = function ( $id, WANObjectCache $cache ) {
+ return $cache->makeGlobalKey( 'key', $id, 'a', $id, 'b' );
+ };
+ $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+ $expected = [
+ "global:key:1:a:1:b" => '1',
+ "global:key:2:a:2:b" => '2',
+ "global:key:3:a:3:b" => '3',
+ "global:key:4:a:4:b" => '4',
+ "global:key:5:a:5:b" => '5',
+ "global:key:6:a:6:b" => '6',
+ "global:key:7:a:7:b" => '7'
+ ];
+ $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+ }
+
+ /**
+ * @covers WANObjectCache::makeMultiKeys
+ */
+ public function testMakeMultiKeysIntString() {
+ $cache = $this->cache;
+ $ids = [ 1, 2, 3, 4, '4', 5, 6, 6, 7, '7' ];
+ $keyCallback = function ( $id, WANObjectCache $cache ) {
+ return $cache->makeGlobalKey( 'key', $id, 'a', $id, 'b' );
+ };
+
+ $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+ $expected = [
+ "global:key:1:a:1:b" => 1,
+ "global:key:2:a:2:b" => 2,
+ "global:key:3:a:3:b" => 3,
+ "global:key:4:a:4:b" => 4,
+ "global:key:5:a:5:b" => 5,
+ "global:key:6:a:6:b" => 6,
+ "global:key:7:a:7:b" => 7
+ ];
+ $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+ }
+
+ /**
+ * @covers WANObjectCache::makeMultiKeys
+ * @expectedException UnexpectedValueException
+ */
+ public function testMakeMultiKeysCollision() {
+ $ids = [ 1, 2, 3, 4, '4', 5, 6, 6, 7 ];
+
+ $this->cache->makeMultiKeys(
+ $ids,
+ function ( $id ) {
+ return "keymod:" . $id % 3;
+ }
+ );
+ }
+
+ /**
+ * @covers WANObjectCache::multiRemap
+ */
+ public function testMultiRemap() {
+ $a = [ 'a', 'b', 'c' ];
+ $res = [ 'keyA' => 1, 'keyB' => 2, 'keyC' => 3 ];
+
+ $this->assertEquals(
+ [ 'a' => 1, 'b' => 2, 'c' => 3 ],
+ $this->cache->multiRemap( $a, $res )
+ );
+
+ $a = [ 'a', 'b', 'c', 'c', 'd' ];
+ $res = [ 'keyA' => 1, 'keyB' => 2, 'keyC' => 3, 'keyD' => 4 ];
+
+ $this->assertEquals(
+ [ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 ],
+ $this->cache->multiRemap( $a, $res )
+ );
+ }
+
+ /**
+ * @covers WANObjectCache::hash256
+ */
+ public function testHash256() {
+ $bag = new HashBagOStuff();
+ $cache = new WANObjectCache( [ 'cache' => $bag, 'epoch' => 5 ] );
+ $this->assertEquals(
+ 'f402bce76bfa1136adc705d8d5719911ce1fe61f0ad82ddf79a15f3c4de6ec4c',
+ $cache->hash256( 'x' )
+ );
+
+ $cache = new WANObjectCache( [ 'cache' => $bag, 'epoch' => 50 ] );
+ $this->assertEquals(
+ 'f79a126722f0a682c4c500509f1b61e836e56c4803f92edc89fc281da5caa54e',
+ $cache->hash256( 'x' )
+ );
+
+ $cache = new WANObjectCache( [ 'cache' => $bag, 'secret' => 'garden' ] );
+ $this->assertEquals(
+ '48cd57016ffe29981a1114c45e5daef327d30fc6206cb73edc3cb94b4d8fe093',
+ $cache->hash256( 'x' )
+ );
+
+ $cache = new WANObjectCache( [ 'cache' => $bag, 'secret' => 'garden', 'epoch' => 3 ] );
+ $this->assertEquals(
+ '48cd57016ffe29981a1114c45e5daef327d30fc6206cb73edc3cb94b4d8fe093',
+ $cache->hash256( 'x' )
+ );
+ }
}
class NearExpiringWANObjectCache extends WANObjectCache {
);
}
+ /**
+ * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
+ *
+ * This mimics modules modified by skins using 'ResourceModuleSkinStyles' and 'OOUIThemePaths'
+ * skin attributes.
+ *
+ * @covers ResourceLoaderFilePath::getLocalBasePath
+ * @covers ResourceLoaderFilePath::getRemoteBasePath
+ */
+ public function testResourceLoaderFilePath() {
+ $basePath = __DIR__ . '/../../data/blahblah';
+ $filePath = __DIR__ . '/../../data/rlfilepath';
+ $testModule = new ResourceLoaderFileModule( [
+ 'localBasePath' => $basePath,
+ 'remoteBasePath' => 'blahblah',
+ 'styles' => new ResourceLoaderFilePath( 'style.css', $filePath, 'rlfilepath' ),
+ 'skinStyles' => [
+ 'vector' => new ResourceLoaderFilePath( 'skinStyle.css', $filePath, 'rlfilepath' ),
+ ],
+ 'scripts' => new ResourceLoaderFilePath( 'script.js', $filePath, 'rlfilepath' ),
+ 'templates' => new ResourceLoaderFilePath( 'template.html', $filePath, 'rlfilepath' ),
+ ] );
+ $expectedModule = new ResourceLoaderFileModule( [
+ 'localBasePath' => $filePath,
+ 'remoteBasePath' => 'rlfilepath',
+ 'styles' => 'style.css',
+ 'skinStyles' => [
+ 'vector' => 'skinStyle.css',
+ ],
+ 'scripts' => 'script.js',
+ 'templates' => 'template.html',
+ ] );
+
+ $context = $this->getResourceLoaderContext();
+ $this->assertEquals(
+ $expectedModule->getModuleContent( $context ),
+ $testModule->getModuleContent( $context ),
+ "Using ResourceLoaderFilePath works correctly"
+ );
+ }
+
public static function providerGetTemplates() {
$modules = self::getModules();
];
}
+ /**
+ * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
+ *
+ * This mimics modules modified by skins using 'ResourceModuleSkinStyles' and 'OOUIThemePaths'
+ * skin attributes.
+ *
+ * @covers ResourceLoaderFilePath::getLocalBasePath
+ * @covers ResourceLoaderFilePath::getRemoteBasePath
+ */
+ public function testResourceLoaderFilePath() {
+ $basePath = __DIR__ . '/../../data/blahblah';
+ $filePath = __DIR__ . '/../../data/rlfilepath';
+ $testModule = new ResourceLoaderImageModule( [
+ 'localBasePath' => $basePath,
+ 'remoteBasePath' => 'blahblah',
+ 'prefix' => 'foo',
+ 'images' => [
+ 'eye' => new ResourceLoaderFilePath( 'eye.svg', $filePath, 'rlfilepath' ),
+ 'flag' => [
+ 'file' => [
+ 'ltr' => new ResourceLoaderFilePath( 'flag-ltr.svg', $filePath, 'rlfilepath' ),
+ 'rtl' => new ResourceLoaderFilePath( 'flag-rtl.svg', $filePath, 'rlfilepath' ),
+ ],
+ ],
+ ],
+ ] );
+ $expectedModule = new ResourceLoaderImageModule( [
+ 'localBasePath' => $filePath,
+ 'remoteBasePath' => 'rlfilepath',
+ 'prefix' => 'foo',
+ 'images' => [
+ 'eye' => 'eye.svg',
+ 'flag' => [
+ 'file' => [
+ 'ltr' => 'flag-ltr.svg',
+ 'rtl' => 'flag-rtl.svg',
+ ],
+ ],
+ ],
+ ] );
+
+ $context = $this->getResourceLoaderContext();
+ $this->assertEquals(
+ $expectedModule->getModuleContent( $context ),
+ $testModule->getModuleContent( $context ),
+ "Using ResourceLoaderFilePath works correctly"
+ );
+ }
+
/**
* @dataProvider providerGetModules
* @covers ResourceLoaderImageModule::getStyles
}
/**
- * Verify that nothing depends on "startup".
+ * Verify that all modules specified as dependencies of other modules actually
+ * exist and are not illegal.
*
- * Depending on it is unsupported as it cannot be loaded by the client.
- *
- * @todo Modules can dynamically choose dependencies based on context. This method does not
- * test such dependencies. The same goes for testMissingDependencies() and
- * testUnsatisfiableDependencies().
+ * @todo Modules can dynamically choose dependencies based on context. This method
+ * does not find all such variations. The same applies to testUnsatisfiableDependencies().
*/
- public function testIllegalDependencies() {
+ public function testValidDependencies() {
$data = self::getAllModules();
-
- $illegalDeps = [];
- foreach ( $data['modules'] as $moduleName => $module ) {
- if ( $module instanceof ResourceLoaderStartUpModule ) {
- $illegalDeps[] = $moduleName;
- }
- }
-
- /** @var ResourceLoaderModule $module */
- foreach ( $data['modules'] as $moduleName => $module ) {
- foreach ( $illegalDeps as $illegalDep ) {
- $this->assertNotContains(
- $illegalDep,
- $module->getDependencies( $data['context'] ),
- "Module '$moduleName' must not depend on '$illegalDep'"
- );
- }
- }
- }
-
- /**
- * Verify that all modules specified as dependencies of other modules actually exist.
- */
- public function testMissingDependencies() {
- $data = self::getAllModules();
- $validDeps = array_keys( $data['modules'] );
+ $knownDeps = array_keys( $data['modules'] );
+ $illegalDeps = [ 'startup' ];
+
+ // Avoid an assert for each module to keep the test fast.
+ // Instead, perform a single assertion against everything at once.
+ // When all is good, actual/expected are both empty arrays.
+ // When we find issues, add the violations to 'actual' and add an empty
+ // key to 'expected'. These keys in expected are because the PHPUnit diff
+ // (as of 6.5) only goes one level deep.
+ $actualUnknown = [];
+ $expectedUnknown = [];
+ $actualIllegal = [];
+ $expectedIllegal = [];
/** @var ResourceLoaderModule $module */
foreach ( $data['modules'] as $moduleName => $module ) {
foreach ( $module->getDependencies( $data['context'] ) as $dep ) {
- $this->assertContains(
- $dep,
- $validDeps,
- "The module '$dep' required by '$moduleName' must exist"
- );
+ if ( !in_array( $dep, $knownDeps, true ) ) {
+ $actualUnknown[$moduleName][] = $dep;
+ $expectedUnknown[$moduleName] = [];
+ }
+ if ( in_array( $dep, $illegalDeps, true ) ) {
+ $actualIllegal[$moduleName][] = $dep;
+ $expectedIllegal[$moduleName] = [];
+ }
}
}
+ $this->assertEquals( $expectedUnknown, $actualUnknown, 'Dependencies that do not exist' );
+ $this->assertEquals( $expectedIllegal, $actualIllegal, 'Dependencies that are not legal' );
}
/**