Merge "resourceloader: Add unit tests for ResourceLoaderFilePath class methods"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 15 Jul 2019 21:58:57 +0000 (21:58 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 15 Jul 2019 21:58:57 +0000 (21:58 +0000)
38 files changed:
RELEASE-NOTES-1.34
docs/extension.schema.v1.json
docs/extension.schema.v2.json
includes/DefaultSettings.php
includes/Defines.php
includes/Setup.php
includes/libs/objectcache/README.md
includes/libs/objectcache/WANObjectCache.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/objectcache/ObjectCache.php
includes/registration/ExtensionProcessor.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderFilePath.php
includes/resourceloader/ResourceLoaderImage.php
includes/resourceloader/ResourceLoaderImageModule.php
includes/resourceloader/ResourceLoaderOOUIImageModule.php
includes/resourceloader/ResourceLoaderOOUIModule.php
includes/skins/SkinTemplate.php
includes/specials/SpecialChangeEmail.php
resources/src/mediawiki.less/mediawiki.ui/variables.less
resources/src/mediawiki.messagePoster.wikitext/WikitextMessagePoster.js
resources/src/mediawiki.messagePoster/MessagePoster.js
tests/parser/ParserTestRunner.php
tests/phpunit/data/rlfilepath/eye.svg [new file with mode: 0644]
tests/phpunit/data/rlfilepath/flag-ltr.svg [new file with mode: 0644]
tests/phpunit/data/rlfilepath/flag-rtl.svg [new file with mode: 0644]
tests/phpunit/data/rlfilepath/script.js [new file with mode: 0644]
tests/phpunit/data/rlfilepath/skinStyle.css [new file with mode: 0644]
tests/phpunit/data/rlfilepath/style.css [new file with mode: 0644]
tests/phpunit/data/rlfilepath/template.html [new file with mode: 0644]
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php

index ac10762..ea46a7d 100644 (file)
@@ -222,6 +222,8 @@ because of Phabricator reports.
   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(),
@@ -277,6 +279,10 @@ because of Phabricator reports.
   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 ===
index 86fa1b3..9ce016f 100644 (file)
                "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"
index c1db2b6..9d874f4 100644 (file)
                        "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"
index 5bf6163..3bfc8f8 100644 (file)
@@ -3277,33 +3277,6 @@ $wgOverrideUcfirstCharacters = [];
  */
 $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 &lt;html&gt; tag, if any.
  *
index 648e493..d818226 100644 (file)
@@ -73,22 +73,6 @@ define( 'NS_HELP', 12 );
 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 );
 /**@}*/
 
 /**@{
index d6f390a..45a2456 100644 (file)
@@ -585,12 +585,6 @@ if ( $wgUseFileCache || $wgUseCdn ) {
        $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 ) );
 
index 833c045..42bf636 100644 (file)
@@ -35,6 +35,14 @@ in sending the value to the backend server.
 * 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()`.
index 45caa78..660d850 100644 (file)
@@ -134,6 +134,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        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;
@@ -256,6 +258,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *       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'];
@@ -263,6 +266,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                $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();
@@ -331,7 +335,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *
         * @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
         */
@@ -366,14 +370,17 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * 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,
@@ -468,10 +475,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
        /**
         * @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 = [];
@@ -814,7 +821,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @see WANObjectCache::getCheckKeyTime()
         * @see WANObjectCache::getWithSetCallback()
         *
-        * @param array $keys
+        * @param string[] $keys
         * @return float[] Map of (key => UNIX timestamp)
         * @since 1.31
         */
@@ -1423,6 +1430,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                ) {
                        // 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 );
@@ -1593,7 +1601,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *         // 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 );
         *             }
         *         ),
@@ -1632,17 +1640,15 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @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;
@@ -1685,7 +1691,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *         // 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 );
         *             }
         *         ),
@@ -1725,21 +1731,19 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @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
@@ -1748,10 +1752,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                // 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;
                        }
                }
 
@@ -1783,7 +1787,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
                // 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 );
                }
 
@@ -1874,18 +1878,133 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        }
 
        /**
-        * @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 );
        }
 
        /**
@@ -2306,9 +2425,9 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        }
 
        /**
-        * @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 = [];
@@ -2394,31 +2513,31 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        }
 
        /**
-        * @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 ) {
index 2cc2c90..2c9858a 100644 (file)
@@ -130,7 +130,7 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function setLBInfo( $name, $value = null ) {
+       public function setLBInfo( $nameOrArray, $value = null ) {
                // Disallow things that might confuse the LoadBalancer tracking
                throw new DBUnexpectedError( $this, "Changing LB info is disallowed to enable reuse." );
        }
index 3024b00..60062fb 100644 (file)
@@ -586,11 +586,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return null;
        }
 
-       public function setLBInfo( $name, $value = null ) {
-               if ( is_null( $value ) ) {
-                       $this->lbInfo = $name;
+       public function setLBInfo( $nameOrArray, $value = null ) {
+               if ( is_array( $nameOrArray ) ) {
+                       $this->lbInfo = $nameOrArray;
+               } elseif ( is_string( $nameOrArray ) ) {
+                       if ( $value !== null ) {
+                               $this->lbInfo[$nameOrArray] = $value;
+                       } else {
+                               unset( $this->lbInfo[$nameOrArray] );
+                       }
                } else {
-                       $this->lbInfo[$name] = $value;
+                       throw new InvalidArgumentException( "Got non-string key" );
                }
        }
 
index 7b3dbb3..11dda2f 100644 (file)
@@ -289,7 +289,11 @@ class DatabaseSqlite extends Database {
                if ( self::$fulltextEnabled === null ) {
                        self::$fulltextEnabled = false;
                        $table = $this->tableName( 'searchindex' );
-                       $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
+                       $res = $this->query(
+                               "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'",
+                               __METHOD__,
+                               self::QUERY_IGNORE_DBO_TRX
+                       );
                        if ( $res ) {
                                $row = $res->fetchRow();
                                self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
@@ -335,7 +339,11 @@ class DatabaseSqlite extends Database {
                $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
                $encFile = $this->addQuotes( $file );
 
-               return $this->query( "ATTACH DATABASE $encFile AS $name", $fname );
+               return $this->query(
+                       "ATTACH DATABASE $encFile AS $name",
+                       $fname,
+                       self::QUERY_IGNORE_DBO_TRX
+               );
        }
 
        protected function isWriteQuery( $sql ) {
@@ -541,7 +549,10 @@ class DatabaseSqlite extends Database {
 
                $encTable = $this->addQuotes( $tableRaw );
                $res = $this->query(
-                       "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable" );
+                       "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
+                       __METHOD__,
+                       self::QUERY_IGNORE_DBO_TRX
+               );
 
                return $res->numRows() ? true : false;
        }
@@ -558,7 +569,7 @@ class DatabaseSqlite extends Database {
         */
        function indexInfo( $table, $index, $fname = __METHOD__ ) {
                $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
-               $res = $this->query( $sql, $fname );
+               $res = $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
                if ( !$res || $res->numRows() == 0 ) {
                        return false;
                }
@@ -804,7 +815,7 @@ class DatabaseSqlite extends Database {
        function fieldInfo( $table, $field ) {
                $tableName = $this->tableName( $table );
                $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
-               $res = $this->query( $sql, __METHOD__ );
+               $res = $this->query( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
                foreach ( $res as $row ) {
                        if ( $row->name == $field ) {
                                return new SQLiteField( $row, $tableName );
@@ -1112,7 +1123,7 @@ class DatabaseSqlite extends Database {
                }
                $sql = "DROP TABLE " . $this->tableName( $tableName );
 
-               return $this->query( $sql, $fName );
+               return $this->query( $sql, $fName, self::QUERY_IGNORE_DBO_TRX );
        }
 
        public function setTableAliases( array $aliases ) {
@@ -1129,7 +1140,11 @@ class DatabaseSqlite extends Database {
        public function resetSequenceForTable( $table, $fname = __METHOD__ ) {
                $encTable = $this->addIdentifierQuotes( 'sqlite_sequence' );
                $encName = $this->addQuotes( $this->tableName( $table, 'raw' ) );
-               $this->query( "DELETE FROM $encTable WHERE name = $encName", $fname );
+               $this->query(
+                       "DELETE FROM $encTable WHERE name = $encName",
+                       $fname,
+                       self::QUERY_IGNORE_DBO_TRX
+               );
        }
 
        public function databasesAreIndependent() {
index 3b9d1af..ef7f1e2 100644 (file)
@@ -223,14 +223,12 @@ interface IDatabase {
        public function getLBInfo( $name = null );
 
        /**
-        * Set the LB info array, or a member of it. If called with one parameter,
-        * the LB info array is set to that parameter. If it is called with two
-        * parameters, the member with the given name is set to the given value.
+        * Set the entire array or a particular key of the managing load balancer info array
         *
-        * @param array|string $name
-        * @param array|null $value
+        * @param array|string $nameOrArray The new array or the name of a key to set
+        * @param array|null $value If $nameOrArray is a string, the new key value (null to unset)
         */
-       public function setLBInfo( $name, $value = null );
+       public function setLBInfo( $nameOrArray, $value = null );
 
        /**
         * Set a lazy-connecting DB handle to the master DB (for replication status purposes)
@@ -1158,7 +1156,7 @@ interface IDatabase {
        /**
         * Change the current database
         *
-        * This should not be called outside LoadBalancer for connections managed by a LoadBalancer
+        * This should only be called by a load balancer or if the handle is not attached to one
         *
         * @param string $db
         * @return bool True unless an exception was thrown
@@ -1171,9 +1169,9 @@ interface IDatabase {
        /**
         * Set the current domain (database, schema, and table prefix)
         *
-        * This will throw an error for some database types if the database unspecified
+        * This will throw an error for some database types if the database is unspecified
         *
-        * This should not be called outside LoadBalancer for connections managed by a LoadBalancer
+        * This should only be called by a load balancer or if the handle is not attached to one
         *
         * @param string|DatabaseDomain $domain
         * @since 1.32
index cab0201..7e94f80 100644 (file)
@@ -1861,7 +1861,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                if ( $conn->getFlag( $conn::DBO_TRX ) ) {
-                       $conn->setLBInfo( 'trxRoundId', false );
+                       $conn->setLBInfo( 'trxRoundId', null ); // remove the round ID
                }
 
                if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
index 21948ef..ffbc378 100644 (file)
@@ -323,7 +323,7 @@ class ObjectCache {
         * @throws UnexpectedValueException
         */
        public static function newWANCacheFromParams( array $params ) {
-               global $wgCommandLineMode;
+               global $wgCommandLineMode, $wgSecretKey;
 
                $services = MediaWikiServices::getInstance();
                $params['cache'] = self::newFromParams( $params['store'] );
@@ -334,6 +334,7 @@ class ObjectCache {
                        // 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 );
index e71de84..6182d5f 100644 (file)
@@ -120,6 +120,7 @@ class ExtensionProcessor implements Processor {
                'ResourceFileModulePaths',
                'ResourceModules',
                'ResourceModuleSkinStyles',
+               'OOUIThemePaths',
                'QUnitTestModule',
                'ExtensionMessagesFiles',
                'MessagesDirs',
@@ -445,7 +446,7 @@ class ExtensionProcessor implements Processor {
                        }
                }
 
-               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'] ) ) {
@@ -459,7 +460,11 @@ class ExtensionProcessor implements Processor {
                                        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;
+                                       }
                                }
                        }
                }
index 017b399..fbc59fe 100644 (file)
@@ -256,11 +256,11 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                                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':
@@ -279,7 +279,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                                                                "'$key' given, string expected."
                                                        );
                                                }
-                                               $this->{$member}[$key] = (array)$value;
+                                               $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
                                        }
                                        break;
                                case 'deprecated':
@@ -315,7 +315,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                        // 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 );
@@ -643,6 +643,18 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                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
@@ -1060,7 +1072,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                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 ) ) {
index 3cf09d8..c01e507 100644 (file)
@@ -62,6 +62,20 @@ class ResourceLoaderFilePath {
                return "{$this->remoteBasePath}/{$this->path}";
        }
 
+       /**
+        * @return string
+        */
+       public function getLocalBasePath() {
+               return $this->localBasePath;
+       }
+
+       /**
+        * @return string
+        */
+       public function getRemoteBasePath() {
+               return $this->remoteBasePath;
+       }
+
        /**
         * @return string
         */
index 7829b71..9003951 100644 (file)
@@ -95,9 +95,9 @@ class ResourceLoaderImage {
 
                // 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 ) {
@@ -150,31 +150,43 @@ class ResourceLoaderImage {
         */
        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.
         *
index 90b18eb..902fa91 100644 (file)
@@ -130,7 +130,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
                $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 );
                }
@@ -259,7 +259,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
                                $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'] : [],
@@ -452,6 +452,18 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
                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.
         *
index 34079c3..c6d4cdf 100644 (file)
@@ -97,6 +97,9 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
                // 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 =
@@ -116,7 +119,7 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
         * @return array|false
         */
        protected function readJSONFile( $dataPath ) {
-               $localDataPath = $this->localBasePath . '/' . $dataPath;
+               $localDataPath = $this->getLocalPath( $dataPath );
 
                if ( !file_exists( $localDataPath ) ) {
                        return false;
@@ -127,7 +130,15 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
                // 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'] ) ) {
index 899fbbd..fdcc213 100644 (file)
@@ -82,7 +82,7 @@ trait ResourceLoaderOOUIModule {
         * 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.
         *
@@ -90,29 +90,57 @@ trait ResourceLoaderOOUIModule {
         */
        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 ) ) {
@@ -124,7 +152,7 @@ trait ResourceLoaderOOUIModule {
        /**
         * @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 ) ) {
@@ -136,7 +164,7 @@ trait ResourceLoaderOOUIModule {
        /**
         * @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 ) ) {
index 5d6197e..6ddced4 100644 (file)
@@ -256,7 +256,7 @@ class SkinTemplate extends Skin {
         * @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;
@@ -306,7 +306,6 @@ class SkinTemplate extends Skin {
                }
 
                $tpl->set( 'mimetype', $wgMimeType );
-               $tpl->set( 'jsmimetype', $wgJsMimeType );
                $tpl->set( 'charset', 'UTF-8' );
                $tpl->set( 'wgScript', $wgScript );
                $tpl->set( 'skinname', $this->skinname );
index 956ff77..c95aa1b 100644 (file)
@@ -78,10 +78,6 @@ class SpecialChangeEmail extends FormSpecialPage {
                        throw new PermissionsError( 'viewmyprivateinfo' );
                }
 
-               if ( $user->isBlockedFromEmailuser() ) {
-                       throw new UserBlockedError( $user->getBlock() );
-               }
-
                parent::checkExecutePermissions( $user );
        }
 
index e493e59..c0e53a7 100644 (file)
 @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;
index 20621d2..e87f8a7 100644 (file)
 
        /**
         * @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 );
index 7f217e9..14c971d 100644 (file)
@@ -22,6 +22,7 @@
         * @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:
         *
index f284b13..f29b0d7 100644 (file)
@@ -279,7 +279,6 @@ class ParserTestRunner {
                $setup['wgNoFollowDomainExceptions'] = [ 'no-nofollow.org' ];
                $setup['wgExternalLinkTarget'] = false;
                $setup['wgLocaltimezone'] = 'UTC';
-               $setup['wgHtml5'] = true;
                $setup['wgDisableLangConversion'] = false;
                $setup['wgDisableTitleConversion'] = false;
 
diff --git a/tests/phpunit/data/rlfilepath/eye.svg b/tests/phpunit/data/rlfilepath/eye.svg
new file mode 100644 (file)
index 0000000..be0c4e6
--- /dev/null
@@ -0,0 +1 @@
+<?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
diff --git a/tests/phpunit/data/rlfilepath/flag-ltr.svg b/tests/phpunit/data/rlfilepath/flag-ltr.svg
new file mode 100644 (file)
index 0000000..d19bed5
--- /dev/null
@@ -0,0 +1 @@
+<?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
diff --git a/tests/phpunit/data/rlfilepath/flag-rtl.svg b/tests/phpunit/data/rlfilepath/flag-rtl.svg
new file mode 100644 (file)
index 0000000..a58bb92
--- /dev/null
@@ -0,0 +1 @@
+<?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
diff --git a/tests/phpunit/data/rlfilepath/script.js b/tests/phpunit/data/rlfilepath/script.js
new file mode 100644 (file)
index 0000000..f5d7aa5
--- /dev/null
@@ -0,0 +1 @@
+mw.test();
diff --git a/tests/phpunit/data/rlfilepath/skinStyle.css b/tests/phpunit/data/rlfilepath/skinStyle.css
new file mode 100644 (file)
index 0000000..575d19f
--- /dev/null
@@ -0,0 +1,3 @@
+body {
+       color: red;
+}
diff --git a/tests/phpunit/data/rlfilepath/style.css b/tests/phpunit/data/rlfilepath/style.css
new file mode 100644 (file)
index 0000000..e87cc6a
--- /dev/null
@@ -0,0 +1,3 @@
+body {
+       color: black;
+}
diff --git a/tests/phpunit/data/rlfilepath/template.html b/tests/phpunit/data/rlfilepath/template.html
new file mode 100644 (file)
index 0000000..7c89b54
--- /dev/null
@@ -0,0 +1 @@
+<div></div>
index 593dd45..6d32201 100644 (file)
@@ -11,7 +11,7 @@ use Wikimedia\TestingAccessWrapper;
  * @covers WANObjectCache::getWarmupKeyMisses
  * @covers WANObjectCache::prefixCacheKeys
  * @covers WANObjectCache::getProcessCache
- * @covers WANObjectCache::getNonProcessCachedKeys
+ * @covers WANObjectCache::getNonProcessCachedMultiKeys
  * @covers WANObjectCache::getRawKeysForWarmup
  * @covers WANObjectCache::getInterimValue
  * @covers WANObjectCache::setInterimValue
@@ -1072,7 +1072,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $cache->set( $key2, $value2, 10 );
 
                $curTTLs = [];
-               $this->assertEquals(
+               $this->assertSame(
                        [ $key1 => $value1, $key2 => $value2 ],
                        $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
                        'Result array populated'
@@ -1088,7 +1088,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $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"
@@ -1149,7 +1149,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        'key2' => $check2,
                        'key3' => $check3,
                ] );
-               $this->assertEquals(
+               $this->assertSame(
                        [ 'key1' => $value1, 'key2' => $value2 ],
                        $result,
                        'Initial values'
@@ -1169,7 +1169,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        'key2' => $check2,
                        'key3' => $check3,
                ] );
-               $this->assertEquals(
+               $this->assertSame(
                        [ 'key1' => $value1, 'key2' => $value2 ],
                        $result,
                        'key1 expired by check1, but value still provided'
@@ -1839,6 +1839,137 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
 
                $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 {
index 8b24791..482ab4b 100644 (file)
@@ -704,4 +704,31 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
                $this->assertSame( $oldDomain, $this->db->getDomainId() );
        }
 
+       /**
+        * @covers Wikimedia\Rdbms\Database::getLBInfo
+        * @covers Wikimedia\Rdbms\Database::setLBInfo
+        */
+       public function testGetSetLBInfo() {
+               $db = $this->getMockDB();
+
+               $this->assertEquals( [], $db->getLBInfo() );
+               $this->assertNull( $db->getLBInfo( 'pringles' ) );
+
+               $db->setLBInfo( 'soda', 'water' );
+               $this->assertEquals( [ 'soda' => 'water' ], $db->getLBInfo() );
+               $this->assertNull( $db->getLBInfo( 'pringles' ) );
+               $this->assertEquals( 'water', $db->getLBInfo( 'soda' ) );
+
+               $db->setLBInfo( 'basketball', 'Lebron' );
+               $this->assertEquals( [ 'soda' => 'water', 'basketball' => 'Lebron' ], $db->getLBInfo() );
+               $this->assertEquals( 'water', $db->getLBInfo( 'soda' ) );
+               $this->assertEquals( 'Lebron', $db->getLBInfo( 'basketball' ) );
+
+               $db->setLBInfo( 'soda', null );
+               $this->assertEquals( [ 'basketball' => 'Lebron' ], $db->getLBInfo() );
+
+               $db->setLBInfo( [ 'King' => 'James' ] );
+               $this->assertNull( $db->getLBInfo( 'basketball' ) );
+               $this->assertEquals( [ 'King' => 'James' ], $db->getLBInfo() );
+       }
 }
index 5be0f9b..a9e7fcf 100644 (file)
@@ -265,6 +265,47 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                );
        }
 
+       /**
+        * 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();
 
index 3f5704d..dad9f1e 100644 (file)
@@ -144,6 +144,55 @@ class ResourceLoaderImageModuleTest extends ResourceLoaderTestCase {
                ];
        }
 
+       /**
+        * 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