Merge "Map WikiMap treat a schema of "dbo" similar to "mediawiki" to account for...
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 28 Mar 2019 21:17:09 +0000 (21:17 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 28 Mar 2019 21:17:09 +0000 (21:17 +0000)
23 files changed:
HISTORY
RELEASE-NOTES-1.33
autoload.php
includes/GlobalFunctions.php
includes/actions/HistoryAction.php
includes/api/ApiQueryRevisions.php
includes/cache/MessageBlobStore.php [deleted file]
includes/libs/rdbms/database/IDatabase.php
includes/resourceloader/MessageBlobStore.php [new file with mode: 0644]
includes/specials/SpecialContributions.php
includes/specials/SpecialRecentchangeslinked.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/findOrphanedFiles.php
maintenance/mediawiki.Title/generateJsToUpperCaseList.js [new file with mode: 0644]
maintenance/mediawiki.Title/generatePhpCharToUpperMappings.php [new file with mode: 0755]
resources/Resources.php
resources/lib/jquery.ui/jquery.ui.spinner.js [deleted file]
resources/lib/jquery.ui/themes/smoothness/jquery.ui.spinner.css [deleted file]
resources/src/mediawiki.Title/Title.js
resources/src/mediawiki.Title/phpCharToUpper.js [deleted file]
resources/src/mediawiki.Title/phpCharToUpper.json [new file with mode: 0644]
tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php

diff --git a/HISTORY b/HISTORY
index 7895316..921b881 100644 (file)
--- a/HISTORY
+++ b/HISTORY
@@ -19159,3 +19159,384 @@ going to run a public MediaWiki, so you can be notified of security fixes.
 === IRC help ===
 
 There's usually someone online in #mediawiki on irc.freenode.net
+
+=MediaWiki 1.3=
+
+== MediaWiki 1.3.18 ==
+(released 2005-11-02)
+MediaWiki 1.3.18 is a bugfix and security maintenance release. A change in PHP
+4.4.1 broke handling of extension and <nowiki><pre></nowiki> sections, causing
+garbage data to be inserted in output and saved edits. This version works
+around the change. This release includes further corrections to the inline CSS
+style sanitation which works around a JavaScript "feature" on Microsoft
+Internet Explorer. Users of Microsoft Internet Explorer for Windows may be
+vulnerable to XSS injections on prior 1.3 releases; users of
+standards-compliant browsers are not vulnerable.
+
+== MediaWiki 1.3.17 ==
+(released 2005-10-05)
+MediaWiki 1.3.17 is a security maintenance release. Unsafe handling of CSS by
+Microsoft Internet Explorer could be exploited to produce cross-site scripting
+attacks by JavaScript injection to clients running that browser. This release
+blacklists several additional variants from use in HTML inline style
+attributes. All publicly accessible wikis are recommended to upgrade to reduce
+the risk to visitors using Microsoft web browsers.Note: the MediaWiki 1.3.x
+series is not compatible with PHP 5.0.5 or higher. Upgrade to the 1.5.0 release
+if you require this version of PHP 5.
+
+== MediaWiki 1.3.16 ==
+(released 2005-09-21)
+MediaWiki 1.3.16 is a security maintenance release. A bug in edit submission
+handling could cause corruption of the previous revision in the database if an
+abnormal URL was used, such as those used by some spambots. Affected releases:
+* 1.4.x <= 1.4.9; fixed in 1.4.10
+* 1.3.x <= 1.3.15; fixed in 1.3.16
+1.5 release candidates are not affected by this problem. All publicly editable
+wikis are strongly recommended to upgrade immediately.
+1.3 releases can be manually patched by changing this bit in
+{{manual|EditPage.php}}:
+<syntaxhighlight lang="php">
+    if( $this->tokenOk( $request ) ) {
+        $this->save    = $request->wasPosted() && !$this->preview;
+    } else {
+</syntaxhighlight>
+to:
+<syntaxhighlight lang="php">
+    if( $this->tokenOk( $request ) ) {
+        $this->save    = $request->getVal( 'action' ) == 'submit' &&
+                         $request->wasPosted() && !$this->preview;
+    } else {
+</syntaxhighlight>
+
+== MediaWiki 1.3.15, 2005-08-29 ==
+MediaWiki 1.3.15 is a security maintenance release. It corrects across-site
+scripting security bug:
+* <nowiki><math></nowiki> tags were handled incorrectly when TeX rendering
+support is off, as in the default configuration. Wikis where the optional math
+support has been *enabled* are not vulnerable. The 1.3.x series is no longer
+maintained except for security fixes; new users and those seeking bug fixes
+should upgrade to 1.4.9 or 1.5.0.
+
+== MediaWiki 1.3.14, 2005-08-23 ==
+MediaWiki 1.3.14 is a security maintenance release. A flaw in the interaction
+between extensions and HTML attribute sanitization was discovered which could
+allow unauthorized use of offsite resources in style sheets, and possible
+exploitation of a JavaScript injection feature on Microsoft Internet Explorer.
+The 1.3.x series is no longer maintained except for security fixes; new users
+and those seeking bug fixes should upgrade to 1.4.8 or 1.5.0. Existing 1.3.x
+installations not willing to upgrade to the current stable release should apply
+the change manually:
+In includes/Parser.php, function {{code|inline=y|lang=php|fixTagAttributes()}}
+add:
+<syntaxhighlight lang="php">
+       # Any placeholder items should have been unstripped already before
+       # we got to this point. Raw text inserted later could be dangerous.
+       if( strpos( $t, UNIQ_PREFIX ) !== false ) {
+           wfDebug( "Parser::fixTagAttributes found stripped data placeholder;
+           dropping attributes\n" );
+           $t = '';
+       }
+</syntaxhighlight>
+If you are actively using extensions to generate HTML attribute values, upgrade
+to 1.4 or 1.5 for a more thorough fix.
+
+== MediaWiki 1.3.13, 2005-06-03 ==
+MediaWiki 1.3.13 is a security maintenance release. Incorrect handling of page
+template inclusions made it possible to inject JavaScript code into HTML
+attributes, which could lead to cross-site scripting attacks on a publicly
+editable wiki. Vulnerable releases and fix:
+* 1.5 prerelease: fixed in 1.5alpha2
+* 1.4 stable series: fixed in 1.4.5
+* 1.3 legacy series: fixed in 1.3.13
+* 1.2 series no longer supported; upgrade to 1.4.5 strongly recommended The
+1.3.x series is no longer maintained except for security fixes; new users and
+those seeking general bug fixes should install 1.4.5. Existing 1.3.x
+installations not willing or able to upgrade to the current stable relase
+should update the installation to 1.3.13; only includes/Parser.php has changed
+from 1.3.12.
+
+== MediaWiki 1.3.12, 2005-02-20 ==
+MediaWiki 1.3.12 is a security maintenance release. A cross-site scripting
+injection vulnerability was discovered, which affects only MSIE clients and is
+only open if MediaWiki has been manually configured to run output through HTML
+Tidy ($wgUseTidy). The 1.3.x series is no longer maintained except for security
+fixes; new users and those seeking bug fixes should upgrade to 1.4.2. Existing
+1.3.x installations using Tidy not willing to upgrade to the current stable
+relase should either turn off Tidy or update the installation to 1.3.12.
+
+== MediaWiki 1.3.11, 2005-02-20 ==
+MediaWiki 1.3.11 is a security release.
+A security audit found and fixed a number of problems. Users of MediaWiki
+1.3.10 and earlier should upgrade to 1.3.11; users of 1.4 beta releases should
+upgrade to 1.4rc1.
+
+=== Cross-site scripting vulnerability ===
+XSS injection points can be used to hijack session and authentication cookies
+as well as more serious attacks.
+* Media: links output raw text into an attribute value, potentially abusable
+for JavaScript injection. This has been corrected.
+* Additional checks added to file upload to protect against MSIE and Safari
+MIME-type autodetection bugs.
+As of <code>1.3.10/1.4beta6</code>, per-user customized CSS and JavaScript is
+disabled by default as a general precaution. Sites which want this ability may
+set {{wg|AllowUserCss}} and {{wg|AllowUserJs}} in LocalSettings.php.
+
+=== Cross-site request forgery ===
+An attacker could use JavaScript-submitted forms to perform various restricted
+actions by tricking an authenticated user into visiting a malicious web page. A
+fix for page editing in 1.3.10/1.4beta6 has been expanded in this release to
+other forms and functions. Authors of bot tools may need to update their code
+to include the additional fields.
+
+=== Directory traversal ===
+An unchecked parameter in image deletion could allow an authenticated
+administrator to delete arbitary files in directories writable by the web
+server, and confirm existence of files not deletable.
+
+== MediaWiki 1.3.10, 2005-02-03 ==
+MediaWiki 1.3.10 is a security release.
+An attacker could craft a URL which, when visited by a particular logged-in
+user, would execute arbitrary JavaScript code on the user's browser in the
+wiki's site context. This attack has been blocked, and as an extra precaution
+the user CSS and JavaScript subpage support is now disabled by default. Sites
+which want this ability may set {{wg|AllowUserCss}} and {{wg|AllowUserJs}} in
+{{manual|LocalSettings.php}}. Additional protections have been added against
+off-site form submissions
+hijacking user credentials. Authors of bot tools may need to update their code
+to include additional fields. All wikis running 1.3.x are strongly urged to
+upgrade to 1.3.10.
+Changes from 1.3.9:
+* Logged-in edits and preview of user CSS/JS are now locked to a session token.
+* Per-user CSS and JavaScript subpage customizations now disabled by default.
+They can be re-enabled via {{wg|AllowUserJs}} and {{wg|AllowUserCss}}.
+* Removed .ogg from the default uploads whitelist as an extra precaution. If
+your web server is configured to serve Ogg files with the correct Content-Type
+header, you can re-add it in LocalSettings.php: {{wg|FileExtensions}}<code>[] =
+'ogg'</code>
+
+== MediaWiki 1.3.9, 2004-12-12 ==
+MediaWiki 1.3.9 is a security and bug fix release.
+A flaw in upload handling has been found which may allow upload and  execution
+of arbitrary scripts with the permissions of the web server. Only wikis that
+have enabled uploads and have a vulnerable Apache  configuration will be
+affected, but to be safe all wikis should upgrade. Wikis with uploads available
+should either disable uploads or upgrade to 1.3.9 immediately; if other files
+are customized and require merging changes,
+includes/{{manual|SpecialUpload.php}} may be replaced individually to add the
+fix. (It is also recommended to configure your web server to disable script
+execution in the 'images' subdirectory where uploads are placed, which prevents
+most attacks even if the wiki fails.)
+Changes from 1.3.8:
+* Backported "Templates used in this page"-feature of EditPage
+* Allow "MySkin" as a default skin.
+* ({{bugzilla|938}}) Parse namespaces correctly on self-interwiki links
+* ({{bugzilla|1010}}) fix broken Commons image link on [[Skin:Classic|Classic]]
+& [[Skin:Cologne Blue|Cologne Blue]]
+* ({{bugzilla|1004}}) Norsk language names for interwiki links changed, Nauruan
+language name changed
+* Enhance upload extension blacklist to protect against vulnerable Apache
+configurations
+
+== MediaWiki 1.3.8, 2004-11-15 ==
+MediaWiki 1.3.8 is a bugfix release. Those running wikis with uploads enabled
+are strongly recommended to upgrade as this fixes several problems with
+overwriting previously-uploaded files.
+Changes from 1.3.7:
+* ({{bugzilla|506}}) fix {{code|inline=y|lang=html|array_key_exists()}} warning
+for IIS servers using ISAPI mode
+* ({{bugzilla|718}}) fix bad charset in (file) cached pages
+* use local numerals in category page (for Hindi et al)
+* alias month abbreviations to month names in Hindi
+* add localized numerals for Gujarati and Kannada
+* fix Category and project namespaces for Hindi
+* Don't output bogus timestamp on [[Special:RecentChanges]] if no entries
+* Correct template include path which broke some but not all Windows installs
+* Fix edit form submission problem with some PHP versions
+* Disallow unreachable titles with %XX hex codes
+* Allow page [[0]] to be renamed
+* ({{bugzilla|774}}) when saving with <code>section=new</code>, return to the
+anchor as with existing numbered section edits
+* Experimental shared upload overlay area (disabled by default)
+* ({{bugzilla|806}}) Removed some "Wikipedia" hardcoding in German localization
+* User option localization fix for some extensions
+* ({{bugzilla|809}}) now try to load the mysql php extension if it isn't loaded
+* ({{bugzilla|848}}) fix error message in [[Special:Newpages]] RSS and Atom
+feeds
+* ({{bugzilla|26}}) fix cache headers on anon talk page notification
+* ({{bugzilla|874}}) added 'cgi' to {{wg|FileBlacklist}}
+* ({{bugzilla|862}}) localize date and time format for Finnish
+* ({{bugzilla|548}}) Don't overwrite images until the user confirms it
+
+== MediaWiki 1.3.7, 2004-10-18 ==
+Changes from 1.3.6:
+* Fix protected-page related security issue.
+
+== MediaWiki 1.3.6, 2004-10-14 ==
+Changes from 1.3.5:
+* ({{bugzilla|296}}) Variables in user interface messages are no longer
+substituted at install time, so changes to the site name etc should be easier
+to make
+* ({{bugzilla|149}}) [[Special:RecentChanges]] "changes from" link preserves
+limit
+* ({{bugzilla|433}}) tooltip for "Undelete" tab now labeled correctly
+* ({{bugzilla|439}}) unclickable "Move" tab no longer displays on protected
+pages
+* ({{bugzilla|484}}) graceful deletion of images where the actual file is
+missing
+* ({{bugzilla|686}}) fixed [[plural]]s in Catalan localization
+* Fixed potential HTML/JavaScript injection attack in the
+[[Extension:UnicodeConverter|UnicodeConverter]] extension. (This extension is
+not enabled by default.)
+* Fixed potential HTML/JavaScript injection attack via raw page views to a
+maliciously crafted wiki page.
+* ({{bugzilla|187}}, {{bugzilla|669}}) Fixed centered thumbnails, using
+{{code|inline=y|lang=html|<div>}} instead of {{code|inline=y|lang=html|<span>}}.
+* catch MySQL error 2000 during installation.
+* ({{bugzilla|704}}) Removed misleading LocalSettings.sample
+* Fix cross site scripting bugs in [[Special:Ipblocklist]],
+[[Special:EmailUser]]
+* Fix SQL injection and cross site scripting bugs in Special:Maintenance
+* Fix cross site scripting bugs and possible filename validation vulnerability
+in ImagePage.
+* and more of that sort
+
+== MediaWiki 1.3.5, 2004-09-30 ==
+Changes from 1.3.4:
+* Clean up input validation in 'raw' page output mode which was a potential
+cross-site scripting opportunity.
+
+== MediaWiki 1.3.4, 2004-09-28 ==
+=== SECURITY NOTE ===
+As of 1.3.4, MediaWiki performs some screening of newly uploaded files for
+validity. (Some)  corrupt image files, and HTML files mistakenly or maliciously
+masquerading as images, should now be rejected. These checks protect against
+Internet Explorer security holes relating to type autodetection which are a
+potential cross-site scripting attack vector, and also rejects at least one
+known version of the "JPEG virus" which might attack unpatched clients. If you
+already have invalid files uploaded this will not protect against them. If you
+have expanded the <code>filetype</code> whitelist or disabled the strict type
+checking, other dangerous file types may still get through. You should always
+be careful when allowing uploads!
+Changes from 1.3.3:
+* Fixed lots of template-related bugs, esp. for cases where template variables
+are used for links, images, etc.
+* Fixed transformation of page messages when viewing [[Special:Allmessages]]
+* Handle "ISBN ISBN 1234" correctly
+* Fixed warning on Category pages
+* Fixed some bad error messages on login page
+* Fixed history entry for initial main page on install
+* Removed problematic <code>{</code> and <code>}</code> from legal title
+characters
+* Strip leading blank from output in preformatted text.
+* Fixed problem when moving pages to titles with '#' in
+* Optional {{wg|RawHtml}} for raw {{code|inline=y|lang=html|<html>}} sections.
+Use only on limited- participation 'trusted' wikis, as it does not protect
+against cross-site scripting attacks. For security, this option can only be
+enabled if in {{wg|WhitelistEdit}} mode.
+* Fixed problem where pages which were created as a redirect following a move
+never showed on [[Special:Randompage]].
+* Fixed line spacing on printed table of contents
+* Allow links to pages with names of the form [[RFC 1234]]
+* Fixed broken edit links being shown for sections from included templates
+* Verify that uploaded image files are of the claimed type.
+
+== MediaWiki 1.3.3, 2004-09-09 ==
+Changes from 1.3.2:
+* Fix for long numeric page titles
+* Fix Go search for "0", numeric almost-self-links
+* Avoid caching of pages with "You have new messages" headers
+* Fix for upgrades as non-root users from 1.2 command-line installs.
+* Fix for {{wg|DebugDumpSql}} debug mode.
+* {{wg|ExtraNamespaces}} setting for configuring additional namespaces (see
+note in {{manual|DefaultSettings.php}})
+* 'recache' on query pages now disabled when miser mode is on; special case the
+global settings in your {{manual|LocalSettings.php}} to do automatic updates.
+* Don't block UTF-8 titles containing byte 0xA0 (bug added in 1.3.2)
+* Watch/unwatch tabs now shown on edit pages in MonoBook.
+* Fix default skin in Irish localization (ga)
+* Add Traditional Chinese localization (zh-tw)
+* Changed default sortkey of subcategories. Don't include "Category:"-prefix
+any longer
+* More helpful info on spam catcher.
+* Allow larger offsets for queries such as [[Special:Listusers]]
+* Semicolon (;) added to French non-break space rules
+* Possible fix for some install errors with path names permission problems.
+* Removed [[Project:All system messages]], which has been superseded by the
+much faster [[Special:Allmessages]]. This speeds up installation considerably.
+
+== MediaWiki 1.3.2, 2004-08-30 ==
+Changes from 1.3.1:
+* Fix namespaced page creation links when no go match
+* When cookies are disabled, don't show login screen twice
+* Install should no longer die when PHP is pre-configured to compress output
+* Fixed bug that caused long Japanese pages to time out with Tidy active
+* When session.handler is set incorrectly, try automatic override to 'files'
+* Watch/Unwatch links back to the affected page instead of Main Page
+* Upload link no longer displayed on Monobook if uploading is disabled
+* Special:Allmessages faster, shows correct original text, works in safe mode
+
+== MediaWiki 1.3.1, 2004-08-14 ==
+Changes from 1.3.0:
+* Watchlist parameters now work with register_globals off
+* Fixed parsing of ''italics'' and '''bold''' mark-up (again)
+* Special:Allpages display is more sensible on smaller wikis
+* Fixed XHTML parsing error in classic skins
+* Moved pages update watchlist correctly
+* Fixed rebuildall.php on case-sensitive Unix filesystems
+* Disabled file cache compression by default due to incompatibility with output
+buffer compression (ob_gzhandler)
+* New magic word {{code|inline=y|PAGENAMEE}} (URL-escaped version of
+{{code|inline=y|PAGENAME}})
+* Installation avoids blank username; better message on missing XML module
+* {{wg|WhitelistAccount}} no longer breaks all logins.
+
+== MediaWiki 1.3.0, 2004-08-11 ==
+Look & layout:
+* New default layout '[[Skin:MonoBook|MonoBook]]' (available on PHP4 only
+currently)
+* Print stylesheet now built-in to every page
+* More or less correct XHTML 1.0 (served as text/html by default)
+Wiki features:
+* Image captions can now include links and other basic formatting
+* Image bounding box can be specified instead of width, e.g. as 100x100px,
+making the image not wider than 100px and not higher than 100px, keeping aspect
+ratio.
+* Templates have been expanded with parameters, and separated from the
+MediaWiki: localization scheme.
+* Categories more or less work
+* added a special page for listing users with sysop rights.
+Editing:
+* Automatic merging of edit conflicts that don't directly interfere
+* Edit summaries can now include basic formatting and links
+Metadata and output:
+* Linked Creative Commons copyright metadata (optional)
+* RSS 2.0 & Atom 0.3 feeds for Recent Changes, New Pages
+Optional modules:
+* WikiHiero hieroglyphic module can be added (separate download)
+* Timeline module can be added (separate download). Requires ploticus.
+* TeX now has an experimental MathML output mode (incomplete!)
+Installation and upgrading:
+* The old install.php and update.php have been removed. In-place installation
+introduced in 1.2 is now the standard installation and upgrade method, see
+INSTALL and UPGRADE for directions.
+Database:
+* The links table has been changed to use a cur_id for l_from. The link tables
+must be converted on upgrade, which may entail some downtime.
+Code and compatibility:
+* Should now run clean with error reporting set to E_ALL.
+* register_globals hack from 1.2 has been replaced with safer code
+* Bundled PHPTAL 0.7.0 from http://phptal.sourceforge.net/ (with some patches)
+* Most image-related code moved to Image.php
+* More fixes for PHP 4.1.2 (thanks to Asheesh Laroia)
+* URL encoding fix for anchors
+* All languages now available in UTF-8 mode
+* Various other fixes
+
+=== Caveats ===
+Some output, particularly involving user-supplied inline HTML, may not produce
+100% valid or well-formed XHTML output. Testers are welcome to set $wgMimeType
+= "application/xhtml+xml"; to test for remaining problem cases, but this is not
+recommended on live sites. (This must be set for MathML to display properly in
+Mozilla.) The new 'MonoBook' skin is not compatible with PHP 5 due to bugs in
+the underlying PHPTAL library. It will be automatically disabled when running
+on PHP5; the older look and feel will be used instead.
index 83b3cc3..ddd6da9 100644 (file)
@@ -134,7 +134,7 @@ For notes on 1.32.x and older releases, see HISTORY.
 * Updated wikimedia/php-session-serializer from 1.0.6 to 1.0.7.
 
 ==== Removed external libraries ====
-* 
+* (T219403) jquery.ui.spinner, deprecated since 1.31, was removed.
 
 === Bug fixes in 1.33 ===
 * (T164211) Special:UserRights could sometimes fail with a
@@ -344,6 +344,7 @@ because of Phabricator reports.
 * The translation of main page in Sardinian (sc) was changed from "Pàgina Base"
   to "Pàgina printzipale". Existing wikis using this content language need to
   move the main page or change the name through MediaWiki:Mainpage page.
+* wfSplitWikiID(), deprecated in 1.32, has been removed.
 
 === Deprecations in 1.33 ===
 * The configuration option $wgUseESI has been deprecated, and is expected
index bb0457b..e9495b3 100644 (file)
@@ -974,7 +974,7 @@ $wgAutoloadLocalClasses = [
        'MergeMessageFileList' => __DIR__ . '/maintenance/mergeMessageFileList.php',
        'MergeableUpdate' => __DIR__ . '/includes/deferred/MergeableUpdate.php',
        'Message' => __DIR__ . '/includes/Message.php',
-       'MessageBlobStore' => __DIR__ . '/includes/cache/MessageBlobStore.php',
+       'MessageBlobStore' => __DIR__ . '/includes/resourceloader/MessageBlobStore.php',
        'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php',
        'MessageCacheUpdate' => __DIR__ . '/includes/deferred/MessageCacheUpdate.php',
        'MessageContent' => __DIR__ . '/includes/content/MessageContent.php',
index 55b78ac..20f3fd0 100644 (file)
@@ -2610,22 +2610,6 @@ function wfWikiID() {
        }
 }
 
-/**
- * Split a wiki ID into DB name and table prefix
- *
- * @param string $wiki
- *
- * @return array
- * @deprecated 1.32
- */
-function wfSplitWikiID( $wiki ) {
-       $bits = explode( '-', $wiki, 2 );
-       if ( count( $bits ) < 2 ) {
-               $bits[] = '';
-       }
-       return $bits;
-}
-
 /**
  * Get a Database object.
  *
index fbf43e0..cc11233 100644 (file)
@@ -78,7 +78,7 @@ class HistoryAction extends FormlessAction {
                                        ->rawParams( $this->getLanguage()->pipeList( $links ) )
                                        ->escaped();
                }
-               return $subtitle;
+               return Html::rawElement( 'div', [ 'class' => 'mw-history-subtitle' ], $subtitle );
        }
 
        /**
index 3781ab0..7e46c1a 100644 (file)
@@ -268,7 +268,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
                                                [ 'ar_rev_id' => $revids ],
                                                __METHOD__
                                        ),
-                               ], false );
+                               ], $db::UNION_DISTINCT );
                                $res = $db->query( $sql, __METHOD__ );
                                foreach ( $res as $row ) {
                                        if ( (int)$row->id === (int)$params['startid'] ) {
diff --git a/includes/cache/MessageBlobStore.php b/includes/cache/MessageBlobStore.php
deleted file mode 100644 (file)
index ceb51f2..0000000
+++ /dev/null
@@ -1,240 +0,0 @@
-<?php
-/**
- * Message blobs storage used by ResourceLoader.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Roan Kattouw
- * @author Trevor Parscal
- * @author Timo Tijhof
- */
-
-use MediaWiki\MediaWikiServices;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use Wikimedia\Rdbms\Database;
-
-/**
- * This class generates message blobs for use by ResourceLoader modules.
- *
- * A message blob is a JSON object containing the interface messages for a certain module in
- * a certain language.
- */
-class MessageBlobStore implements LoggerAwareInterface {
-
-       /* @var ResourceLoader */
-       private $resourceloader;
-
-       /**
-        * @var LoggerInterface
-        */
-       protected $logger;
-
-       /**
-        * @var WANObjectCache
-        */
-       protected $wanCache;
-
-       /**
-        * @param ResourceLoader $rl
-        * @param LoggerInterface|null $logger
-        */
-       public function __construct( ResourceLoader $rl, LoggerInterface $logger = null ) {
-               $this->resourceloader = $rl;
-               $this->logger = $logger ?: new NullLogger();
-               $this->wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-       }
-
-       /**
-        * @since 1.27
-        * @param LoggerInterface $logger
-        */
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-
-       /**
-        * Get the message blob for a module
-        *
-        * @since 1.27
-        * @param ResourceLoaderModule $module
-        * @param string $lang Language code
-        * @return string JSON
-        */
-       public function getBlob( ResourceLoaderModule $module, $lang ) {
-               $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang );
-               return $blobs[$module->getName()];
-       }
-
-       /**
-        * Get the message blobs for a set of modules
-        *
-        * @since 1.27
-        * @param ResourceLoaderModule[] $modules Array of module objects keyed by name
-        * @param string $lang Language code
-        * @return array An array mapping module names to message blobs
-        */
-       public function getBlobs( array $modules, $lang ) {
-               // Each cache key for a message blob by module name and language code also has a generic
-               // check key without language code. This is used to invalidate any and all language subkeys
-               // that exist for a module from the updateMessage() method.
-               $cache = $this->wanCache;
-               $checkKeys = [
-                       // Global check key, see clear()
-                       $cache->makeKey( __CLASS__ )
-               ];
-               $cacheKeys = [];
-               foreach ( $modules as $name => $module ) {
-                       $cacheKey = $this->makeCacheKey( $module, $lang );
-                       $cacheKeys[$name] = $cacheKey;
-                       // Per-module check key, see updateMessage()
-                       $checkKeys[$cacheKey][] = $cache->makeKey( __CLASS__, $name );
-               }
-               $curTTLs = [];
-               $result = $cache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys );
-
-               $blobs = [];
-               foreach ( $modules as $name => $module ) {
-                       $key = $cacheKeys[$name];
-                       if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
-                               $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
-                       } else {
-                               // Use unexpired cache
-                               $blobs[$name] = $result[$key];
-                       }
-               }
-               return $blobs;
-       }
-
-       /**
-        * @deprecated since 1.27 Use getBlobs() instead
-        * @return array
-        */
-       public function get( ResourceLoader $resourceLoader, $modules, $lang ) {
-               return $this->getBlobs( $modules, $lang );
-       }
-
-       /**
-        * @since 1.27
-        * @param ResourceLoaderModule $module
-        * @param string $lang
-        * @return string Cache key
-        */
-       private function makeCacheKey( ResourceLoaderModule $module, $lang ) {
-               $messages = array_values( array_unique( $module->getMessages() ) );
-               sort( $messages );
-               return $this->wanCache->makeKey( __CLASS__, $module->getName(), $lang,
-                       md5( json_encode( $messages ) )
-               );
-       }
-
-       /**
-        * @since 1.27
-        * @param string $cacheKey
-        * @param ResourceLoaderModule $module
-        * @param string $lang
-        * @return string JSON blob
-        */
-       protected function recacheMessageBlob( $cacheKey, ResourceLoaderModule $module, $lang ) {
-               $blob = $this->generateMessageBlob( $module, $lang );
-               $cache = $this->wanCache;
-               $cache->set( $cacheKey, $blob,
-                       // Add part of a day to TTL to avoid all modules expiring at once
-                       $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ),
-                       Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) )
-               );
-               return $blob;
-       }
-
-       /**
-        * Invalidate cache keys for modules using this message key.
-        * Called by MessageCache when a message has changed.
-        *
-        * @param string $key Message key
-        */
-       public function updateMessage( $key ) {
-               $moduleNames = $this->getResourceLoader()->getModulesByMessage( $key );
-               foreach ( $moduleNames as $moduleName ) {
-                       // Uses a holdoff to account for database replica DB lag (for MessageCache)
-                       $this->wanCache->touchCheckKey( $this->wanCache->makeKey( __CLASS__, $moduleName ) );
-               }
-       }
-
-       /**
-        * Invalidate cache keys for all known modules.
-        * Called by LocalisationCache after cache is regenerated.
-        */
-       public function clear() {
-               $cache = $this->wanCache;
-               // Disable holdoff because this invalidates all modules and also not needed since
-               // LocalisationCache is stored outside the database and doesn't have lag.
-               $cache->touchCheckKey( $cache->makeKey( __CLASS__ ), $cache::HOLDOFF_NONE );
-       }
-
-       /**
-        * @since 1.27
-        * @return ResourceLoader
-        */
-       protected function getResourceLoader() {
-               return $this->resourceloader;
-       }
-
-       /**
-        * @since 1.27
-        * @param string $key Message key
-        * @param string $lang Language code
-        * @return string
-        */
-       protected function fetchMessage( $key, $lang ) {
-               $message = wfMessage( $key )->inLanguage( $lang );
-               $value = $message->plain();
-               if ( !$message->exists() ) {
-                       $this->logger->warning( 'Failed to find {messageKey} ({lang})', [
-                               'messageKey' => $key,
-                               'lang' => $lang,
-                       ] );
-               }
-               return $value;
-       }
-
-       /**
-        * Generate the message blob for a given module in a given language.
-        *
-        * @param ResourceLoaderModule $module
-        * @param string $lang Language code
-        * @return string JSON blob
-        */
-       private function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
-               $messages = [];
-               foreach ( $module->getMessages() as $key ) {
-                       $messages[$key] = $this->fetchMessage( $key, $lang );
-               }
-
-               $json = FormatJson::encode( (object)$messages );
-               // @codeCoverageIgnoreStart
-               if ( $json === false ) {
-                       $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [
-                               'module' => $module->getName(),
-                               'lang' => $lang,
-                       ] );
-                       $json = '{}';
-               }
-               // codeCoverageIgnoreEnd
-               return $json;
-       }
-}
index eac9bae..b4440d6 100644 (file)
@@ -114,6 +114,11 @@ interface IDatabase {
         */
        const QUERY_PSEUDO_PERMANENT = 2;
 
+       /** @var bool Parameter to unionQueries() for UNION ALL */
+       const UNION_ALL = true;
+       /** @var bool Parameter to unionQueries() for UNION DISTINCT */
+       const UNION_DISTINCT = false;
+
        /**
         * A string describing the current software version, and possibly
         * other details in a user-friendly way. Will be listed on Special:Version, etc.
@@ -1384,7 +1389,7 @@ interface IDatabase {
         * This is used for providing overload point for other DB abstractions
         * not compatible with the MySQL syntax.
         * @param array $sqls SQL statements to combine
-        * @param bool $all Use UNION ALL
+        * @param bool $all Either IDatabase::UNION_ALL or IDatabase::UNION_DISTINCT
         * @return string SQL fragment
         */
        public function unionQueries( $sqls, $all );
diff --git a/includes/resourceloader/MessageBlobStore.php b/includes/resourceloader/MessageBlobStore.php
new file mode 100644 (file)
index 0000000..ceb51f2
--- /dev/null
@@ -0,0 +1,240 @@
+<?php
+/**
+ * Message blobs storage used by ResourceLoader.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Roan Kattouw
+ * @author Trevor Parscal
+ * @author Timo Tijhof
+ */
+
+use MediaWiki\MediaWikiServices;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Wikimedia\Rdbms\Database;
+
+/**
+ * This class generates message blobs for use by ResourceLoader modules.
+ *
+ * A message blob is a JSON object containing the interface messages for a certain module in
+ * a certain language.
+ */
+class MessageBlobStore implements LoggerAwareInterface {
+
+       /* @var ResourceLoader */
+       private $resourceloader;
+
+       /**
+        * @var LoggerInterface
+        */
+       protected $logger;
+
+       /**
+        * @var WANObjectCache
+        */
+       protected $wanCache;
+
+       /**
+        * @param ResourceLoader $rl
+        * @param LoggerInterface|null $logger
+        */
+       public function __construct( ResourceLoader $rl, LoggerInterface $logger = null ) {
+               $this->resourceloader = $rl;
+               $this->logger = $logger ?: new NullLogger();
+               $this->wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+       }
+
+       /**
+        * @since 1.27
+        * @param LoggerInterface $logger
+        */
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Get the message blob for a module
+        *
+        * @since 1.27
+        * @param ResourceLoaderModule $module
+        * @param string $lang Language code
+        * @return string JSON
+        */
+       public function getBlob( ResourceLoaderModule $module, $lang ) {
+               $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang );
+               return $blobs[$module->getName()];
+       }
+
+       /**
+        * Get the message blobs for a set of modules
+        *
+        * @since 1.27
+        * @param ResourceLoaderModule[] $modules Array of module objects keyed by name
+        * @param string $lang Language code
+        * @return array An array mapping module names to message blobs
+        */
+       public function getBlobs( array $modules, $lang ) {
+               // Each cache key for a message blob by module name and language code also has a generic
+               // check key without language code. This is used to invalidate any and all language subkeys
+               // that exist for a module from the updateMessage() method.
+               $cache = $this->wanCache;
+               $checkKeys = [
+                       // Global check key, see clear()
+                       $cache->makeKey( __CLASS__ )
+               ];
+               $cacheKeys = [];
+               foreach ( $modules as $name => $module ) {
+                       $cacheKey = $this->makeCacheKey( $module, $lang );
+                       $cacheKeys[$name] = $cacheKey;
+                       // Per-module check key, see updateMessage()
+                       $checkKeys[$cacheKey][] = $cache->makeKey( __CLASS__, $name );
+               }
+               $curTTLs = [];
+               $result = $cache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys );
+
+               $blobs = [];
+               foreach ( $modules as $name => $module ) {
+                       $key = $cacheKeys[$name];
+                       if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
+                               $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
+                       } else {
+                               // Use unexpired cache
+                               $blobs[$name] = $result[$key];
+                       }
+               }
+               return $blobs;
+       }
+
+       /**
+        * @deprecated since 1.27 Use getBlobs() instead
+        * @return array
+        */
+       public function get( ResourceLoader $resourceLoader, $modules, $lang ) {
+               return $this->getBlobs( $modules, $lang );
+       }
+
+       /**
+        * @since 1.27
+        * @param ResourceLoaderModule $module
+        * @param string $lang
+        * @return string Cache key
+        */
+       private function makeCacheKey( ResourceLoaderModule $module, $lang ) {
+               $messages = array_values( array_unique( $module->getMessages() ) );
+               sort( $messages );
+               return $this->wanCache->makeKey( __CLASS__, $module->getName(), $lang,
+                       md5( json_encode( $messages ) )
+               );
+       }
+
+       /**
+        * @since 1.27
+        * @param string $cacheKey
+        * @param ResourceLoaderModule $module
+        * @param string $lang
+        * @return string JSON blob
+        */
+       protected function recacheMessageBlob( $cacheKey, ResourceLoaderModule $module, $lang ) {
+               $blob = $this->generateMessageBlob( $module, $lang );
+               $cache = $this->wanCache;
+               $cache->set( $cacheKey, $blob,
+                       // Add part of a day to TTL to avoid all modules expiring at once
+                       $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ),
+                       Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) )
+               );
+               return $blob;
+       }
+
+       /**
+        * Invalidate cache keys for modules using this message key.
+        * Called by MessageCache when a message has changed.
+        *
+        * @param string $key Message key
+        */
+       public function updateMessage( $key ) {
+               $moduleNames = $this->getResourceLoader()->getModulesByMessage( $key );
+               foreach ( $moduleNames as $moduleName ) {
+                       // Uses a holdoff to account for database replica DB lag (for MessageCache)
+                       $this->wanCache->touchCheckKey( $this->wanCache->makeKey( __CLASS__, $moduleName ) );
+               }
+       }
+
+       /**
+        * Invalidate cache keys for all known modules.
+        * Called by LocalisationCache after cache is regenerated.
+        */
+       public function clear() {
+               $cache = $this->wanCache;
+               // Disable holdoff because this invalidates all modules and also not needed since
+               // LocalisationCache is stored outside the database and doesn't have lag.
+               $cache->touchCheckKey( $cache->makeKey( __CLASS__ ), $cache::HOLDOFF_NONE );
+       }
+
+       /**
+        * @since 1.27
+        * @return ResourceLoader
+        */
+       protected function getResourceLoader() {
+               return $this->resourceloader;
+       }
+
+       /**
+        * @since 1.27
+        * @param string $key Message key
+        * @param string $lang Language code
+        * @return string
+        */
+       protected function fetchMessage( $key, $lang ) {
+               $message = wfMessage( $key )->inLanguage( $lang );
+               $value = $message->plain();
+               if ( !$message->exists() ) {
+                       $this->logger->warning( 'Failed to find {messageKey} ({lang})', [
+                               'messageKey' => $key,
+                               'lang' => $lang,
+                       ] );
+               }
+               return $value;
+       }
+
+       /**
+        * Generate the message blob for a given module in a given language.
+        *
+        * @param ResourceLoaderModule $module
+        * @param string $lang Language code
+        * @return string JSON blob
+        */
+       private function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
+               $messages = [];
+               foreach ( $module->getMessages() as $key ) {
+                       $messages[$key] = $this->fetchMessage( $key, $lang );
+               }
+
+               $json = FormatJson::encode( (object)$messages );
+               // @codeCoverageIgnoreStart
+               if ( $json === false ) {
+                       $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [
+                               'module' => $module->getName(),
+                               'lang' => $lang,
+                       ] );
+                       $json = '{}';
+               }
+               // codeCoverageIgnoreEnd
+               return $json;
+       }
+}
index f60d5f0..055a6e2 100644 (file)
@@ -313,7 +313,11 @@ class SpecialContributions extends IncludableSpecialPage {
                $links = '';
                if ( $talk ) {
                        $tools = self::getUserLinks( $this, $userObj );
-                       $links = $this->getLanguage()->pipeList( $tools );
+                       $links = Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
+                       foreach ( $tools as $tool ) {
+                               $links .= Html::rawElement( 'span', [], $tool ) . ' ';
+                       }
+                       $links = trim( $links ) . Html::closeElement( 'span' );
 
                        // Show a note if the user is blocked and display the last block log entry.
                        // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
@@ -354,7 +358,8 @@ class SpecialContributions extends IncludableSpecialPage {
                }
 
                return Html::rawElement( 'div', [ 'class' => 'mw-contributions-user-tools' ],
-                       $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() )
+                       $this->msg( 'contributions-subtitle' )->rawParams( $user )->params( $userObj->getName() )
+                       . ' ' . $links
                );
        }
 
index 62c867b..8865654 100644 (file)
@@ -224,7 +224,8 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
                        $sql = $subsql[0];
                } else {
                        // need to resort and relimit after union
-                       $sql = $dbr->unionQueries( $subsql, false ) . ' ORDER BY rc_timestamp DESC';
+                       $sql = $dbr->unionQueries( $subsql, $dbr::UNION_DISTINCT ) .
+                               ' ORDER BY rc_timestamp DESC';
                        $sql = $dbr->limitResult( $sql, $limit, false );
                }
 
index 8f4d587..65b956e 100644 (file)
        "mycontris": "Contributions",
        "anoncontribs": "Contributions",
        "contribsub2": "For {{GENDER:$3|$1}} ($2)",
+       "contributions-subtitle": "For {{GENDER:$3|$1}}",
        "contributions-userdoesnotexist": "User account \"$1\" is not registered.",
        "negative-namespace-not-supported": "Namespaces with negative values are not supported.",
        "nocontribs": "No changes were found matching these criteria.",
index 292f682..f37b5c7 100644 (file)
        "mycontris": "In the personal urls page section - right upper corner.\n\nSee also:\n* {{msg-mw|Mycontris}}\n* {{msg-mw|Accesskey-pt-mycontris}}\n* {{msg-mw|Tooltip-pt-mycontris}}\n{{Identical|Contribution}}",
        "anoncontribs": "Same as {{msg-mw|mycontris}} but used for non-logged-in users.\n\nSee also:\n* {{msg-mw|Accesskey-pt-anoncontribs}}\n* {{msg-mw|Tooltip-pt-anoncontribs}}\n{{Identical|Contribution}}",
        "contribsub2": "Contributions for \"user\" (links). Parameters:\n* $1 is an IP address or a username, with a link which points to the user page (if registered user).\n* $2 is list of tool links. The list contains a link which has text {{msg-mw|Sp-contributions-talk}}.\n* $3 is a plain text username used for GENDER.\n{{Identical|For $1}}",
+       "contributions-subtitle": "Successor to {{msg-mw|contribssub2}}. Contributions for \"user\". Parameters:\n* $1 is an IP address or a username, with a link which points to the user page (if registered user).\n",
        "contributions-userdoesnotexist": "This message is used in [[Special:Contributions]]. It is used to tell the user that the name he searched for doesn't exist.\n\nParameters:\n* $1 - a username\n{{Identical|Userdoesnotexist}}",
        "negative-namespace-not-supported": "This message is used in [[Special:Contributions]] to tell users that use namespaces with negative value. It not supported as associated namespace(s) doesn't exist.",
        "nocontribs": "Used in [[Special:Contributions]] and [[Special:DeletedContributions]].\n\nSee examples: [[Special:Contributions/x]] and [[Special:DeletedContributions/x]].\n\nParameters:\n* $1 - (Unused) the user name",
index 57e04e0..e81e197 100644 (file)
@@ -117,7 +117,7 @@ class FindOrphanedFiles extends Maintenance {
                                                $oiWheres ? $dbr->makeList( $oiWheres, LIST_OR ) : '1=0'
                                        )
                                ],
-                               true // UNION ALL (performance)
+                               $dbr::UNION_ALL
                        ),
                        __METHOD__
                );
diff --git a/maintenance/mediawiki.Title/generateJsToUpperCaseList.js b/maintenance/mediawiki.Title/generateJsToUpperCaseList.js
new file mode 100644 (file)
index 0000000..fd742f6
--- /dev/null
@@ -0,0 +1,8 @@
+/* eslint-env node, es6 */
+var i, chars = [];
+
+for ( i = 0; i < 65536; i++ ) {
+       chars.push( String.fromCharCode( i ).toUpperCase() );
+}
+// eslint-disable-next-line no-console
+console.log( JSON.stringify( chars ) );
diff --git a/maintenance/mediawiki.Title/generatePhpCharToUpperMappings.php b/maintenance/mediawiki.Title/generatePhpCharToUpperMappings.php
new file mode 100755 (executable)
index 0000000..a04958c
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
+ *
+ * Compares output of String.toUpperCase in JavaScript with
+ * mb_strtoupper in PHP, and outputs a list of lower:upper
+ * mappings where they differ. This is then used by Title.js
+ * to provide the same normalization in the client as on
+ * the server.
+ */
+
+$data = [];
+
+// phpcs:disable MediaWiki.Usage.ForbiddenFunctions.exec
+$jsUpperChars = json_decode( exec( 'node generateJsToUpperCaseList.js' ) );
+// phpcs:enable MediaWiki.Usage.ForbiddenFunctions.exec
+
+for ( $i = 0; $i < 65536; $i++ ) {
+       if ( $i >= 0xd800 && $i <= 0xdfff ) {
+               // Skip surrogate pairs
+               continue;
+       }
+       $char = mb_convert_encoding( '&#' . $i . ';', 'UTF-8', 'HTML-ENTITIES' );
+       $phpUpper = mb_strtoupper( $char );
+       $jsUpper = $jsUpperChars[$i];
+       if ( $jsUpper !== $phpUpper ) {
+               $data[$char] = $phpUpper;
+       }
+}
+
+echo str_replace( '    ', "\t",
+       json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE )
+) . "\n";
index 3f12dc6..ce36105 100644 (file)
@@ -591,18 +591,6 @@ return [
                ],
                'group' => 'jquery.ui',
        ],
-       'jquery.ui.spinner' => [
-               'scripts' => 'resources/lib/jquery.ui/jquery.ui.spinner.js',
-               'dependencies' => [
-                       'jquery.ui.core',
-                       'jquery.ui.widget',
-                       'jquery.ui.button',
-               ],
-               'skinStyles' => [
-                       'default' => 'resources/lib/jquery.ui/themes/smoothness/jquery.ui.spinner.css',
-               ],
-               'group' => 'jquery.ui',
-       ],
        'jquery.ui.tabs' => [
                'scripts' => 'resources/lib/jquery.ui/jquery.ui.tabs.js',
                'dependencies' => [
@@ -1135,9 +1123,11 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.Title' => [
-               'scripts' => [
-                       'resources/src/mediawiki.Title/Title.js',
-                       'resources/src/mediawiki.Title/phpCharToUpper.js',
+               'localBasePath' => "$IP/resources/src/mediawiki.Title",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.Title",
+               'packageFiles' => [
+                       'Title.js',
+                       'phpCharToUpper.json'
                ],
                'dependencies' => [
                        'mediawiki.String',
@@ -1456,6 +1446,7 @@ return [
                'skinStyles' => [
                        'default' => 'resources/src/mediawiki.action/mediawiki.action.history.styles.css',
                ],
+               'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.action.view.dblClickEdit' => [
                'scripts' => 'resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js',
diff --git a/resources/lib/jquery.ui/jquery.ui.spinner.js b/resources/lib/jquery.ui/jquery.ui.spinner.js
deleted file mode 100644 (file)
index 98dc9df..0000000
+++ /dev/null
@@ -1,478 +0,0 @@
-/*!
- * jQuery UI Spinner 1.9.2
- * http://jqueryui.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- *
- * http://api.jqueryui.com/spinner/
- *
- * Depends:
- *  jquery.ui.core.js
- *  jquery.ui.widget.js
- *  jquery.ui.button.js
- */
-(function( $ ) {
-
-function modifier( fn ) {
-       return function() {
-               var previous = this.element.val();
-               fn.apply( this, arguments );
-               this._refresh();
-               if ( previous !== this.element.val() ) {
-                       this._trigger( "change" );
-               }
-       };
-}
-
-$.widget( "ui.spinner", {
-       version: "1.9.2",
-       defaultElement: "<input>",
-       widgetEventPrefix: "spin",
-       options: {
-               culture: null,
-               icons: {
-                       down: "ui-icon-triangle-1-s",
-                       up: "ui-icon-triangle-1-n"
-               },
-               incremental: true,
-               max: null,
-               min: null,
-               numberFormat: null,
-               page: 10,
-               step: 1,
-
-               change: null,
-               spin: null,
-               start: null,
-               stop: null
-       },
-
-       _create: function() {
-               // handle string values that need to be parsed
-               this._setOption( "max", this.options.max );
-               this._setOption( "min", this.options.min );
-               this._setOption( "step", this.options.step );
-
-               // format the value, but don't constrain
-               this._value( this.element.val(), true );
-
-               this._draw();
-               this._on( this._events );
-               this._refresh();
-
-               // turning off autocomplete prevents the browser from remembering the
-               // value when navigating through history, so we re-enable autocomplete
-               // if the page is unloaded before the widget is destroyed. #7790
-               this._on( this.window, {
-                       beforeunload: function() {
-                               this.element.removeAttr( "autocomplete" );
-                       }
-               });
-       },
-
-       _getCreateOptions: function() {
-               var options = {},
-                       element = this.element;
-
-               $.each( [ "min", "max", "step" ], function( i, option ) {
-                       var value = element.attr( option );
-                       if ( value !== undefined && value.length ) {
-                               options[ option ] = value;
-                       }
-               });
-
-               return options;
-       },
-
-       _events: {
-               keydown: function( event ) {
-                       if ( this._start( event ) && this._keydown( event ) ) {
-                               event.preventDefault();
-                       }
-               },
-               keyup: "_stop",
-               focus: function() {
-                       this.previous = this.element.val();
-               },
-               blur: function( event ) {
-                       if ( this.cancelBlur ) {
-                               delete this.cancelBlur;
-                               return;
-                       }
-
-                       this._refresh();
-                       if ( this.previous !== this.element.val() ) {
-                               this._trigger( "change", event );
-                       }
-               },
-               mousewheel: function( event, delta ) {
-                       if ( !delta ) {
-                               return;
-                       }
-                       if ( !this.spinning && !this._start( event ) ) {
-                               return false;
-                       }
-
-                       this._spin( (delta > 0 ? 1 : -1) * this.options.step, event );
-                       clearTimeout( this.mousewheelTimer );
-                       this.mousewheelTimer = this._delay(function() {
-                               if ( this.spinning ) {
-                                       this._stop( event );
-                               }
-                       }, 100 );
-                       event.preventDefault();
-               },
-               "mousedown .ui-spinner-button": function( event ) {
-                       var previous;
-
-                       // We never want the buttons to have focus; whenever the user is
-                       // interacting with the spinner, the focus should be on the input.
-                       // If the input is focused then this.previous is properly set from
-                       // when the input first received focus. If the input is not focused
-                       // then we need to set this.previous based on the value before spinning.
-                       previous = this.element[0] === this.document[0].activeElement ?
-                               this.previous : this.element.val();
-                       function checkFocus() {
-                               var isActive = this.element[0] === this.document[0].activeElement;
-                               if ( !isActive ) {
-                                       this.element.focus();
-                                       this.previous = previous;
-                                       // support: IE
-                                       // IE sets focus asynchronously, so we need to check if focus
-                                       // moved off of the input because the user clicked on the button.
-                                       this._delay(function() {
-                                               this.previous = previous;
-                                       });
-                               }
-                       }
-
-                       // ensure focus is on (or stays on) the text field
-                       event.preventDefault();
-                       checkFocus.call( this );
-
-                       // support: IE
-                       // IE doesn't prevent moving focus even with event.preventDefault()
-                       // so we set a flag to know when we should ignore the blur event
-                       // and check (again) if focus moved off of the input.
-                       this.cancelBlur = true;
-                       this._delay(function() {
-                               delete this.cancelBlur;
-                               checkFocus.call( this );
-                       });
-
-                       if ( this._start( event ) === false ) {
-                               return;
-                       }
-
-                       this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event );
-               },
-               "mouseup .ui-spinner-button": "_stop",
-               "mouseenter .ui-spinner-button": function( event ) {
-                       // button will add ui-state-active if mouse was down while mouseleave and kept down
-                       if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) {
-                               return;
-                       }
-
-                       if ( this._start( event ) === false ) {
-                               return false;
-                       }
-                       this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event );
-               },
-               // TODO: do we really want to consider this a stop?
-               // shouldn't we just stop the repeater and wait until mouseup before
-               // we trigger the stop event?
-               "mouseleave .ui-spinner-button": "_stop"
-       },
-
-       _draw: function() {
-               var uiSpinner = this.uiSpinner = this.element
-                       .addClass( "ui-spinner-input" )
-                       .attr( "autocomplete", "off" )
-                       .wrap( this._uiSpinnerHtml() )
-                       .parent()
-                               // add buttons
-                               .append( this._buttonHtml() );
-
-               this.element.attr( "role", "spinbutton" );
-
-               // button bindings
-               this.buttons = uiSpinner.find( ".ui-spinner-button" )
-                       .attr( "tabIndex", -1 )
-                       .button()
-                       .removeClass( "ui-corner-all" );
-
-               // IE 6 doesn't understand height: 50% for the buttons
-               // unless the wrapper has an explicit height
-               if ( this.buttons.height() > Math.ceil( uiSpinner.height() * 0.5 ) &&
-                               uiSpinner.height() > 0 ) {
-                       uiSpinner.height( uiSpinner.height() );
-               }
-
-               // disable spinner if element was already disabled
-               if ( this.options.disabled ) {
-                       this.disable();
-               }
-       },
-
-       _keydown: function( event ) {
-               var options = this.options,
-                       keyCode = $.ui.keyCode;
-
-               switch ( event.keyCode ) {
-               case keyCode.UP:
-                       this._repeat( null, 1, event );
-                       return true;
-               case keyCode.DOWN:
-                       this._repeat( null, -1, event );
-                       return true;
-               case keyCode.PAGE_UP:
-                       this._repeat( null, options.page, event );
-                       return true;
-               case keyCode.PAGE_DOWN:
-                       this._repeat( null, -options.page, event );
-                       return true;
-               }
-
-               return false;
-       },
-
-       _uiSpinnerHtml: function() {
-               return "<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>";
-       },
-
-       _buttonHtml: function() {
-               return "" +
-                       "<a class='ui-spinner-button ui-spinner-up ui-corner-tr'>" +
-                               "<span class='ui-icon " + this.options.icons.up + "'>&#9650;</span>" +
-                       "</a>" +
-                       "<a class='ui-spinner-button ui-spinner-down ui-corner-br'>" +
-                               "<span class='ui-icon " + this.options.icons.down + "'>&#9660;</span>" +
-                       "</a>";
-       },
-
-       _start: function( event ) {
-               if ( !this.spinning && this._trigger( "start", event ) === false ) {
-                       return false;
-               }
-
-               if ( !this.counter ) {
-                       this.counter = 1;
-               }
-               this.spinning = true;
-               return true;
-       },
-
-       _repeat: function( i, steps, event ) {
-               i = i || 500;
-
-               clearTimeout( this.timer );
-               this.timer = this._delay(function() {
-                       this._repeat( 40, steps, event );
-               }, i );
-
-               this._spin( steps * this.options.step, event );
-       },
-
-       _spin: function( step, event ) {
-               var value = this.value() || 0;
-
-               if ( !this.counter ) {
-                       this.counter = 1;
-               }
-
-               value = this._adjustValue( value + step * this._increment( this.counter ) );
-
-               if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false) {
-                       this._value( value );
-                       this.counter++;
-               }
-       },
-
-       _increment: function( i ) {
-               var incremental = this.options.incremental;
-
-               if ( incremental ) {
-                       return $.isFunction( incremental ) ?
-                               incremental( i ) :
-                               Math.floor( i*i*i/50000 - i*i/500 + 17*i/200 + 1 );
-               }
-
-               return 1;
-       },
-
-       _precision: function() {
-               var precision = this._precisionOf( this.options.step );
-               if ( this.options.min !== null ) {
-                       precision = Math.max( precision, this._precisionOf( this.options.min ) );
-               }
-               return precision;
-       },
-
-       _precisionOf: function( num ) {
-               var str = num.toString(),
-                       decimal = str.indexOf( "." );
-               return decimal === -1 ? 0 : str.length - decimal - 1;
-       },
-
-       _adjustValue: function( value ) {
-               var base, aboveMin,
-                       options = this.options;
-
-               // make sure we're at a valid step
-               // - find out where we are relative to the base (min or 0)
-               base = options.min !== null ? options.min : 0;
-               aboveMin = value - base;
-               // - round to the nearest step
-               aboveMin = Math.round(aboveMin / options.step) * options.step;
-               // - rounding is based on 0, so adjust back to our base
-               value = base + aboveMin;
-
-               // fix precision from bad JS floating point math
-               value = parseFloat( value.toFixed( this._precision() ) );
-
-               // clamp the value
-               if ( options.max !== null && value > options.max) {
-                       return options.max;
-               }
-               if ( options.min !== null && value < options.min ) {
-                       return options.min;
-               }
-
-               return value;
-       },
-
-       _stop: function( event ) {
-               if ( !this.spinning ) {
-                       return;
-               }
-
-               clearTimeout( this.timer );
-               clearTimeout( this.mousewheelTimer );
-               this.counter = 0;
-               this.spinning = false;
-               this._trigger( "stop", event );
-       },
-
-       _setOption: function( key, value ) {
-               if ( key === "culture" || key === "numberFormat" ) {
-                       var prevValue = this._parse( this.element.val() );
-                       this.options[ key ] = value;
-                       this.element.val( this._format( prevValue ) );
-                       return;
-               }
-
-               if ( key === "max" || key === "min" || key === "step" ) {
-                       if ( typeof value === "string" ) {
-                               value = this._parse( value );
-                       }
-               }
-
-               this._super( key, value );
-
-               if ( key === "disabled" ) {
-                       if ( value ) {
-                               this.element.prop( "disabled", true );
-                               this.buttons.button( "disable" );
-                       } else {
-                               this.element.prop( "disabled", false );
-                               this.buttons.button( "enable" );
-                       }
-               }
-       },
-
-       _setOptions: modifier(function( options ) {
-               this._super( options );
-               this._value( this.element.val() );
-       }),
-
-       _parse: function( val ) {
-               if ( typeof val === "string" && val !== "" ) {
-                       val = window.Globalize && this.options.numberFormat ?
-                               Globalize.parseFloat( val, 10, this.options.culture ) : +val;
-               }
-               return val === "" || isNaN( val ) ? null : val;
-       },
-
-       _format: function( value ) {
-               if ( value === "" ) {
-                       return "";
-               }
-               return window.Globalize && this.options.numberFormat ?
-                       Globalize.format( value, this.options.numberFormat, this.options.culture ) :
-                       value;
-       },
-
-       _refresh: function() {
-               this.element.attr({
-                       "aria-valuemin": this.options.min,
-                       "aria-valuemax": this.options.max,
-                       // TODO: what should we do with values that can't be parsed?
-                       "aria-valuenow": this._parse( this.element.val() )
-               });
-       },
-
-       // update the value without triggering change
-       _value: function( value, allowAny ) {
-               var parsed;
-               if ( value !== "" ) {
-                       parsed = this._parse( value );
-                       if ( parsed !== null ) {
-                               if ( !allowAny ) {
-                                       parsed = this._adjustValue( parsed );
-                               }
-                               value = this._format( parsed );
-                       }
-               }
-               this.element.val( value );
-               this._refresh();
-       },
-
-       _destroy: function() {
-               this.element
-                       .removeClass( "ui-spinner-input" )
-                       .prop( "disabled", false )
-                       .removeAttr( "autocomplete" )
-                       .removeAttr( "role" )
-                       .removeAttr( "aria-valuemin" )
-                       .removeAttr( "aria-valuemax" )
-                       .removeAttr( "aria-valuenow" );
-               this.uiSpinner.replaceWith( this.element );
-       },
-
-       stepUp: modifier(function( steps ) {
-               this._stepUp( steps );
-       }),
-       _stepUp: function( steps ) {
-               this._spin( (steps || 1) * this.options.step );
-       },
-
-       stepDown: modifier(function( steps ) {
-               this._stepDown( steps );
-       }),
-       _stepDown: function( steps ) {
-               this._spin( (steps || 1) * -this.options.step );
-       },
-
-       pageUp: modifier(function( pages ) {
-               this._stepUp( (pages || 1) * this.options.page );
-       }),
-
-       pageDown: modifier(function( pages ) {
-               this._stepDown( (pages || 1) * this.options.page );
-       }),
-
-       value: function( newVal ) {
-               if ( !arguments.length ) {
-                       return this._parse( this.element.val() );
-               }
-               modifier( this._value ).call( this, newVal );
-       },
-
-       widget: function() {
-               return this.uiSpinner;
-       }
-});
-
-}( jQuery ) );
diff --git a/resources/lib/jquery.ui/themes/smoothness/jquery.ui.spinner.css b/resources/lib/jquery.ui/themes/smoothness/jquery.ui.spinner.css
deleted file mode 100644 (file)
index e89b720..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*!
- * jQuery UI Spinner 1.9.2
- * http://jqueryui.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- *
- * http://docs.jquery.com/UI/Spinner#theming
- */
-.ui-spinner { position:relative; display: inline-block; overflow: hidden; padding: 0; vertical-align: middle; }
-.ui-spinner-input { border: none; background: none; padding: 0; margin: .2em 0; vertical-align: middle; margin-left: .4em; margin-right: 22px; }
-.ui-spinner-button { width: 16px; height: 50%; font-size: .5em; padding: 0; margin: 0; text-align: center; position: absolute; cursor: default; display: block; overflow: hidden; right: 0; }
-.ui-spinner a.ui-spinner-button { border-top: none; border-bottom: none; border-right: none; } /* more specificity required here to overide default borders */
-.ui-spinner .ui-icon { position: absolute; margin-top: -8px; top: 50%; left: 0; } /* vertical centre icon */
-.ui-spinner-up { top: 0; }
-.ui-spinner-down { bottom: 0; }
-
-/* TR overrides */
-.ui-spinner .ui-icon-triangle-1-s {
-       /* need to fix icons sprite */
-       background-position:-65px -16px;
-}
index 6bb3bce..78ae135 100644 (file)
@@ -34,6 +34,8 @@
        var
                mwString = require( 'mediawiki.String' ),
 
+               toUpperMapping = require( './phpCharToUpper.json' ),
+
                namespaceIds = mw.config.get( 'wgNamespaceIds' ),
 
                /**
                }
        };
 
+       /**
+        * PHP's strtoupper differs from String.toUpperCase in a number of cases (T147646).
+        *
+        * @param {string} chr Unicode character
+        * @return {string} Unicode character, in upper case, according to the same rules as in PHP
+        */
+       Title.phpCharToUpper = function ( chr ) {
+               var mapped = toUpperMapping[ chr ];
+               return mapped || chr.toUpperCase();
+       };
+
        /* Public members */
 
        Title.prototype = {
                        ) {
                                return this.title;
                        }
-                       // PHP's strtoupper differs from String.toUpperCase in a number of cases
-                       // Bug: T147646
                        return mw.Title.phpCharToUpper( this.title[ 0 ] ) + this.title.slice( 1 );
                },
 
diff --git a/resources/src/mediawiki.Title/phpCharToUpper.js b/resources/src/mediawiki.Title/phpCharToUpper.js
deleted file mode 100644 (file)
index ed700f0..0000000
+++ /dev/null
@@ -1,255 +0,0 @@
-// This file can't be parsed by JSDuck due to <https://github.com/tenderlove/rkelly/issues/35>.
-// (It is excluded in jsduck.json.)
-// ESLint suggests unquoting some object keys, which would render the file unparseable by Opera 12.
-/* eslint-disable quote-props */
-( function () {
-       var toUpperMapping = {
-               'ß': 'ß',
-               'ʼn': 'ʼn',
-               'Dž': 'Dž',
-               'dž': 'Dž',
-               'Lj': 'Lj',
-               'lj': 'Lj',
-               'Nj': 'Nj',
-               'nj': 'Nj',
-               'ǰ': 'ǰ',
-               'Dz': 'Dz',
-               'dz': 'Dz',
-               'ʝ': 'Ʝ',
-               'ͅ': 'ͅ',
-               'ΐ': 'ΐ',
-               'ΰ': 'ΰ',
-               'և': 'և',
-               'ᏸ': 'Ᏸ',
-               'ᏹ': 'Ᏹ',
-               'ᏺ': 'Ᏺ',
-               'ᏻ': 'Ᏻ',
-               'ᏼ': 'Ᏼ',
-               'ᏽ': 'Ᏽ',
-               'ẖ': 'ẖ',
-               'ẗ': 'ẗ',
-               'ẘ': 'ẘ',
-               'ẙ': 'ẙ',
-               'ẚ': 'ẚ',
-               'ὐ': 'ὐ',
-               'ὒ': 'ὒ',
-               'ὔ': 'ὔ',
-               'ὖ': 'ὖ',
-               'ᾀ': 'ᾈ',
-               'ᾁ': 'ᾉ',
-               'ᾂ': 'ᾊ',
-               'ᾃ': 'ᾋ',
-               'ᾄ': 'ᾌ',
-               'ᾅ': 'ᾍ',
-               'ᾆ': 'ᾎ',
-               'ᾇ': 'ᾏ',
-               'ᾈ': 'ᾈ',
-               'ᾉ': 'ᾉ',
-               'ᾊ': 'ᾊ',
-               'ᾋ': 'ᾋ',
-               'ᾌ': 'ᾌ',
-               'ᾍ': 'ᾍ',
-               'ᾎ': 'ᾎ',
-               'ᾏ': 'ᾏ',
-               'ᾐ': 'ᾘ',
-               'ᾑ': 'ᾙ',
-               'ᾒ': 'ᾚ',
-               'ᾓ': 'ᾛ',
-               'ᾔ': 'ᾜ',
-               'ᾕ': 'ᾝ',
-               'ᾖ': 'ᾞ',
-               'ᾗ': 'ᾟ',
-               'ᾘ': 'ᾘ',
-               'ᾙ': 'ᾙ',
-               'ᾚ': 'ᾚ',
-               'ᾛ': 'ᾛ',
-               'ᾜ': 'ᾜ',
-               'ᾝ': 'ᾝ',
-               'ᾞ': 'ᾞ',
-               'ᾟ': 'ᾟ',
-               'ᾠ': 'ᾨ',
-               'ᾡ': 'ᾩ',
-               'ᾢ': 'ᾪ',
-               'ᾣ': 'ᾫ',
-               'ᾤ': 'ᾬ',
-               'ᾥ': 'ᾭ',
-               'ᾦ': 'ᾮ',
-               'ᾧ': 'ᾯ',
-               'ᾨ': 'ᾨ',
-               'ᾩ': 'ᾩ',
-               'ᾪ': 'ᾪ',
-               'ᾫ': 'ᾫ',
-               'ᾬ': 'ᾬ',
-               'ᾭ': 'ᾭ',
-               'ᾮ': 'ᾮ',
-               'ᾯ': 'ᾯ',
-               'ᾲ': 'ᾲ',
-               'ᾳ': 'ᾼ',
-               'ᾴ': 'ᾴ',
-               'ᾶ': 'ᾶ',
-               'ᾷ': 'ᾷ',
-               'ᾼ': 'ᾼ',
-               'ῂ': 'ῂ',
-               'ῃ': 'ῌ',
-               'ῄ': 'ῄ',
-               'ῆ': 'ῆ',
-               'ῇ': 'ῇ',
-               'ῌ': 'ῌ',
-               'ῒ': 'ῒ',
-               'ΐ': 'ΐ',
-               'ῖ': 'ῖ',
-               'ῗ': 'ῗ',
-               'ῢ': 'ῢ',
-               'ΰ': 'ΰ',
-               'ῤ': 'ῤ',
-               'ῦ': 'ῦ',
-               'ῧ': 'ῧ',
-               'ῲ': 'ῲ',
-               'ῳ': 'ῼ',
-               'ῴ': 'ῴ',
-               'ῶ': 'ῶ',
-               'ῷ': 'ῷ',
-               'ῼ': 'ῼ',
-               'ⅰ': 'ⅰ',
-               'ⅱ': 'ⅱ',
-               'ⅲ': 'ⅲ',
-               'ⅳ': 'ⅳ',
-               'ⅴ': 'ⅴ',
-               'ⅵ': 'ⅵ',
-               'ⅶ': 'ⅶ',
-               'ⅷ': 'ⅷ',
-               'ⅸ': 'ⅸ',
-               'ⅹ': 'ⅹ',
-               'ⅺ': 'ⅺ',
-               'ⅻ': 'ⅻ',
-               'ⅼ': 'ⅼ',
-               'ⅽ': 'ⅽ',
-               'ⅾ': 'ⅾ',
-               'ⅿ': 'ⅿ',
-               'ⓐ': 'ⓐ',
-               'ⓑ': 'ⓑ',
-               'ⓒ': 'ⓒ',
-               'ⓓ': 'ⓓ',
-               'ⓔ': 'ⓔ',
-               'ⓕ': 'ⓕ',
-               'ⓖ': 'ⓖ',
-               'ⓗ': 'ⓗ',
-               'ⓘ': 'ⓘ',
-               'ⓙ': 'ⓙ',
-               'ⓚ': 'ⓚ',
-               'ⓛ': 'ⓛ',
-               'ⓜ': 'ⓜ',
-               'ⓝ': 'ⓝ',
-               'ⓞ': 'ⓞ',
-               'ⓟ': 'ⓟ',
-               'ⓠ': 'ⓠ',
-               'ⓡ': 'ⓡ',
-               'ⓢ': 'ⓢ',
-               'ⓣ': 'ⓣ',
-               'ⓤ': 'ⓤ',
-               'ⓥ': 'ⓥ',
-               'ⓦ': 'ⓦ',
-               'ⓧ': 'ⓧ',
-               'ⓨ': 'ⓨ',
-               'ⓩ': 'ⓩ',
-               'ꞵ': 'Ꞵ',
-               'ꞷ': 'Ꞷ',
-               'ꭓ': 'Ꭓ',
-               'ꭰ': 'Ꭰ',
-               'ꭱ': 'Ꭱ',
-               'ꭲ': 'Ꭲ',
-               'ꭳ': 'Ꭳ',
-               'ꭴ': 'Ꭴ',
-               'ꭵ': 'Ꭵ',
-               'ꭶ': 'Ꭶ',
-               'ꭷ': 'Ꭷ',
-               'ꭸ': 'Ꭸ',
-               'ꭹ': 'Ꭹ',
-               'ꭺ': 'Ꭺ',
-               'ꭻ': 'Ꭻ',
-               'ꭼ': 'Ꭼ',
-               'ꭽ': 'Ꭽ',
-               'ꭾ': 'Ꭾ',
-               'ꭿ': 'Ꭿ',
-               'ꮀ': 'Ꮀ',
-               'ꮁ': 'Ꮁ',
-               'ꮂ': 'Ꮂ',
-               'ꮃ': 'Ꮃ',
-               'ꮄ': 'Ꮄ',
-               'ꮅ': 'Ꮅ',
-               'ꮆ': 'Ꮆ',
-               'ꮇ': 'Ꮇ',
-               'ꮈ': 'Ꮈ',
-               'ꮉ': 'Ꮉ',
-               'ꮊ': 'Ꮊ',
-               'ꮋ': 'Ꮋ',
-               'ꮌ': 'Ꮌ',
-               'ꮍ': 'Ꮍ',
-               'ꮎ': 'Ꮎ',
-               'ꮏ': 'Ꮏ',
-               'ꮐ': 'Ꮐ',
-               'ꮑ': 'Ꮑ',
-               'ꮒ': 'Ꮒ',
-               'ꮓ': 'Ꮓ',
-               'ꮔ': 'Ꮔ',
-               'ꮕ': 'Ꮕ',
-               'ꮖ': 'Ꮖ',
-               'ꮗ': 'Ꮗ',
-               'ꮘ': 'Ꮘ',
-               'ꮙ': 'Ꮙ',
-               'ꮚ': 'Ꮚ',
-               'ꮛ': 'Ꮛ',
-               'ꮜ': 'Ꮜ',
-               'ꮝ': 'Ꮝ',
-               'ꮞ': 'Ꮞ',
-               'ꮟ': 'Ꮟ',
-               'ꮠ': 'Ꮠ',
-               'ꮡ': 'Ꮡ',
-               'ꮢ': 'Ꮢ',
-               'ꮣ': 'Ꮣ',
-               'ꮤ': 'Ꮤ',
-               'ꮥ': 'Ꮥ',
-               'ꮦ': 'Ꮦ',
-               'ꮧ': 'Ꮧ',
-               'ꮨ': 'Ꮨ',
-               'ꮩ': 'Ꮩ',
-               'ꮪ': 'Ꮪ',
-               'ꮫ': 'Ꮫ',
-               'ꮬ': 'Ꮬ',
-               'ꮭ': 'Ꮭ',
-               'ꮮ': 'Ꮮ',
-               'ꮯ': 'Ꮯ',
-               'ꮰ': 'Ꮰ',
-               'ꮱ': 'Ꮱ',
-               'ꮲ': 'Ꮲ',
-               'ꮳ': 'Ꮳ',
-               'ꮴ': 'Ꮴ',
-               'ꮵ': 'Ꮵ',
-               'ꮶ': 'Ꮶ',
-               'ꮷ': 'Ꮷ',
-               'ꮸ': 'Ꮸ',
-               'ꮹ': 'Ꮹ',
-               'ꮺ': 'Ꮺ',
-               'ꮻ': 'Ꮻ',
-               'ꮼ': 'Ꮼ',
-               'ꮽ': 'Ꮽ',
-               'ꮾ': 'Ꮾ',
-               'ꮿ': 'Ꮿ',
-               'ff': 'ff',
-               'fi': 'fi',
-               'fl': 'fl',
-               'ffi': 'ffi',
-               'ffl': 'ffl',
-               'ſt': 'ſt',
-               'st': 'st',
-               'ﬓ': 'ﬓ',
-               'ﬔ': 'ﬔ',
-               'ﬕ': 'ﬕ',
-               'ﬖ': 'ﬖ',
-               'ﬗ': 'ﬗ'
-       };
-       mw.Title.phpCharToUpper = function ( chr ) {
-               var mapped = toUpperMapping[ chr ];
-               return mapped || chr.toUpperCase();
-       };
-}() );
diff --git a/resources/src/mediawiki.Title/phpCharToUpper.json b/resources/src/mediawiki.Title/phpCharToUpper.json
new file mode 100644 (file)
index 0000000..b0887fa
--- /dev/null
@@ -0,0 +1,245 @@
+{
+       "ß": "ß",
+       "ʼn": "ʼn",
+       "Dž": "Dž",
+       "dž": "Dž",
+       "Lj": "Lj",
+       "lj": "Lj",
+       "Nj": "Nj",
+       "nj": "Nj",
+       "ǰ": "ǰ",
+       "Dz": "Dz",
+       "dz": "Dz",
+       "ʝ": "Ʝ",
+       "ͅ": "ͅ",
+       "ΐ": "ΐ",
+       "ΰ": "ΰ",
+       "և": "և",
+       "ᏸ": "Ᏸ",
+       "ᏹ": "Ᏹ",
+       "ᏺ": "Ᏺ",
+       "ᏻ": "Ᏻ",
+       "ᏼ": "Ᏼ",
+       "ᏽ": "Ᏽ",
+       "ẖ": "ẖ",
+       "ẗ": "ẗ",
+       "ẘ": "ẘ",
+       "ẙ": "ẙ",
+       "ẚ": "ẚ",
+       "ὐ": "ὐ",
+       "ὒ": "ὒ",
+       "ὔ": "ὔ",
+       "ὖ": "ὖ",
+       "ᾀ": "ᾈ",
+       "ᾁ": "ᾉ",
+       "ᾂ": "ᾊ",
+       "ᾃ": "ᾋ",
+       "ᾄ": "ᾌ",
+       "ᾅ": "ᾍ",
+       "ᾆ": "ᾎ",
+       "ᾇ": "ᾏ",
+       "ᾈ": "ᾈ",
+       "ᾉ": "ᾉ",
+       "ᾊ": "ᾊ",
+       "ᾋ": "ᾋ",
+       "ᾌ": "ᾌ",
+       "ᾍ": "ᾍ",
+       "ᾎ": "ᾎ",
+       "ᾏ": "ᾏ",
+       "ᾐ": "ᾘ",
+       "ᾑ": "ᾙ",
+       "ᾒ": "ᾚ",
+       "ᾓ": "ᾛ",
+       "ᾔ": "ᾜ",
+       "ᾕ": "ᾝ",
+       "ᾖ": "ᾞ",
+       "ᾗ": "ᾟ",
+       "ᾘ": "ᾘ",
+       "ᾙ": "ᾙ",
+       "ᾚ": "ᾚ",
+       "ᾛ": "ᾛ",
+       "ᾜ": "ᾜ",
+       "ᾝ": "ᾝ",
+       "ᾞ": "ᾞ",
+       "ᾟ": "ᾟ",
+       "ᾠ": "ᾨ",
+       "ᾡ": "ᾩ",
+       "ᾢ": "ᾪ",
+       "ᾣ": "ᾫ",
+       "ᾤ": "ᾬ",
+       "ᾥ": "ᾭ",
+       "ᾦ": "ᾮ",
+       "ᾧ": "ᾯ",
+       "ᾨ": "ᾨ",
+       "ᾩ": "ᾩ",
+       "ᾪ": "ᾪ",
+       "ᾫ": "ᾫ",
+       "ᾬ": "ᾬ",
+       "ᾭ": "ᾭ",
+       "ᾮ": "ᾮ",
+       "ᾯ": "ᾯ",
+       "ᾲ": "ᾲ",
+       "ᾳ": "ᾼ",
+       "ᾴ": "ᾴ",
+       "ᾶ": "ᾶ",
+       "ᾷ": "ᾷ",
+       "ᾼ": "ᾼ",
+       "ῂ": "ῂ",
+       "ῃ": "ῌ",
+       "ῄ": "ῄ",
+       "ῆ": "ῆ",
+       "ῇ": "ῇ",
+       "ῌ": "ῌ",
+       "ῒ": "ῒ",
+       "ΐ": "ΐ",
+       "ῖ": "ῖ",
+       "ῗ": "ῗ",
+       "ῢ": "ῢ",
+       "ΰ": "ΰ",
+       "ῤ": "ῤ",
+       "ῦ": "ῦ",
+       "ῧ": "ῧ",
+       "ῲ": "ῲ",
+       "ῳ": "ῼ",
+       "ῴ": "ῴ",
+       "ῶ": "ῶ",
+       "ῷ": "ῷ",
+       "ῼ": "ῼ",
+       "ⅰ": "ⅰ",
+       "ⅱ": "ⅱ",
+       "ⅲ": "ⅲ",
+       "ⅳ": "ⅳ",
+       "ⅴ": "ⅴ",
+       "ⅵ": "ⅵ",
+       "ⅶ": "ⅶ",
+       "ⅷ": "ⅷ",
+       "ⅸ": "ⅸ",
+       "ⅹ": "ⅹ",
+       "ⅺ": "ⅺ",
+       "ⅻ": "ⅻ",
+       "ⅼ": "ⅼ",
+       "ⅽ": "ⅽ",
+       "ⅾ": "ⅾ",
+       "ⅿ": "ⅿ",
+       "ⓐ": "ⓐ",
+       "ⓑ": "ⓑ",
+       "ⓒ": "ⓒ",
+       "ⓓ": "ⓓ",
+       "ⓔ": "ⓔ",
+       "ⓕ": "ⓕ",
+       "ⓖ": "ⓖ",
+       "ⓗ": "ⓗ",
+       "ⓘ": "ⓘ",
+       "ⓙ": "ⓙ",
+       "ⓚ": "ⓚ",
+       "ⓛ": "ⓛ",
+       "ⓜ": "ⓜ",
+       "ⓝ": "ⓝ",
+       "ⓞ": "ⓞ",
+       "ⓟ": "ⓟ",
+       "ⓠ": "ⓠ",
+       "ⓡ": "ⓡ",
+       "ⓢ": "ⓢ",
+       "ⓣ": "ⓣ",
+       "ⓤ": "ⓤ",
+       "ⓥ": "ⓥ",
+       "ⓦ": "ⓦ",
+       "ⓧ": "ⓧ",
+       "ⓨ": "ⓨ",
+       "ⓩ": "ⓩ",
+       "ꞵ": "Ꞵ",
+       "ꞷ": "Ꞷ",
+       "ꭓ": "Ꭓ",
+       "ꭰ": "Ꭰ",
+       "ꭱ": "Ꭱ",
+       "ꭲ": "Ꭲ",
+       "ꭳ": "Ꭳ",
+       "ꭴ": "Ꭴ",
+       "ꭵ": "Ꭵ",
+       "ꭶ": "Ꭶ",
+       "ꭷ": "Ꭷ",
+       "ꭸ": "Ꭸ",
+       "ꭹ": "Ꭹ",
+       "ꭺ": "Ꭺ",
+       "ꭻ": "Ꭻ",
+       "ꭼ": "Ꭼ",
+       "ꭽ": "Ꭽ",
+       "ꭾ": "Ꭾ",
+       "ꭿ": "Ꭿ",
+       "ꮀ": "Ꮀ",
+       "ꮁ": "Ꮁ",
+       "ꮂ": "Ꮂ",
+       "ꮃ": "Ꮃ",
+       "ꮄ": "Ꮄ",
+       "ꮅ": "Ꮅ",
+       "ꮆ": "Ꮆ",
+       "ꮇ": "Ꮇ",
+       "ꮈ": "Ꮈ",
+       "ꮉ": "Ꮉ",
+       "ꮊ": "Ꮊ",
+       "ꮋ": "Ꮋ",
+       "ꮌ": "Ꮌ",
+       "ꮍ": "Ꮍ",
+       "ꮎ": "Ꮎ",
+       "ꮏ": "Ꮏ",
+       "ꮐ": "Ꮐ",
+       "ꮑ": "Ꮑ",
+       "ꮒ": "Ꮒ",
+       "ꮓ": "Ꮓ",
+       "ꮔ": "Ꮔ",
+       "ꮕ": "Ꮕ",
+       "ꮖ": "Ꮖ",
+       "ꮗ": "Ꮗ",
+       "ꮘ": "Ꮘ",
+       "ꮙ": "Ꮙ",
+       "ꮚ": "Ꮚ",
+       "ꮛ": "Ꮛ",
+       "ꮜ": "Ꮜ",
+       "ꮝ": "Ꮝ",
+       "ꮞ": "Ꮞ",
+       "ꮟ": "Ꮟ",
+       "ꮠ": "Ꮠ",
+       "ꮡ": "Ꮡ",
+       "ꮢ": "Ꮢ",
+       "ꮣ": "Ꮣ",
+       "ꮤ": "Ꮤ",
+       "ꮥ": "Ꮥ",
+       "ꮦ": "Ꮦ",
+       "ꮧ": "Ꮧ",
+       "ꮨ": "Ꮨ",
+       "ꮩ": "Ꮩ",
+       "ꮪ": "Ꮪ",
+       "ꮫ": "Ꮫ",
+       "ꮬ": "Ꮬ",
+       "ꮭ": "Ꮭ",
+       "ꮮ": "Ꮮ",
+       "ꮯ": "Ꮯ",
+       "ꮰ": "Ꮰ",
+       "ꮱ": "Ꮱ",
+       "ꮲ": "Ꮲ",
+       "ꮳ": "Ꮳ",
+       "ꮴ": "Ꮴ",
+       "ꮵ": "Ꮵ",
+       "ꮶ": "Ꮶ",
+       "ꮷ": "Ꮷ",
+       "ꮸ": "Ꮸ",
+       "ꮹ": "Ꮹ",
+       "ꮺ": "Ꮺ",
+       "ꮻ": "Ꮻ",
+       "ꮼ": "Ꮼ",
+       "ꮽ": "Ꮽ",
+       "ꮾ": "Ꮾ",
+       "ꮿ": "Ꮿ",
+       "ff": "ff",
+       "fi": "fi",
+       "fl": "fl",
+       "ffi": "ffi",
+       "ffl": "ffl",
+       "ſt": "ſt",
+       "st": "st",
+       "ﬓ": "ﬓ",
+       "ﬔ": "ﬔ",
+       "ﬕ": "ﬕ",
+       "ﬖ": "ﬖ",
+       "ﬗ": "ﬗ"
+}
index 70bf39f..e577643 100644 (file)
@@ -3,7 +3,7 @@
 use Wikimedia\TestingAccessWrapper;
 
 /**
- * @group Cache
+ * @group ResourceLoader
  * @covers MessageBlobStore
  */
 class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
@@ -13,64 +13,17 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
 
        protected function setUp() {
                parent::setUp();
-               // MediaWiki tests defaults $wgMainWANCache to CACHE_NONE.
-               // Use hash instead so that caching is observed
-               $this->wanCache = $this->getMockBuilder( WANObjectCache::class )
-                       ->setConstructorArgs( [ [
-                               'cache' => new HashBagOStuff(),
-                               'pool' => 'test',
-                               'relayer' => new EventRelayerNull( [] )
-                       ] ] )
-                       ->setMethods( [ 'makePurgeValue' ] )
-                       ->getMock();
-
-               $this->wanCache->expects( $this->any() )
-                       ->method( 'makePurgeValue' )
-                       ->will( $this->returnCallback( function ( $timestamp, $holdoff ) {
-                               // Disable holdoff as it messes with testing. Aside from a 0-second holdoff,
-                               // make sure that "time" passes between getMulti() check init and the set()
-                               // in recacheMessageBlob(). This especially matters for Windows clocks.
-                               $ts = (float)$timestamp - 0.0001;
-
-                               return WANObjectCache::PURGE_VAL_PREFIX . $ts . ':0';
-                       } ) );
-       }
-
-       protected function makeBlobStore( $methods = null, $rl = null ) {
-               $blobStore = $this->getMockBuilder( MessageBlobStore::class )
-                       ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] )
-                       ->setMethods( $methods )
-                       ->getMock();
-
-               $access = TestingAccessWrapper::newFromObject( $blobStore );
-               $access->wanCache = $this->wanCache;
-               return $blobStore;
-       }
-
-       protected function makeModule( array $messages ) {
-               $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] );
-               $module->setName( 'test.blobstore' );
-               return $module;
-       }
-
-       /** @covers MessageBlobStore::setLogger */
-       public function testSetLogger() {
-               $blobStore = $this->makeBlobStore();
-               $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) );
+               // MediaWiki's test wrapper sets $wgMainWANCache to CACHE_NONE.
+               // Use HashBagOStuff here so that we can observe caching.
+               $this->wanCache = new WANObjectCache( [
+                       'cache' => new HashBagOStuff()
+               ] );
+
+               $this->clock = 1301655600.000;
+               $this->wanCache->setMockTime( $this->clock );
        }
 
-       /** @covers MessageBlobStore::getResourceLoader */
-       public function testGetResourceLoader() {
-               // Call protected method
-               $blobStore = TestingAccessWrapper::newFromObject( $this->makeBlobStore() );
-               $this->assertInstanceOf(
-                       ResourceLoader::class,
-                       $blobStore->getResourceLoader()
-               );
-       }
-
-       /** @covers MessageBlobStore::fetchMessage */
-       public function testFetchMessage() {
+       public function testBlobCreation() {
                $module = $this->makeModule( [ 'mainpage' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
@@ -81,140 +34,153 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' );
        }
 
-       /** @covers MessageBlobStore::fetchMessage */
-       public function testFetchMessageFail() {
+       public function testBlobCreation_unknownMessage() {
                $module = $this->makeModule( [ 'i-dont-exist' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
-
                $blobStore = $this->makeBlobStore( null, $rl );
-               $blob = $blobStore->getBlob( $module, 'en' );
 
+               // Generating a blob should succeed without errors,
+               // even if a message is unknown.
+               $blob = $blobStore->getBlob( $module, 'en' );
                $this->assertEquals( '{"i-dont-exist":"\u29fci-dont-exist\u29fd"}', $blob, 'Generated blob' );
        }
 
-       public function testGetBlob() {
-               $module = $this->makeModule( [ 'foo' ] );
+       public function testMessageCachingAndPurging() {
+               $module = $this->makeModule( [ 'example' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
-
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
+
+               // Advance this new WANObjectCache instance to a normal state,
+               // by doing one "get" and letting its hold off period expire.
+               // Without this, the first real "get" would lazy-initialise the
+               // checkKey and thus reject the first "set".
+               $blobStore->getBlob( $module, 'en' );
+               $this->clock += 20;
+
+               // Arrange version 1 of a message
                $blobStore->expects( $this->once() )
                        ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'Example' ) );
+                       ->will( $this->returnValue( 'First version' ) );
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"First version"}', $blob, 'Blob for v1' );
 
-               $this->assertEquals( '{"foo":"Example"}', $blob, 'Generated blob' );
-       }
-
-       public function testGetBlobCached() {
-               $module = $this->makeModule( [ 'example' ] );
-               $rl = new ResourceLoader();
-               $rl->register( $module->getName(), $module );
-
+               // Arrange version 2
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->once() )
                        ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'First' ) );
+                       ->will( $this->returnValue( 'Second version' ) );
+               $this->clock += 20;
 
-               $module = $this->makeModule( [ 'example' ] );
+               // Assert
+               // We do not validate whether a cached message is up-to-date.
+               // Instead, changes to messages will send us a purge.
+               // When cache is not purged or expired, it must be used.
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
+               $this->assertEquals( '{"example":"First version"}', $blob, 'Reuse cached v1 blob' );
 
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->never() )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'Second' ) );
+               // Purge cache
+               $blobStore->updateMessage( 'example' );
+               $this->clock += 20;
 
-               $module = $this->makeModule( [ 'example' ] );
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Cache hit' );
+               $this->assertEquals( '{"example":"Second version"}', $blob, 'Updated blob for v2' );
        }
 
-       public function testUpdateMessage() {
+       public function testPurgeEverything() {
                $module = $this->makeModule( [ 'example' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->once() )
+               // Advance this new WANObjectCache instance to a normal state.
+               $blobStore->getBlob( $module, 'en' );
+               $this->clock += 20;
+
+               // Arrange version 1 and 2
+               $blobStore->expects( $this->exactly( 2 ) )
                        ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'First' ) );
+                       ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
+               $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1' );
 
-               $blobStore->updateMessage( 'example' );
+               $this->clock += 20;
 
-               $module = $this->makeModule( [ 'example' ] );
-               $rl = new ResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->once() )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'Second' ) );
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1 again' );
+
+               // Purge everything
+               $blobStore->clear();
+               $this->clock += 20;
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' );
+               $this->assertEquals( '{"example":"Second"}', $blob, 'Blob for v2' );
        }
 
-       public function testValidation() {
+       public function testValidateAgainstModuleRegistry() {
+               // Arrange version 1 of a module
                $module = $this->makeModule( [ 'foo' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
-
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->once() )
                        ->method( 'fetchMessage' )
                        ->will( $this->returnValueMap( [
+                               // message key, language code, message value
                                [ 'foo', 'en', 'Hello' ],
                        ] ) );
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"foo":"Hello"}', $blob, 'Generated blob' );
+               $this->assertEquals( '{"foo":"Hello"}', $blob, 'Blob for v1' );
 
-               // Now, imagine a change to the module is deployed. The module now contains
-               // message 'foo' and 'bar'. While updateMessage() was not called (since no
-               // message values were changed) it should detect the change in list of
-               // message keys.
+               // Arrange version 2 of module
+               // While message values may be out of date, the set of messages returned
+               // must always match the set of message keys required by the module.
+               // We do not receive purges for this because no messages were changed.
                $module = $this->makeModule( [ 'foo', 'bar' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
-
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->exactly( 2 ) )
                        ->method( 'fetchMessage' )
                        ->will( $this->returnValueMap( [
+                               // message key, language code, message value
                                [ 'foo', 'en', 'Hello' ],
                                [ 'bar', 'en', 'World' ],
                        ] ) );
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Updated blob' );
+               $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Blob for v2' );
        }
 
-       public function testClear() {
-               $module = $this->makeModule( [ 'example' ] );
-               $rl = new ResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->exactly( 2 ) )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
-
-               $now = microtime( true );
-               $this->wanCache->setMockTime( $now );
-
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
+       public function testSetLoggedIsVoid() {
+               $blobStore = $this->makeBlobStore();
+               $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) );
+       }
 
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Cache-hit' );
+       private function makeBlobStore( $methods = null, $rl = null ) {
+               $blobStore = $this->getMockBuilder( MessageBlobStore::class )
+                       ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] )
+                       ->setMethods( $methods )
+                       ->getMock();
 
-               $now += 1;
-               $blobStore->clear();
+               $access = TestingAccessWrapper::newFromObject( $blobStore );
+               $access->wanCache = $this->wanCache;
+               return $blobStore;
+       }
 
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' );
+       private function makeModule( array $messages ) {
+               $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] );
+               $module->setName( 'test.blobstore' );
+               return $module;
        }
 }