Merge "Allow skins/extensions to define custom OOUI themes"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 15 Jul 2019 20:21:01 +0000 (20:21 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 15 Jul 2019 20:21:01 +0000 (20:21 +0000)
265 files changed:
.phpcs.xml
RELEASE-NOTES-1.34
composer.json
docs/hooks.txt
docs/pageupdater.txt
includes/CategoryFinder.php
includes/DefaultSettings.php
includes/GlobalFunctions.php
includes/Html.php
includes/MediaWiki.php
includes/Revision/RevisionStore.php
includes/Setup.php
includes/SiteStats.php
includes/Storage/DerivedPageDataUpdater.php
includes/Storage/PageEditStash.php
includes/Title.php
includes/WikiMap.php
includes/actions/HistoryAction.php
includes/actions/pagers/HistoryPager.php
includes/api/i18n/fr.json
includes/api/i18n/mk.json
includes/api/i18n/zh-hans.json
includes/block/BlockRestrictionStore.php
includes/cache/LinkCache.php
includes/cache/localisation/LocalisationCache.php
includes/db/DatabaseOracle.php
includes/deferred/DeferredUpdates.php
includes/deferred/LinksUpdate.php
includes/deferred/SiteStatsUpdate.php
includes/deferred/UserEditCountUpdate.php
includes/historyblob/ConcatenatedGzipHistoryBlob.php
includes/historyblob/HistoryBlobCurStub.php
includes/historyblob/HistoryBlobStub.php
includes/installer/SqliteInstaller.php
includes/installer/i18n/be-tarask.json
includes/installer/i18n/da.json
includes/installer/i18n/fa.json
includes/installer/i18n/it.json
includes/installer/i18n/mk.json
includes/installer/i18n/pt-br.json
includes/installer/i18n/sr-ec.json
includes/installer/i18n/sv.json
includes/installer/i18n/uk.json
includes/installer/i18n/vi.json
includes/jobqueue/JobQueue.php
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobQueueGroup.php
includes/jobqueue/JobQueueMemory.php
includes/jobqueue/JobQueueRedis.php
includes/jobqueue/jobs/CategoryMembershipChangeJob.php
includes/jobqueue/jobs/ClearUserWatchlistJob.php
includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/libs/MapCacheLRU.php
includes/libs/filebackend/FileBackend.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/filebackend/HTTPFileStreamer.php
includes/libs/http/MultiHttpClient.php
includes/libs/lockmanager/LockManager.php
includes/libs/mime/MimeAnalyzer.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/CachedBagOStuff.php
includes/libs/objectcache/EmptyBagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/MemcachedBagOStuff.php
includes/libs/objectcache/MemcachedPeclBagOStuff.php
includes/libs/objectcache/MemcachedPhpBagOStuff.php
includes/libs/objectcache/MultiWriteBagOStuff.php
includes/libs/objectcache/README.md [new file with mode: 0644]
includes/libs/objectcache/RedisBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/libs/objectcache/WinCacheBagOStuff.php
includes/libs/rdbms/ChronologyProtector.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMssql.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/redis/RedisConnectionPool.php
includes/objectcache/ObjectCache.php
includes/objectcache/SqlBagOStuff.php
includes/page/Article.php
includes/page/WikiPage.php
includes/poolcounter/PoolCounterWork.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderImage.php
includes/search/SearchOracle.php
includes/search/SearchSqlite.php
includes/session/PHPSessionHandler.php
includes/site/DBSiteStore.php
includes/skins/SkinTemplate.php
includes/specials/SpecialBrokenRedirects.php
includes/specials/SpecialChangeCredentials.php
includes/specials/SpecialChangeEmail.php
includes/specials/SpecialComparePages.php
includes/specials/SpecialDeletedContributions.php
includes/specials/SpecialDoubleRedirects.php
includes/specials/SpecialListGroupRights.php
includes/specials/SpecialListredirects.php
includes/specials/SpecialProtectedpages.php
includes/specials/SpecialProtectedtitles.php
includes/specials/SpecialTags.php
includes/specials/SpecialUncategorizedimages.php
includes/specials/SpecialUncategorizedpages.php
includes/specials/SpecialUndelete.php
includes/upload/UploadBase.php
includes/upload/UploadFromChunks.php
includes/user/User.php
includes/user/UserGroupMembership.php
includes/watcheditem/WatchedItemStore.php
languages/data/ZhConversion.php
languages/i18n/ban.json
languages/i18n/be-tarask.json
languages/i18n/bg.json
languages/i18n/ce.json
languages/i18n/ckb.json
languages/i18n/co.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/dsb.json
languages/i18n/es.json
languages/i18n/exif/sr-ec.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hy.json
languages/i18n/id.json
languages/i18n/ja.json
languages/i18n/ko.json
languages/i18n/min.json
languages/i18n/nap.json
languages/i18n/nqo.json
languages/i18n/ru.json
languages/i18n/sr-ec.json
languages/i18n/th.json
maintenance/attachLatest.php
maintenance/cleanupInvalidDbKeys.php
maintenance/copyFileBackend.php
maintenance/createAndPromote.php
maintenance/dictionary/mediawiki.dic
maintenance/includes/BackupDumper.php
maintenance/includes/TextPassDumper.php
maintenance/initEditCount.php
maintenance/language/zhtable/toCN.manual
maintenance/language/zhtable/toHK.manual
maintenance/language/zhtable/toSimp.manual
maintenance/language/zhtable/toTW.manual
maintenance/language/zhtable/toTrad.manual
maintenance/language/zhtable/trad2simp.manual
maintenance/language/zhtable/tradphrases.manual
maintenance/language/zhtable/tradphrases_exclude.manual
maintenance/mctest.php
maintenance/nukePage.php
maintenance/rebuildrecentchanges.php
maintenance/storage/orphanStats.php
maintenance/storage/recompressTracked.php
maintenance/syncFileBackend.php
maintenance/update.php
maintenance/updateSearchIndex.php
phpunit.xml.dist
resources/src/mediawiki.legacy/shared.css
resources/src/mediawiki.less/mediawiki.ui/variables.less
resources/src/mediawiki.messagePoster.wikitext/WikitextMessagePoster.js
resources/src/mediawiki.messagePoster/MessagePoster.js
resources/src/mediawiki.ui/components/forms.less
tests/common/TestSetup.php
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/MediaWikiUnitTestCase.php
tests/phpunit/ResourceLoaderTestCase.php
tests/phpunit/bootstrap.php
tests/phpunit/includes/GlobalFunctions/wfAppendQueryTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfArrayPlus2dTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfEscapeShellArgTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php [deleted file]
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/PathRouterTest.php [deleted file]
tests/phpunit/includes/Rest/ResponseFactoryTest.php [deleted file]
tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php [deleted file]
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
tests/phpunit/includes/Revision/SlotRecordTest.php [deleted file]
tests/phpunit/includes/TitleArrayFromResultTest.php [deleted file]
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/WikiMapTest.php
tests/phpunit/includes/WikiReferenceTest.php [deleted file]
tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/db/LoadBalancerTest.php
tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php [deleted file]
tests/phpunit/includes/deferred/SiteStatsUpdateTest.php
tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php [deleted file]
tests/phpunit/includes/diff/SlotDiffRendererTest.php [deleted file]
tests/phpunit/includes/filebackend/HTTPFileStreamerTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php [deleted file]
tests/phpunit/includes/filerepo/file/ForeignDBFileTest.php [deleted file]
tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php [deleted file]
tests/phpunit/includes/http/HttpTest.php
tests/phpunit/includes/jobqueue/jobs/RefreshLinksJobTest.php
tests/phpunit/includes/json/FormatJsonTest.php [deleted file]
tests/phpunit/includes/libs/MapCacheLRUTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/includes/libs/rdbms/resultwrapper/FakeResultWrapperTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/rdbms/resultwrapper/ResultWrapperTest.php [new file with mode: 0644]
tests/phpunit/includes/linker/LinkRendererTest.php
tests/phpunit/includes/media/JpegMetadataExtractorTest.php [deleted file]
tests/phpunit/includes/page/ArticleTest.php [deleted file]
tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php
tests/phpunit/includes/session/SessionTest.php
tests/phpunit/includes/session/TokenTest.php [deleted file]
tests/phpunit/includes/shell/FirejailCommandTest.php [deleted file]
tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php [deleted file]
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/utils/ZipDirectoryReaderTest.php [deleted file]
tests/phpunit/languages/LanguageCodeTest.php [deleted file]
tests/phpunit/structure/ResourcesTest.php
tests/phpunit/unit/includes/GlobalFunctions/wfAppendQueryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfArrayPlus2dTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfAssembleUrlTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfBaseNameTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfEscapeShellArgTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfGetCallerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfShorthandToIntegerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfStringToBoolTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfTimestampTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfUrlencodeTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/PathRouterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Rest/ResponseFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/SlotRecordTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/TitleArrayFromResultTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/WikiReferenceTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/CeeFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/filerepo/file/ForeignDBFileTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/htmlform/HTMLCheckMatrixTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/http/HttpUnitTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/json/FormatJsonTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/language/LanguageCodeTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/language/SpecialPageAliasTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/page/ArticleTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/SessionUnitTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/TokenTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/shell/FirejailCommandTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php [new file with mode: 0644]
tests/phpunit/unit/languages/SpecialPageAliasTest.php [deleted file]

index 8f3bd8c..9f11ebc 100644 (file)
                <exclude-pattern>*/maintenance/storage/recompressTracked\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/storage/trackBlobs\.php</exclude-pattern>
                <!-- Skip violations in some tests for now -->
+               <exclude-pattern>*/tests/phpunit/unit/includes/GlobalFunctions/*\.php</exclude-pattern>
                <exclude-pattern>*/tests/phpunit/includes/GlobalFunctions/*\.php</exclude-pattern>
                <exclude-pattern>*/tests/phpunit/maintenance/*\.php</exclude-pattern>
                <exclude-pattern>*/tests/phpunit/integration/includes/GlobalFunctions/*\.php</exclude-pattern>
index 297ce28..44f69c7 100644 (file)
@@ -58,6 +58,9 @@ For notes on 1.33.x and older releases, see HISTORY.
 * $wgWikiDiff2MovedParagraphDetectionCutoff — If you still want a custom change
   size threshold, please specify in php.ini, using the configuration variable
   wikidiff2.moved_paragraph_detection_cutoff.
+* $wgDebugPrintHttpHeaders - The default of including HTTP headers in the
+  debug log channel is no longer configurable. The debug log itself remains
+  configurable via $wgDebugLogFile.
 
 === New user-facing features in 1.34 ===
 * Special:Mute has been added as a quick way for users to block unwanted emails
@@ -82,9 +85,10 @@ For notes on 1.33.x and older releases, see HISTORY.
 * Updated wikimedia/at-ease from 1.2.0 to 2.0.0.
 * Updated wikimedia/remex-html from 2.0.1 to 2.0.3.
 * Updated monolog/monolog from 1.22.1 to 1.24.0 (dev-only).
-* Updated wikimedia/object-factory from 1.0.0 to 2.0.0.
+* Updated wikimedia/object-factory from 1.0.0 to 2.1.0.
 * Updated wikimedia/timestamp from 2.2.0 to 3.0.0.
 * Updated wikimedia/xmp-reader from 0.6.2 to 0.6.3.
+* Updated mediawiki/mediawiki-phan-config from 0.6.0 to 0.6.1 (dev-only).
 * …
 
 ==== Removed external libraries ====
@@ -265,6 +269,18 @@ because of Phabricator reports.
   in JavaScript, use mw.log.deprecate() instead.
 * The 'user.groups' module, deprecated in 1.28, was removed.
   Use the 'user' module instead.
+* The ability to override User::$mRights has been removed.
+* Previously, when iterating ResultWrapper with foreach() or a similar
+  construct, the range of the index was 1..numRows. This has been fixed to be
+  0..(numRows-1).
+* The ChangePasswordForm hook, deprecated in 1.27, has been removed. Use the
+  AuthChangeFormFields hook or security levels instead.
+* WikiMap::getWikiIdFromDomain(), deprecated in 1.33, has been removed.
+  Use WikiMap::getWikiIdFromDbDomain() instead.
+* The config variables $wgHtml5, $wgJsMimeType, and $wgXhtmlDefaultNamespace,
+  which were deprecated and ignored by core since 1.22, are no longer set to any
+  value, and SkinTemplate no longer emits a 'jsmimetype' key. Any extensions not
+  updated since 2013 to cope with this deprecation may now break.
 * …
 
 === Deprecations in 1.34 ===
@@ -342,6 +358,8 @@ because of Phabricator reports.
   template option 'searchaction' instead.
 * LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have
   been deprecated.
+* User::getRights() and User::$mRights have been deprecated. Use
+  PermissionManager::getUserPermissions() instead.
 
 === Other changes in 1.34 ===
 * …
index 35e451d..307e310 100644 (file)
@@ -43,7 +43,7 @@
                "wikimedia/html-formatter": "1.0.2",
                "wikimedia/ip-set": "2.0.1",
                "wikimedia/less.php": "1.8.0",
-               "wikimedia/object-factory": "2.0.0",
+               "wikimedia/object-factory": "2.1.0",
                "wikimedia/password-blacklist": "0.1.4",
                "wikimedia/php-session-serializer": "1.0.7",
                "wikimedia/purtle": "1.0.7",
@@ -76,7 +76,7 @@
                "wikimedia/avro": "1.8.0",
                "wikimedia/testing-access-wrapper": "~1.0",
                "wmde/hamcrest-html-matchers": "^0.1.0",
-               "mediawiki/mediawiki-phan-config": "0.6.0",
+               "mediawiki/mediawiki-phan-config": "0.6.1",
                "symfony/yaml": "3.4.28",
                "johnkary/phpunit-speedtrap": "^1.0 | ^2.0"
        },
index 1e5072f..80453f4 100644 (file)
@@ -944,12 +944,6 @@ No return data is accepted; this hook is for auditing only.
 $req: AuthenticationRequest object describing the change (and target user)
 $status: StatusValue with the result of the action
 
-'ChangePasswordForm': DEPRECATED since 1.27! Use AuthChangeFormFields or
-security levels. For extensions that need to add a field to the ChangePassword
-form via the Preferences form.
-&$extraFields: An array of arrays that hold fields like would be passed to the
-  pretty function.
-
 'ChangesListInitRows': Batch process change list rows prior to rendering.
 $changesList: ChangesList instance
 $rows: The data that will be rendered. May be a \Wikimedia\Rdbms\IResultWrapper
index 54eb91a..fd084c0 100644 (file)
@@ -161,11 +161,11 @@ Calling prepareUpdate() with the same parameters again has no effect.
 Calling it again with mismatching parameters, or calling it with parameters mismatching
 the ones prepareContent() was called with, triggers a LogicException.
 
-- getSecondaryDataUpdtes() returns DataUpdates that represent derived data for the revision.
+- getSecondaryDataUpdates() returns DataUpdates that represent derived data for the revision.
 These may be used to update such data, e.g. in ApiPurge, RefreshLinksJob, and the refreshLinks
 script.
 
-- doUpdates() triggers the updates defined by getSecondaryDataUpdtes(), and also causes
+- doUpdates() triggers the updates defined by getSecondaryDataUpdates(), and also causes
 updates to cached artifacts in the ParserCache, the CDN layer, etc. This is primarily
 used by PageUpdater, but also by PageArchive during undeletion, and when importing
 revisions from XML. doUpdates() can only be called after prepareUpdate() was used to
index 7446b59..720abc3 100644 (file)
@@ -213,14 +213,14 @@ class CategoryFinder {
                        /* WHERE  */ [ 'cl_from' => $this->next ],
                        __METHOD__ . '-1'
                );
-               foreach ( $res as $o ) {
-                       $k = $o->cl_to;
+               foreach ( $res as $row ) {
+                       $k = $row->cl_to;
 
                        # Update parent tree
-                       if ( !isset( $this->parents[$o->cl_from] ) ) {
-                               $this->parents[$o->cl_from] = [];
+                       if ( !isset( $this->parents[$row->cl_from] ) ) {
+                               $this->parents[$row->cl_from] = [];
                        }
-                       $this->parents[$o->cl_from][$k] = $o;
+                       $this->parents[$row->cl_from][$k] = $row;
 
                        # Ignore those we already have
                        if ( in_array( $k, $this->deadend ) ) {
@@ -245,9 +245,9 @@ class CategoryFinder {
                                /* WHERE  */ [ 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ],
                                __METHOD__ . '-2'
                        );
-                       foreach ( $res as $o ) {
-                               $id = $o->page_id;
-                               $name = $o->page_title;
+                       foreach ( $res as $row ) {
+                               $id = $row->page_id;
+                               $name = $row->page_title;
                                $this->name2id[$name] = $id;
                                $this->next[] = $id;
                                unset( $layer[$name] );
index 65b23d5..3bfc8f8 100644 (file)
@@ -2416,11 +2416,11 @@ $wgObjectCaches = [
                'class'       => ReplicatedBagOStuff::class,
                'readFactory' => [
                        'class' => SqlBagOStuff::class,
-                       'args'  => [ [ 'slaveOnly' => true ] ]
+                       'args'  => [ [ 'replicaOnly' => true ] ]
                ],
                'writeFactory' => [
                        'class' => SqlBagOStuff::class,
-                       'args'  => [ [ 'slaveOnly' => false ] ]
+                       'args'  => [ [ 'replicaOnly' => false ] ]
                ],
                'loggroup'  => 'SQLBagOStuff',
                'reportDupes' => false
@@ -2492,11 +2492,35 @@ $wgWANObjectCaches = [
 $wgEnableWANCacheReaper = false;
 
 /**
- * Main object stash type. This should be a fast storage system for storing
- * lightweight data like hit counters and user activity. Sites with multiple
- * data-centers should have this use a store that replicates all writes. The
- * store should have enough consistency for CAS operations to be usable.
- * Reads outside of those needed for merge() may be eventually consistent.
+ * The object store type of the main stash.
+ *
+ * This store should be a very fast storage system optimized for holding lightweight data
+ * like incrementable hit counters and current user activity. The store should replicate the
+ * dataset among all data-centers. Any add(), merge(), lock(), and unlock() operations should
+ * maintain "best effort" linearizability; as long as connectivity is strong, latency is low,
+ * and there is no eviction pressure prompted by low free space, those operations should be
+ * linearizable. In terms of PACELC (https://en.wikipedia.org/wiki/PACELC_theorem), the store
+ * should act as a PA/EL distributed system for these operations. One optimization for these
+ * operations is to route them to a "primary" data-center (e.g. one that serves HTTP POST) for
+ * synchronous execution and then replicate to the others asynchronously. This means that at
+ * least calls to these operations during HTTP POST requests would quickly return.
+ *
+ * All other operations, such as get(), set(), delete(), changeTTL(), incr(), and decr(),
+ * should be synchronous in the local data-center, replicating asynchronously to the others.
+ * This behavior can be overriden by the use of the WRITE_SYNC and READ_LATEST flags.
+ *
+ * The store should *preferably* have eventual consistency to handle network partitions.
+ *
+ * Modules that rely on the stash should be prepared for:
+ *   - add(), merge(), lock(), and unlock() to be slower than other write operations,
+ *     at least in "secondary" data-centers (e.g. one that only serves HTTP GET/HEAD)
+ *   - Other write operations to have race conditions accross data-centers
+ *   - Read operations to have race conditions accross data-centers
+ *   - Consistency to be either eventual (with Last-Write-Wins) or just "best effort"
+ *
+ * In general, this means avoiding updates during idempotent HTTP requests (GET/HEAD) and
+ * avoiding assumptions of true linearizability (e.g. accepting anomalies). Modules that need
+ * these kind of guarantees should use other storage mediums.
  *
  * The options are:
  *   - db:      Store cache objects in the DB
@@ -3253,33 +3277,6 @@ $wgOverrideUcfirstCharacters = [];
  */
 $wgMimeType = 'text/html';
 
-/**
- * Previously used as content type in HTML script tags. This is now ignored since
- * HTML5 doesn't require a MIME type for script tags (javascript is the default).
- * It was also previously used by RawAction to determine the ctype query parameter
- * value that will result in a javascript response.
- * @deprecated since 1.22
- */
-$wgJsMimeType = null;
-
-/**
- * The default xmlns attribute. The option to define this has been removed.
- * The value of this variable is no longer used by core and is set to a fixed
- * value in Setup.php for compatibility with extensions that depend on the value
- * of this variable being set. Such a dependency however is deprecated.
- * @deprecated since 1.22
- */
-$wgXhtmlDefaultNamespace = null;
-
-/**
- * Previously used to determine if we should output an HTML5 doctype.
- * This is no longer used as we always output HTML5 now. For compatibility with
- * extensions that still check the value of this config it's value is now forced
- * to true by Setup.php.
- * @deprecated since 1.22
- */
-$wgHtml5 = true;
-
 /**
  * Defines the value of the version attribute in the &lt;html&gt; tag, if any.
  *
@@ -6306,11 +6303,6 @@ $wgShowDebug = false;
  */
 $wgDebugTimestamps = false;
 
-/**
- * Print HTTP headers for every request in the debug information.
- */
-$wgDebugPrintHttpHeaders = true;
-
 /**
  * Show the contents of $wgHooks in Special:Version
  */
@@ -6499,14 +6491,6 @@ $wgStatsdSamplingRates = [
  */
 $wgPageInfoTransclusionLimit = 50;
 
-/**
- * Set this to an integer to only do synchronous site_stats updates
- * one every *this many* updates. The other requests go into pending
- * delta values in $wgMemc. Make sure that $wgMemc is a global cache.
- * If set to -1, updates *only* go to $wgMemc (useful for daemons).
- */
-$wgSiteStatsAsyncFactor = false;
-
 /**
  * Parser test suite files to be run by parserTests.php when no specific
  * filename is passed to it.
index 5f17ad8..c6c386c 100644 (file)
@@ -2756,30 +2756,27 @@ function wfStripIllegalFilenameChars( $name ) {
 }
 
 /**
- * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit
+ * Raise PHP's memory limit (if needed).
  *
- * @return int Resulting value of the memory limit.
+ * @internal For use by Setup.php
  */
-function wfMemoryLimit() {
-       global $wgMemoryLimit;
-       $memlimit = wfShorthandToInteger( ini_get( 'memory_limit' ) );
-       if ( $memlimit != -1 ) {
-               $conflimit = wfShorthandToInteger( $wgMemoryLimit );
-               if ( $conflimit == -1 ) {
+function wfMemoryLimit( $newLimit ) {
+       $oldLimit = wfShorthandToInteger( ini_get( 'memory_limit' ) );
+       // If the INI config is already unlimited, there is nothing larger
+       if ( $oldLimit != -1 ) {
+               $newLimit = wfShorthandToInteger( $newLimit );
+               if ( $newLimit == -1 ) {
                        wfDebug( "Removing PHP's memory limit\n" );
                        Wikimedia\suppressWarnings();
-                       ini_set( 'memory_limit', $conflimit );
+                       ini_set( 'memory_limit', $newLimit );
                        Wikimedia\restoreWarnings();
-                       return $conflimit;
-               } elseif ( $conflimit > $memlimit ) {
-                       wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" );
+               } elseif ( $newLimit > $oldLimit ) {
+                       wfDebug( "Raising PHP's memory limit to $newLimit bytes\n" );
                        Wikimedia\suppressWarnings();
-                       ini_set( 'memory_limit', $conflimit );
+                       ini_set( 'memory_limit', $newLimit );
                        Wikimedia\restoreWarnings();
-                       return $conflimit;
                }
        }
-       return $memlimit;
 }
 
 /**
index d0f9fc6..c4b57af 100644 (file)
@@ -154,8 +154,7 @@ class Html {
         * Returns an HTML link element in a string styled as a button
         * (when $wgUseMediaWikiUIEverywhere is enabled).
         *
-        * @param string $contents The raw HTML contents of the element: *not*
-        *   escaped!
+        * @param string $text The text of the element. Will be escaped (not raw HTML)
         * @param array $attrs Associative array of attributes, e.g., [
         *   'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
         *   further documentation.
@@ -163,10 +162,10 @@ class Html {
         * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
         * @return string Raw HTML
         */
-       public static function linkButton( $contents, array $attrs, array $modifiers = [] ) {
+       public static function linkButton( $text, array $attrs, array $modifiers = [] ) {
                return self::element( 'a',
                        self::buttonAttributes( $attrs, $modifiers ),
-                       $contents
+                       $text
                );
        }
 
index 69f23c1..3934cd2 100644 (file)
@@ -260,8 +260,16 @@ class MediaWiki {
                                        ) {
                                                list( , $subpage ) = $spFactory->resolveAlias( $title->getDBkey() );
                                                $target = $specialPage->getRedirect( $subpage );
-                                               // target can also be true. We let that case fall through to normal processing.
+                                               // Target can also be true. We let that case fall through to normal processing.
                                                if ( $target instanceof Title ) {
+                                                       if ( $target->isExternal() ) {
+                                                               // Handle interwiki redirects
+                                                               $target = SpecialPage::getTitleFor(
+                                                                       'GoToInterwiki',
+                                                                       $target->getPrefixedDBkey()
+                                                               );
+                                                       }
+
                                                        $query = $specialPage->getRedirectQuery( $subpage ) ?: [];
                                                        $request = new DerivativeRequest( $this->context->getRequest(), $query );
                                                        $request->setRequestURL( $this->context->getRequest()->getRequestURL() );
index ec1c08c..8a4b6dc 100644 (file)
@@ -445,7 +445,7 @@ class RevisionStore
         */
        public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
                // TODO: pass in a DBTransactionContext instead of a database connection.
-               $this->checkDatabaseWikiId( $dbw );
+               $this->checkDatabaseDomain( $dbw );
 
                $slotRoles = $rev->getSlotRoles();
 
@@ -1073,7 +1073,7 @@ class RevisionStore
                $minor,
                User $user
        ) {
-               $this->checkDatabaseWikiId( $dbw );
+               $this->checkDatabaseDomain( $dbw );
 
                $pageId = $title->getArticleID();
 
@@ -2247,32 +2247,14 @@ class RevisionStore
         * @param IDatabase $db
         * @throws MWException
         */
-       private function checkDatabaseWikiId( IDatabase $db ) {
-               $storeWiki = $this->dbDomain;
-               $dbWiki = $db->getDomainID();
-
-               if ( $dbWiki === $storeWiki ) {
-                       return;
-               }
-
-               $storeWiki = $storeWiki ?: $this->loadBalancer->getLocalDomainID();
-               // @FIXME: when would getDomainID() be false here?
-               $dbWiki = $dbWiki ?: wfWikiID();
-
-               if ( $dbWiki === $storeWiki ) {
-                       return;
-               }
-
-               // HACK: counteract encoding imposed by DatabaseDomain
-               $storeWiki = str_replace( '?h', '-', $storeWiki );
-               $dbWiki = str_replace( '?h', '-', $dbWiki );
-
-               if ( $dbWiki === $storeWiki ) {
+       private function checkDatabaseDomain( IDatabase $db ) {
+               $dbDomain = $db->getDomainID();
+               $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
+               if ( $dbDomain === $storeDomain ) {
                        return;
                }
 
-               throw new MWException( "RevisionStore for $storeWiki "
-                       . "cannot be used with a DB connection for $dbWiki" );
+               throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
        }
 
        /**
@@ -2288,7 +2270,7 @@ class RevisionStore
         * @return object|false data row as a raw object
         */
        private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
-               $this->checkDatabaseWikiId( $db );
+               $this->checkDatabaseDomain( $db );
 
                $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
                $options = [];
@@ -2608,7 +2590,7 @@ class RevisionStore
         *         of the corresponding revision.
         */
        public function listRevisionSizes( IDatabase $db, array $revIds ) {
-               $this->checkDatabaseWikiId( $db );
+               $this->checkDatabaseDomain( $db );
 
                $revLens = [];
                if ( !$revIds ) {
@@ -2745,7 +2727,7 @@ class RevisionStore
         * @return int
         */
        private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
-               $this->checkDatabaseWikiId( $db );
+               $this->checkDatabaseDomain( $db );
 
                if ( $rev->getPageId() === null ) {
                        return 0;
@@ -2804,7 +2786,7 @@ class RevisionStore
         * @return int
         */
        public function countRevisionsByPageId( IDatabase $db, $id ) {
-               $this->checkDatabaseWikiId( $db );
+               $this->checkDatabaseDomain( $db );
 
                $row = $db->selectRow( 'revision',
                        [ 'revCount' => 'COUNT(*)' ],
@@ -2853,7 +2835,7 @@ class RevisionStore
         * @return bool True if the given user was the only one to edit since the given timestamp
         */
        public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
-               $this->checkDatabaseWikiId( $db );
+               $this->checkDatabaseDomain( $db );
 
                if ( !$userId ) {
                        return false;
index 641f1f9..45a2456 100644 (file)
@@ -55,7 +55,7 @@ if ( ini_get( 'mbstring.func_overload' ) ) {
 // Start the autoloader, so that extensions can derive classes from core files
 require_once "$IP/includes/AutoLoader.php";
 
-// Load up some global defines
+// Load global constants
 require_once "$IP/includes/Defines.php";
 
 // Load default settings
@@ -89,9 +89,17 @@ if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) {
        die( 1 );
 }
 
+/**
+ * Changes to the PHP environment that don't vary on configuration.
+ */
+
 // Install a header callback
 MediaWiki\HeaderCallback::register();
 
+// Set the encoding used by reading HTTP input, writing HTTP output.
+// This is also the default for mbstring functions.
+mb_internal_encoding( 'UTF-8' );
+
 /**
  * Load LocalSettings.php
  */
@@ -128,8 +136,6 @@ ExtensionRegistry::getInstance()->loadFromQueue();
 // Don't let any other extensions load
 ExtensionRegistry::getInstance()->finish();
 
-mb_internal_encoding( 'UTF-8' );
-
 // Set the configured locale on all requests for consisteny
 putenv( "LC_ALL=$wgShellLocale" );
 setlocale( LC_ALL, $wgShellLocale );
@@ -579,12 +585,6 @@ if ( $wgUseFileCache || $wgUseCdn ) {
        $wgDebugToolbar = false;
 }
 
-// We always output HTML5 since 1.22, overriding these is no longer supported
-// we set them here for extensions that depend on its value.
-$wgHtml5 = true;
-$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml';
-$wgJsMimeType = 'text/javascript';
-
 // Blacklisted file extensions shouldn't appear on the "allowed" list
 $wgFileExtensions = array_values( array_diff( $wgFileExtensions, $wgFileBlacklist ) );
 
@@ -754,7 +754,9 @@ Profiler::instance()->scopedProfileOut( $ps_default2 );
 $ps_misc = Profiler::instance()->scopedProfileIn( $fname . '-misc' );
 
 // Raise the memory limit if it's too low
-wfMemoryLimit();
+// Note, this makes use of wfDebug, and thus should not be before
+// MWDebug::init() is called.
+wfMemoryLimit( $wgMemoryLimit );
 
 /**
  * Set up the timezone, suppressing the pseudo-security warning in PHP 5.1+
@@ -812,13 +814,9 @@ if ( $wgCommandLineMode ) {
        }
 } else {
        $debug = "\n\nStart request {$wgRequest->getMethod()} {$wgRequest->getRequestURL()}\n";
-
-       if ( $wgDebugPrintHttpHeaders ) {
-               $debug .= "HTTP HEADERS:\n";
-
-               foreach ( $wgRequest->getAllHeaders() as $name => $value ) {
-                       $debug .= "$name: $value\n";
-               }
+       $debug .= "HTTP HEADERS:\n";
+       foreach ( $wgRequest->getAllHeaders() as $name => $value ) {
+               $debug .= "$name: $value\n";
        }
        wfDebug( $debug );
 }
index e3cb617..cf3a1eb 100644 (file)
@@ -52,14 +52,14 @@ class SiteStats {
                $config = MediaWikiServices::getInstance()->getMainConfig();
 
                $lb = self::getLB();
-               $dbr = $lb->getConnection( DB_REPLICA );
+               $dbr = $lb->getConnectionRef( DB_REPLICA );
                wfDebug( __METHOD__ . ": reading site_stats from replica DB\n" );
                $row = self::doLoadFromDB( $dbr );
 
                if ( !self::isRowSane( $row ) && $lb->hasOrMadeRecentMasterChanges() ) {
                        // Might have just been initialized during this request? Underflow?
                        wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
-                       $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
+                       $row = self::doLoadFromDB( $lb->getConnectionRef( DB_MASTER ) );
                }
 
                if ( !self::isRowSane( $row ) ) {
@@ -76,7 +76,7 @@ class SiteStats {
                                SiteStatsInit::doAllAndCommit( $dbr );
                        }
 
-                       $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
+                       $row = self::doLoadFromDB( $lb->getConnectionRef( DB_MASTER ) );
                }
 
                if ( !self::isRowSane( $row ) ) {
@@ -155,7 +155,7 @@ class SiteStats {
                        $cache->makeKey( 'SiteStats', 'groupcounts', $group ),
                        $cache::TTL_HOUR,
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $group, $fname ) {
-                               $dbr = self::getLB()->getConnection( DB_REPLICA );
+                               $dbr = self::getLB()->getConnectionRef( DB_REPLICA );
                                $setOpts += Database::getCacheSetOptions( $dbr );
 
                                return (int)$dbr->selectField(
@@ -206,7 +206,7 @@ class SiteStats {
                        $cache->makeKey( 'SiteStats', 'page-in-namespace', $ns ),
                        $cache::TTL_HOUR,
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $ns, $fname ) {
-                               $dbr = self::getLB()->getConnection( DB_REPLICA );
+                               $dbr = self::getLB()->getConnectionRef( DB_REPLICA );
                                $setOpts += Database::getCacheSetOptions( $dbr );
 
                                return (int)$dbr->selectField(
index b4d6f05..5d847b6 100644 (file)
@@ -79,7 +79,7 @@ use WikiPage;
  *
  * DerivedPageDataUpdater instances are designed to be cached inside a WikiPage instance,
  * and re-used by callback code over the course of an update operation. It's a stepping stone
- * one the way to a more complete refactoring of WikiPage.
+ * on the way to a more complete refactoring of WikiPage.
  *
  * When using a DerivedPageDataUpdater, the following life cycle must be observed:
  * grabCurrentRevision (optional), prepareContent (optional), prepareUpdate (required
@@ -343,14 +343,6 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
                }
        }
 
-       /**
-        * @return bool|string
-        */
-       private function getWikiId() {
-               // TODO: get from RevisionStore
-               return false;
-       }
-
        /**
         * Checks whether this DerivedPageDataUpdater can be re-used for running updates targeting
         * the given revision.
@@ -580,7 +572,6 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
         */
        public function isContentDeleted() {
                if ( $this->revision ) {
-                       // XXX: if that revision is the current revision, this should be skipped
                        return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
                } else {
                        // If the content has not been saved yet, it cannot have been deleted yet.
@@ -1082,6 +1073,11 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
         *    See DataUpdate::getCauseAction(). (default 'unknown')
         *  - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent().
         *    (string, default 'unknown')
+        *  - known-revision-output: a combined canonical ParserOutput for the revision, perhaps
+        *    from some cache. The caller is responsible for ensuring that the ParserOutput indeed
+        *    matched the $rev and $options. This mechanism is intended as a temporary stop-gap,
+        *    for the time until caches have been changed to store RenderedRevision states instead
+        *    of ParserOutput objects. (default: null) (since 1.33)
         */
        public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
                Assert::parameter(
@@ -1228,14 +1224,17 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
                if ( $this->renderedRevision ) {
                        $this->renderedRevision->updateRevision( $revision );
                } else {
-
                        // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
                        // NOTE: the revision is either new or current, so we can bypass audience checks.
                        $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
                                $this->revision,
                                null,
                                null,
-                               [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ]
+                               [
+                                       'use-master' => $this->useMaster(),
+                                       'audience' => RevisionRecord::RAW,
+                                       'known-revision-output' => $options['known-revision-output'] ?? null
+                               ]
                        );
 
                        // XXX: Since we presumably are dealing with the current revision,
@@ -1574,7 +1573,10 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
 
                // TODO: In the wiring, register a listener for this on the new PageEventEmitter
                ResourceLoaderWikiModule::invalidateModuleCache(
-                       $title, $oldLegacyRevision, $legacyRevision, $this->getWikiId() ?: wfWikiID()
+                       $title,
+                       $oldLegacyRevision,
+                       $legacyRevision,
+                       $this->loadbalancerFactory->getLocalDomainID()
                );
 
                $this->doTransition( 'done' );
index 2285f4a..6caca29 100644 (file)
@@ -109,7 +109,7 @@ class PageEditStash {
                // the stash request finishes parsing. For the lock acquisition below, there is not much
                // need to duplicate parsing of the same content/user/summary bundle, so try to avoid
                // blocking at all here.
-               $dbw = $this->lb->getConnection( DB_MASTER );
+               $dbw = $this->lb->getConnectionRef( DB_MASTER );
                if ( !$dbw->lock( $key, $fname, 0 ) ) {
                        // De-duplicate requests on the same key
                        return self::ERROR_BUSY;
@@ -357,7 +357,8 @@ class PageEditStash {
         * @return string|null TS_MW timestamp or null
         */
        private function lastEditTime( User $user ) {
-               $db = $this->lb->getConnection( DB_REPLICA );
+               $db = $this->lb->getConnectionRef( DB_REPLICA );
+
                $actorQuery = ActorMigration::newMigration()->getWhere( $db, 'rc_user', $user, false );
                $time = $db->selectField(
                        [ 'recentchanges' ] + $actorQuery['tables'],
index 6e75102..95ccd9a 100644 (file)
@@ -1770,6 +1770,7 @@ class Title implements LinkTarget, IDBAccessObject {
                if (
                        !MediaWikiServices::getInstance()->getNamespaceInfo()->
                                hasSubpages( $this->mNamespace )
+                       || strtok( $this->getText(), '/' ) === false
                ) {
                        return $this->getText();
                }
@@ -1887,7 +1888,12 @@ class Title implements LinkTarget, IDBAccessObject {
         * @since 1.20
         */
        public function getSubpage( $text ) {
-               return self::makeTitleSafe( $this->mNamespace, $this->getText() . '/' . $text );
+               return self::makeTitleSafe(
+                       $this->mNamespace,
+                       $this->getText() . '/' . $text,
+                       '',
+                       $this->mInterwiki
+               );
        }
 
        /**
@@ -2294,34 +2300,6 @@ class Title implements LinkTarget, IDBAccessObject {
                        ->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors );
        }
 
-       /**
-        * Add the resulting error code to the errors array
-        *
-        * @param array $errors List of current errors
-        * @param array|string|MessageSpecifier|false $result Result of errors
-        *
-        * @return array List of errors
-        */
-       private function resultToError( $errors, $result ) {
-               if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
-                       // A single array representing an error
-                       $errors[] = $result;
-               } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
-                       // A nested array representing multiple errors
-                       $errors = array_merge( $errors, $result );
-               } elseif ( $result !== '' && is_string( $result ) ) {
-                       // A string representing a message-id
-                       $errors[] = [ $result ];
-               } elseif ( $result instanceof MessageSpecifier ) {
-                       // A message specifier representing an error
-                       $errors[] = [ $result ];
-               } elseif ( $result === false ) {
-                       // a generic "We don't want them to do that"
-                       $errors[] = [ 'badaccess-group0' ];
-               }
-               return $errors;
-       }
-
        /**
         * Get a filtered list of all restriction types supported by this wiki.
         * @param bool $exists True to get all restriction types that apply to
@@ -2949,7 +2927,7 @@ class Title implements LinkTarget, IDBAccessObject {
                        $this->mHasSubpages = false;
                        $subpages = $this->getSubpages( 1 );
                        if ( $subpages instanceof TitleArray ) {
-                               $this->mHasSubpages = (bool)$subpages->count();
+                               $this->mHasSubpages = (bool)$subpages->current();
                        }
                }
 
@@ -4290,7 +4268,7 @@ class Title implements LinkTarget, IDBAccessObject {
         * Get the timestamp when this page was updated since the user last saw it.
         *
         * @param User|null $user
-        * @return string|null
+        * @return string|bool|null String timestamp, false if not watched, null if nothing is unseen
         */
        public function getNotificationTimestamp( $user = null ) {
                global $wgUser;
index 23b0e3e..f2641f4 100644 (file)
@@ -286,15 +286,6 @@ class WikiMap {
                        : (string)$domain->getDatabase();
        }
 
-       /**
-        * @param string $domain
-        * @return string
-        * @deprecated Since 1.33; use getWikiIdFromDbDomain()
-        */
-       public static function getWikiIdFromDomain( $domain ) {
-               return self::getWikiIdFromDbDomain( $domain );
-       }
-
        /**
         * @return DatabaseDomain Database domain of the current wiki
         * @since 1.33
@@ -311,7 +302,7 @@ class WikiMap {
         * @since 1.33
         */
        public static function isCurrentWikiDbDomain( $domain ) {
-               return self::getCurrentWikiDbDomain()->equals( DatabaseDomain::newFromId( $domain ) );
+               return self::getCurrentWikiDbDomain()->equals( $domain );
        }
 
        /**
index 4df2f56..958ec06 100644 (file)
@@ -148,10 +148,17 @@ class HistoryAction extends FormlessAction {
                $out = $this->getOutput();
                $request = $this->getRequest();
 
-               /**
-                * Allow client caching.
-                */
-               if ( $out->checkLastModified( $this->page->getTouched() ) ) {
+               // Allow client-side HTTP caching of the history page.
+               // But, always ignore this cache if the (logged-in) user has this page on their watchlist
+               // and has one or more unseen revisions. Otherwise, we might be showing stale update markers.
+               // The Last-Modified for the history page does not change when user's markers are cleared,
+               // so going from "some unseen" to "all seen" would not clear the cache.
+               // But, when all of the revisions are marked as seen, then only way for new unseen revision
+               // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified.
+               if (
+                       !$this->hasUnseenRevisionMarkers() &&
+                       $out->checkLastModified( $this->page->getTouched() )
+               ) {
                        return null; // Client cache fresh and headers sent, nothing more to do.
                }
 
@@ -305,6 +312,16 @@ class HistoryAction extends FormlessAction {
                return null;
        }
 
+       /**
+        * @return bool Page is watched by and has unseen revision for the user
+        */
+       private function hasUnseenRevisionMarkers() {
+               return (
+                       $this->getContext()->getConfig()->get( 'ShowUpdatedMarker' ) &&
+                       $this->getTitle()->getNotificationTimestamp( $this->getUser() )
+               );
+       }
+
        /**
         * Fetch an array of revisions, specified by a given limit, offset and
         * direction. This is now only used by the feeds. It was previously
index c9c1b51..99c57e1 100644 (file)
@@ -123,7 +123,6 @@ class HistoryPager extends ReverseChronologicalPager {
         */
        function formatRow( $row ) {
                if ( $this->lastRow ) {
-                       $latest = ( $this->counter == 1 && $this->mIsFirst );
                        $firstInList = $this->counter == 1;
                        $this->counter++;
 
@@ -131,8 +130,7 @@ class HistoryPager extends ReverseChronologicalPager {
                                ? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
                                : false;
 
-                       $s = $this->historyLine(
-                               $this->lastRow, $row, $notifTimestamp, $latest, $firstInList );
+                       $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
                } else {
                        $s = '';
                }
@@ -185,34 +183,40 @@ class HistoryPager extends ReverseChronologicalPager {
                $s .= Html::hidden( 'type', 'revision' ) . "\n";
 
                // Button container stored in $this->buttons for re-use in getEndBody()
-               $this->buttons = Html::openElement( 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
-               $className = 'historysubmit mw-history-compareselectedversions-button';
-               $attrs = [ 'class' => $className ]
-                       + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
-               $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
-                       $attrs
-               ) . "\n";
-
-               $user = $this->getUser();
-               $actionButtons = '';
-               if ( $user->isAllowed( 'deleterevision' ) ) {
-                       $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' );
-               }
-               if ( $this->showTagEditUI ) {
-                       $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' );
-               }
-               if ( $actionButtons ) {
-                       $this->buttons .= Xml::tags( 'div', [ 'class' =>
-                               'mw-history-revisionactions' ], $actionButtons );
-               }
+               $this->buttons = '';
+               if ( $this->getNumRows() > 0 ) {
+                       $this->buttons .= Html::openElement(
+                               'div', [ 'class' => 'mw-history-compareselectedversions' ] );
+                       $className = 'historysubmit mw-history-compareselectedversions-button';
+                       $attrs = [ 'class' => $className ]
+                               + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
+                       $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
+                               $attrs
+                       ) . "\n";
+
+                       $user = $this->getUser();
+                       $actionButtons = '';
+                       if ( $user->isAllowed( 'deleterevision' ) ) {
+                               $actionButtons .= $this->getRevisionButton(
+                                       'revisiondelete', 'showhideselectedversions' );
+                       }
+                       if ( $this->showTagEditUI ) {
+                               $actionButtons .= $this->getRevisionButton(
+                                       'editchangetags', 'history-edit-tags' );
+                       }
+                       if ( $actionButtons ) {
+                               $this->buttons .= Xml::tags( 'div', [ 'class' =>
+                                       'mw-history-revisionactions' ], $actionButtons );
+                       }
 
-               if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
-                       $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
-               }
+                       if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
+                               $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
+                       }
 
-               $this->buttons .= '</div>';
+                       $this->buttons .= '</div>';
 
-               $s .= $this->buttons;
+                       $s .= $this->buttons;
+               }
                $s .= '<ul id="pagehistory">' . "\n";
 
                return $s;
@@ -236,7 +240,6 @@ class HistoryPager extends ReverseChronologicalPager {
 
        protected function getEndBody() {
                if ( $this->lastRow ) {
-                       $latest = $this->counter == 1 && $this->mIsFirst;
                        $firstInList = $this->counter == 1;
                        if ( $this->mIsBackwards ) {
                                # Next row is unknown, but for UI reasons, probably exists if an offset has been specified
@@ -255,8 +258,7 @@ class HistoryPager extends ReverseChronologicalPager {
                                ? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
                                : false;
 
-                       $s = $this->historyLine(
-                               $this->lastRow, $next, $notifTimestamp, $latest, $firstInList );
+                       $s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
                } else {
                        $s = '';
                }
@@ -295,13 +297,13 @@ class HistoryPager extends ReverseChronologicalPager {
         * @param mixed $next The database row corresponding to the next line
         *   (chronologically previous)
         * @param bool|string $notificationtimestamp
-        * @param bool $latest Whether this row corresponds to the page's latest revision.
+        * @param bool $dummy Unused.
         * @param bool $firstInList Whether this row corresponds to the first
         *   displayed on this history page.
         * @return string HTML output for the row
         */
        function historyLine( $row, $next, $notificationtimestamp = false,
-               $latest = false, $firstInList = false ) {
+               $dummy = false, $firstInList = false ) {
                $rev = new Revision( $row, 0, $this->getTitle() );
 
                if ( is_object( $next ) ) {
@@ -310,7 +312,8 @@ class HistoryPager extends ReverseChronologicalPager {
                        $prevRev = null;
                }
 
-               $curlink = $this->curLink( $rev, $latest );
+               $latest = $rev->getId() === $this->getWikiPage()->getLatest();
+               $curlink = $this->curLink( $rev );
                $lastlink = $this->lastLink( $rev, $next );
                $curLastlinks = Html::rawElement( 'span', [], $curlink ) .
                        Html::rawElement( 'span', [], $lastlink );
@@ -483,12 +486,12 @@ class HistoryPager extends ReverseChronologicalPager {
         * Create a diff-to-current link for this revision for this page
         *
         * @param Revision $rev
-        * @param bool $latest This is the latest revision of the page?
         * @return string
         */
-       function curLink( $rev, $latest ) {
+       function curLink( $rev ) {
                $cur = $this->historyPage->message['cur'];
-               if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+               $latest = $this->getWikiPage()->getLatest();
+               if ( $latest === $rev->getId() || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
                        return $cur;
                } else {
                        return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
@@ -496,7 +499,7 @@ class HistoryPager extends ReverseChronologicalPager {
                                new HtmlArmor( $cur ),
                                [],
                                [
-                                       'diff' => $this->getWikiPage()->getLatest(),
+                                       'diff' => $latest,
                                        'oldid' => $rev->getId()
                                ]
                        );
index 640ddfa..b04ad1b 100644 (file)
@@ -33,7 +33,8 @@
                        "Kenjiraw",
                        "Framawiki",
                        "Epok",
-                       "Derugon"
+                       "Derugon",
+                       "Lucas Werkmeister (WMDE)"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> L’API MediaWiki est une interface stable et mature qui est supportée et améliorée de façon active. Bien que nous essayions de l’éviter, nous pouvons avoir parfois besoin de faire des modifications impactantes ; inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet entête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Errors and warnings]].\n\n<p class=\"mw-apisandbox-link\"><strong>Test :</strong> Pour faciliter le test des requêtes à l’API, voyez [[Special:ApiSandbox]].</p>",
        "apihelp-query+languageinfo-paramvalue-prop-code": "Le code de langue (ce code est spécifique à MédiaWiki, bien qu’il y ait des recouvrements avec d’autres standards).",
        "apihelp-query+languageinfo-paramvalue-prop-bcp47": "Le code de langue BCP-47.",
        "apihelp-query+languageinfo-paramvalue-prop-dir": "La direction d’écriture de la langue (<code>ltr</code> ou <code>rtl</code>).",
-       "apihelp-query+languageinfo-paramvalue-prop-autonym": "L’autonyme d'une langue, c’est-à-dire son nom dans cette langue.",
+       "apihelp-query+languageinfo-paramvalue-prop-autonym": "L’autonyme dune langue, c’est-à-dire son nom dans cette langue.",
        "apihelp-query+languageinfo-paramvalue-prop-name": "Le nom de la langue dans la langue spécifiée par le paramètre <var>lilang</var>, avec application des langues de secours si besoin.",
        "apihelp-query+languageinfo-paramvalue-prop-fallbacks": "Les codes de langue des langues de secours configurées pour cette langue. Le secours implicite final en 'en' n’est pas inclus (mais certaines langues peuvent avoir 'en' en secours explicitement).",
        "apihelp-query+languageinfo-paramvalue-prop-variants": "Les codes de langue des variantes supportées par cette langue.",
index 26c7f10..f91cf3d 100644 (file)
@@ -20,7 +20,7 @@
        "apihelp-main-param-uselang": "Јазик за преведување на пораките. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> со <kbd>siprop=languages</kbd> дава список на јазични кодови, или укажете <kbd>user</kbd> за да го користите тековно зададениот јазик корисникот, или пак укажете <kbd>content</kbd> за да го користите јазикот на содржината на ова вики.",
        "apihelp-block-summary": "Блокирај корисник.",
        "apihelp-block-param-user": "Корисничко име, IP-адреса или IP-опсег ако сакате да блокирате. Не може да се користи заедно со <var>$1userid</var>",
-       "apihelp-block-param-expiry": "Време на истек. Може да биде релативно (на пр. <kbd>5 months</kbd> или „2 недели“) или пак апсолутно (на пр. <kbd>2014-09-18T12:34:56Z</kbd>). Ако го зададете <kbd>infinite</kbd>, <kbd>indefinite</kbd> или <kbd>never</kbd>, блокот ќе трае засекогаш.",
+       "apihelp-block-param-expiry": "Време на истек. Може да биде релативно (на пр. <kbd>5 months</kbd> или <kbd>2 weeks</kbd>) или пак апсолутно (на пр. <kbd>2014-09-18T12:34:56Z</kbd>). Ако го зададете <kbd>infinite</kbd>, <kbd>indefinite</kbd> или <kbd>never</kbd>, блокот ќе трае засекогаш.",
        "apihelp-block-param-reason": "Причина за блокирање.",
        "apihelp-block-param-anononly": "Блокирај само анонимни корисници (т.е. оневозможи анонимно уредување од оваа IP-адреса).",
        "apihelp-block-param-nocreate": "Оневозможи создавање кориснички сметки.",
        "apihelp-opensearch-param-search": "Низа за пребарување.",
        "apihelp-opensearch-param-limit": "Највеќе ставки за прикажување.",
        "apihelp-opensearch-param-namespace": "Именски простори за пребарување.",
-       "apihelp-opensearch-param-suggest": "Не прави ништо ако <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> е неточно.",
+       "apihelp-opensearch-param-suggest": "Не прави ништо ако <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> е неточно.",
        "apihelp-opensearch-param-redirects": "Како да се работи со пренасочувања:\n;return: Дај го самото пренасочување.\n;resolve: Дај ја целната страница. Може да даде помалку од $1limit ставки.\nОд историски причини, по основно е „return“ за $1format=json и „resolve“ за други формати.",
        "apihelp-opensearch-param-format": "Формат на изводот.",
        "apihelp-opensearch-example-te": "Најди страници што почнуваат со <kbd>Те</kbd>.",
        "apihelp-options-param-resetkinds": "Сисок на типови можности за повраток кога е зададена можноста <var>$1reset</var>.",
        "apihelp-options-param-change": "Список на промени во форматот name=value (на пр. skin=vector). Вредностите не треба да содржат исправени црти. Ако не зададете вредност (дури ни знак за равенство), на пр., можност|другаможност|..., ќе биде зададена вредноста на можноста по основно.",
        "apihelp-options-param-optionname": "Назив на можноста што треба да ѝ се зададе на вредноста дадена од <var>$1optionvalue</var>.",
-       "apihelp-options-param-optionvalue": "Вредноста на можноста укажана од <var>$1optionname</var>. Може да содржи исправени црти.",
+       "apihelp-options-param-optionvalue": "Вредноста на можноста укажана од <var>$1optionname</var>.",
        "apihelp-options-example-reset": "Врати ги сите поставки по основно",
        "apihelp-options-example-change": "Смени ги поставките <kbd>skin</kbd и <kbd>hideminor</kbd>.",
        "apihelp-options-example-complex": "Врати ги сите нагодувања по основно, а потоа задај ги <kbd>skin</kbd> и <kbd>nickname</kbd>.",
        "apihelp-paraminfo-summary": "Набави информации за извршнички (API) модули.",
-       "apihelp-paraminfo-param-modules": "Список на називи на модули (вредности на параметрите <var>action</var> и <var>format</var>, или пак <kbd>main</kbd>). Може да се укажат подмодули со <kbd>+</kbd>.",
+       "apihelp-paraminfo-param-modules": "Список на називи на модули (вредности на параметрите <var>action</var> и <var>format</var>, или пак <kbd>main</kbd>). Може да се укажат подмодули со <kbd>+</kbd>, или сите подмодули <kbd>+*</kbd>, или сите подмодули рекурзивно со <kbd>+**</kbd>.",
        "apihelp-paraminfo-param-helpformat": "Формат на помошните низи.",
        "apihelp-paraminfo-param-querymodules": "Список на називи на модули за барања (вредност на параметарот <var>prop</var>, <var>meta</var> или <var>list</var>). Користете го <kbd>$1modules=query+foo</kbd> наместо <kbd>$1querymodules=foo</kbd>.",
        "apihelp-paraminfo-param-mainmodule": "Добави информации и за главниот (врховен) модул. Користете го <kbd>$1modules=main</kbd> наместо тоа.",
        "apihelp-paraminfo-param-pagesetmodule": "Дај ги сите информации и за модулот на збирот страници (укажувајќи titles= и сродни).",
        "apihelp-paraminfo-param-formatmodules": "Список на називи на форматни модули (вредностза параметарот <var>format</var>). Наместо тоа, користете го <var>$1modules</var>.",
+       "apihelp-paraminfo-example-1": "Прикажи информации за <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> и <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
+       "apihelp-paraminfo-example-2": "Прикажи информации за сите подмодули на <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.",
        "apihelp-parse-param-summary": "Опис за расчленување.",
        "apihelp-parse-param-preview": "Расчлени во прегледен режим.",
        "apihelp-parse-param-sectionpreview": "Расчлени во прегледен режим на поднасловот (го овозможува и прегледниот режим).",
        "apihelp-patrol-summary": "Испатролирај страница или преработка.",
        "apihelp-patrol-param-rcid": "Назнака на спорешните промени за патролирање.",
        "apihelp-patrol-param-revid": "Назнака на преработката за патролирање.",
+       "apihelp-patrol-param-tags": "Ознаки за примена врз ставката во дневникот на патролирања.",
        "apihelp-patrol-example-rcid": "Испатролирај скорешна промена",
        "apihelp-patrol-example-revid": "Патролирај праработка",
        "apihelp-protect-summary": "Смени го степенот на заштита на страница.",
        "apihelp-protect-param-title": "Наслов на страница што се (од)заштитува. Не може да се користи заедно со $1pageid.",
        "apihelp-protect-param-pageid": "Назнака на страница што се (од)заштитува. Не може да се користи заедно со $1title.",
        "apihelp-protect-param-reason": "Причиина за (од)заштитување",
+       "apihelp-protect-param-tags": "Ознаки за примена врз ставката во дневникот на заштита.",
        "apihelp-protect-example-protect": "Заштити страница",
        "apihelp-purge-param-forcelinkupdate": "Поднови ги табелите со врски.",
        "apihelp-purge-example-simple": "Превчитај ги <kbd>Main Page</kbd> и <kbd>API</kbd>.",
        "apihelp-query+allcategories-param-from": "Од која категорија да почне набројувањето.",
        "apihelp-query+allcategories-param-to": "На која категорија да запре набројувањето.",
        "apihelp-query+allcategories-param-dir": "Насока на подредувањето.",
+       "apihelp-query+allcategories-param-limit": "Колку категории да се дадат.",
        "apihelp-query+allcategories-param-prop": "Кои својства да се дадат:",
        "apihelp-query+alldeletedrevisions-param-from": "Почни го исписот од овој наслов.",
        "apihelp-query+alldeletedrevisions-param-to": "Запри го исписот на овој наслов.",
index cf80ac0..83e8314 100644 (file)
@@ -29,7 +29,8 @@
                        "WhitePhosphorus",
                        "科劳",
                        "SolidBlock",
-                       "神樂坂秀吉"
+                       "神樂坂秀吉",
+                       "94rain"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|文档]]\n* [[mw:Special:MyLanguage/API:FAQ|常见问题]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 邮件列表]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 程序错误与功能请求]\n</div>\n<strong>状态信息:</strong>MediaWiki API是一个成熟稳定的,不断受到支持和改进的界面。尽管我们尽力避免,但偶尔也需要作出重大更新;请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n<strong>错误请求:</strong>当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:Special:MyLanguage/API:Errors_and_warnings|API:错误与警告]]。\n\n<p class=\"mw-apisandbox-link\"><strong>测试中:</strong>测试API请求的易用性,请参见[[Special:ApiSandbox]]。</p>",
        "apihelp-upload-param-comment": "上传注释。如果没有指定<var>$1text</var>,那么它也被用于新文件的初始页面文本。",
        "apihelp-upload-param-tags": "更改标签以应用于上传日志记录和文件页面修订中。",
        "apihelp-upload-param-text": "用于新文件的初始页面文本。",
-       "apihelp-upload-param-watch": "关注页面。",
+       "apihelp-upload-param-watch": "监视页面。",
        "apihelp-upload-param-watchlist": "无条件地将页面加入至当前用户的监视列表或将其移除,使用设置或不更改监视。",
        "apihelp-upload-param-ignorewarnings": "忽略任何警告。",
        "apihelp-upload-param-file": "文件内容。",
index df09ead..4fa4cfe 100644 (file)
@@ -66,7 +66,7 @@ class BlockRestrictionStore {
                        return [];
                }
 
-               $db = $db ?: $this->loadBalancer->getConnection( DB_REPLICA );
+               $db = $db ?: $this->loadBalancer->getConnectionRef( DB_REPLICA );
 
                $result = $db->select(
                        [ 'ipblocks_restrictions', 'page' ],
@@ -104,7 +104,7 @@ class BlockRestrictionStore {
                        return false;
                }
 
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
 
                $dbw->insert(
                        'ipblocks_restrictions',
@@ -125,7 +125,7 @@ class BlockRestrictionStore {
         * @return bool
         */
        public function update( array $restrictions ) {
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
 
                $dbw->startAtomic( __METHOD__ );
 
@@ -197,7 +197,7 @@ class BlockRestrictionStore {
 
                $parentBlockId = (int)$parentBlockId;
 
-               $db = $this->loadBalancer->getConnection( DB_MASTER );
+               $db = $this->loadBalancer->getConnectionRef( DB_MASTER );
 
                $db->startAtomic( __METHOD__ );
 
@@ -230,7 +230,7 @@ class BlockRestrictionStore {
         * @return bool
         */
        public function delete( array $restrictions ) {
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
                $result = true;
                foreach ( $restrictions as $restriction ) {
                        if ( !$restriction instanceof Restriction ) {
@@ -260,7 +260,7 @@ class BlockRestrictionStore {
         * @return bool
         */
        public function deleteByBlockId( $blockId ) {
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
                return $dbw->delete(
                        'ipblocks_restrictions',
                        [ 'ir_ipb_id' => $blockId ],
@@ -277,7 +277,7 @@ class BlockRestrictionStore {
         * @return bool
         */
        public function deleteByParentBlockId( $parentBlockId ) {
-               $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
                return $dbw->deleteJoin(
                        'ipblocks_restrictions',
                        'ipblocks',
index 1bcf948..8018117 100644 (file)
@@ -306,7 +306,7 @@ class LinkCache {
 
        private function isCacheable( LinkTarget $title ) {
                $ns = $title->getNamespace();
-               if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY ] ) ) {
+               if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
                        return true;
                }
                // Focus on transcluded pages more than the main content
index bb84f97..c4a7e89 100644 (file)
@@ -22,6 +22,7 @@
 
 use CLDRPluralRuleParser\Evaluator;
 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
+use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
 
 /**
@@ -69,6 +70,11 @@ class LocalisationCache {
         */
        private $store;
 
+       /**
+        * @var \Psr\Log\LoggerInterface
+        */
+       private $logger;
+
        /**
         * A 2-d associative array, code/key, where presence indicates that the item
         * is loaded. Value arbitrary.
@@ -193,6 +199,7 @@ class LocalisationCache {
                global $wgCacheDirectory;
 
                $this->conf = $conf;
+               $this->logger = LoggerFactory::getInstance( 'localisation' );
 
                $directory = !empty( $conf['storeDirectory'] ) ? $conf['storeDirectory'] : $wgCacheDirectory;
                $storeArg = [];
@@ -227,8 +234,7 @@ class LocalisationCache {
                                        );
                        }
                }
-
-               wfDebugLog( 'caches', static::class . ": using store $storeClass" );
+               $this->logger->debug( static::class . ": using store $storeClass" );
 
                $this->store = new $storeClass( $storeArg );
                foreach ( [ 'manualRecache', 'forceRecache' ] as $var ) {
@@ -401,7 +407,7 @@ class LocalisationCache {
         */
        public function isExpired( $code ) {
                if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
-                       wfDebug( __METHOD__ . "($code): forced reload\n" );
+                       $this->logger->debug( __METHOD__ . "($code): forced reload\n" );
 
                        return true;
                }
@@ -411,7 +417,7 @@ class LocalisationCache {
                $preload = $this->store->get( $code, 'preload' );
                // Different keys may expire separately for some stores
                if ( $deps === null || $keys === null || $preload === null ) {
-                       wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
+                       $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one\n" );
 
                        return true;
                }
@@ -422,7 +428,7 @@ class LocalisationCache {
                        // anymore (e.g. uninstalled extensions)
                        // When this happens, always expire the cache
                        if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
-                               wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
+                               $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
                                        get_class( $dep ) . "\n" );
 
                                return true;
@@ -590,7 +596,7 @@ class LocalisationCache {
                try {
                        $compiledRules = Evaluator::compile( $rules );
                } catch ( CLDRPluralRuleError $e ) {
-                       wfDebugLog( 'l10n', $e->getMessage() );
+                       $this->logger->debug( $e->getMessage() );
 
                        return [];
                }
@@ -830,10 +836,10 @@ class LocalisationCache {
                # Load the primary localisation from the source file
                $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
                if ( $data === false ) {
-                       wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
+                       $this->logger->debug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
                        $coreData['fallback'] = 'en';
                } else {
-                       wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
+                       $this->logger->debug( __METHOD__ . ": got localisation for $code from source\n" );
 
                        # Merge primary localisation
                        foreach ( $data as $key => $value ) {
index a123d00..501f01a 100644 (file)
@@ -62,7 +62,7 @@ class DatabaseOracle extends Database {
         * @param array $params Additional parameters include:
         *   - keywordTableMap : Map of reserved table names to alternative table names to use
         */
-       function __construct( array $params ) {
+       public function __construct( array $params ) {
                $this->keywordTableMap = $params['keywordTableMap'] ?? [];
                $params['tablePrefix'] = strtoupper( $params['tablePrefix'] );
                parent::__construct( $params );
@@ -97,6 +97,15 @@ class DatabaseOracle extends Database {
                                        "and database)\n" );
                }
 
+               if ( $schema !== null ) {
+                       // We use the *database* aspect of $domain for schema, not the domain schema
+                       throw new DBExpectedError(
+                               $this,
+                               __CLASS__ . ": cannot use schema '$schema'; " .
+                               "the database component '$dbName' is actually interpreted as the Oracle schema."
+                       );
+               }
+
                $this->close();
                $this->user = $user;
                $this->password = $password;
@@ -1028,7 +1037,11 @@ class DatabaseOracle extends Database {
        protected function doSelectDomain( DatabaseDomain $domain ) {
                if ( $domain->getSchema() !== null ) {
                        // We use the *database* aspect of $domain for schema, not the domain schema
-                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+                       throw new DBExpectedError(
+                               $this,
+                               __CLASS__ . ": domain '{$domain->getId()}' has a schema component; " .
+                               "the database component is actually interpreted as the Oracle schema."
+                       );
                }
 
                $database = $domain->getDatabase();
index f5d22c1..c754cff 100644 (file)
  *
  * @file
  */
+
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
+use MediaWiki\Logger\LoggerFactory;
+use Psr\Log\LoggerInterface;
 use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\LBFactory;
@@ -181,11 +185,14 @@ class DeferredUpdates {
        protected static function handleUpdateQueue( array &$queue, $mode, $stage ) {
                $services = MediaWikiServices::getInstance();
                $stats = $services->getStatsdDataFactory();
-               $lbFactory = $services->getDBLoadBalancerFactory();
-               $method = RequestContext::getMain()->getRequest()->getMethod();
-
-               /** @var ErrorPageError $reportableError */
-               $reportableError = null;
+               $lbf = $services->getDBLoadBalancerFactory();
+               $logger = LoggerFactory::getInstance( 'DeferredUpdates' );
+               $httpMethod = $services->getMainConfig()->get( 'CommandLineMode' )
+                       ? 'cli'
+                       : strtolower( RequestContext::getMain()->getRequest()->getMethod() );
+
+               /** @var ErrorPageError $guiEx */
+               $guiEx = null;
                /** @var DeferrableUpdate[] $updates Snapshot of queue */
                $updates = $queue;
 
@@ -193,37 +200,37 @@ class DeferredUpdates {
                while ( $updates ) {
                        $queue = []; // clear the queue
 
-                       // Order will be DataUpdate followed by generic DeferrableUpdate tasks
-                       $updatesByType = [ 'data' => [], 'generic' => [] ];
-                       foreach ( $updates as $du ) {
-                               if ( $du instanceof DataUpdate ) {
-                                       $updatesByType['data'][] = $du;
+                       // Segregate the queue into one for DataUpdate and one for everything else
+                       $dataUpdateQueue = [];
+                       $genericUpdateQueue = [];
+                       foreach ( $updates as $update ) {
+                               if ( $update instanceof DataUpdate ) {
+                                       $dataUpdateQueue[] = $update;
                                } else {
-                                       $updatesByType['generic'][] = $du;
+                                       $genericUpdateQueue[] = $update;
                                }
-
-                               $name = ( $du instanceof DeferrableCallback )
-                                       ? get_class( $du ) . '-' . $du->getOrigin()
-                                       : get_class( $du );
-                               $stats->increment( 'deferred_updates.' . $method . '.' . $name );
                        }
-
-                       // Execute all remaining tasks...
-                       foreach ( $updatesByType as $updatesForType ) {
-                               foreach ( $updatesForType as $update ) {
+                       // Execute all DataUpdate queue followed by the DeferrableUpdate queue...
+                       foreach ( [ $dataUpdateQueue, $genericUpdateQueue ] as $updateQueue ) {
+                               foreach ( $updateQueue as $du ) {
+                                       // Enqueue the task into the job queue system instead if applicable
+                                       if ( $mode === 'enqueue' && $du instanceof EnqueueableDataUpdate ) {
+                                               self::jobify( $du, $lbf, $logger, $stats, $httpMethod );
+                                               continue;
+                                       }
+                                       // Otherwise, execute the task and any subtasks that it spawns
                                        self::$executeContext = [ 'stage' => $stage, 'subqueue' => [] ];
                                        try {
-                                               /** @var DeferrableUpdate $update */
-                                               $guiError = self::handleUpdate( $update, $lbFactory, $mode, $stage );
-                                               $reportableError = $reportableError ?: $guiError;
+                                               $e = self::run( $du, $lbf, $logger, $stats, $httpMethod );
+                                               $guiEx = $guiEx ?: ( $e instanceof ErrorPageError ? $e : null );
                                                // Do the subqueue updates for $update until there are none
                                                while ( self::$executeContext['subqueue'] ) {
-                                                       $subUpdate = reset( self::$executeContext['subqueue'] );
+                                                       $duChild = reset( self::$executeContext['subqueue'] );
                                                        $firstKey = key( self::$executeContext['subqueue'] );
                                                        unset( self::$executeContext['subqueue'][$firstKey] );
 
-                                                       $guiError = self::handleUpdate( $subUpdate, $lbFactory, $mode, $stage );
-                                                       $reportableError = $reportableError ?: $guiError;
+                                                       $e = self::run( $duChild, $lbf, $logger, $stats, $httpMethod );
+                                                       $guiEx = $guiEx ?: ( $e instanceof ErrorPageError ? $e : null );
                                                }
                                        } finally {
                                                // Make sure we always clean up the context.
@@ -237,40 +244,53 @@ class DeferredUpdates {
                        $updates = $queue; // new snapshot of queue (check for new entries)
                }
 
-               if ( $reportableError ) {
-                       throw $reportableError; // throw the first of any GUI errors
+               // Throw the first of any GUI errors as long as the context is HTTP pre-send. However,
+               // callers should check permissions *before* enqueueing updates. If the main transaction
+               // round actions succeed but some deferred updates fail due to permissions errors then
+               // there is a risk that some secondary data was not properly updated.
+               if ( $guiEx && $stage === self::PRESEND && !headers_sent() ) {
+                       throw $guiEx;
                }
        }
 
        /**
-        * Run or enqueue an update
+        * Run a task and catch/log any exceptions
         *
         * @param DeferrableUpdate $update
         * @param LBFactory $lbFactory
-        * @param string $mode
-        * @param int $stage
-        * @return ErrorPageError|null
+        * @param LoggerInterface $logger
+        * @param StatsdDataFactoryInterface $stats
+        * @param string $httpMethod
+        * @return Exception|Throwable|null
         */
-       private static function handleUpdate(
-               DeferrableUpdate $update, LBFactory $lbFactory, $mode, $stage
+       private static function run(
+               DeferrableUpdate $update,
+               LBFactory $lbFactory,
+               LoggerInterface $logger,
+               StatsdDataFactoryInterface $stats,
+               $httpMethod
        ) {
-               $guiError = null;
+               $name = get_class( $update );
+               $suffix = ( $update instanceof DeferrableCallback ) ? "_{$update->getOrigin()}" : '';
+               $stats->increment( "deferred_updates.$httpMethod.{$name}{$suffix}" );
+
+               $e = null;
                try {
-                       if ( $mode === 'enqueue' && $update instanceof EnqueueableDataUpdate ) {
-                               // Run only the job enqueue logic to complete the update later
-                               $spec = $update->getAsJobSpecification();
-                               $domain = $spec['domain'] ?? $spec['wiki'];
-                               JobQueueGroup::singleton( $domain )->push( $spec['job'] );
-                       } else {
-                               self::attemptUpdate( $update, $lbFactory );
-                       }
+                       self::attemptUpdate( $update, $lbFactory );
                } catch ( Exception $e ) {
-                       // Reporting GUI exceptions does not work post-send
-                       if ( $e instanceof ErrorPageError && $stage === self::PRESEND ) {
-                               $guiError = $e;
-                       }
-                       $lbFactory->rollbackMasterChanges( __METHOD__ );
+               } catch ( Throwable $e ) {
+               }
 
+               if ( $e ) {
+                       $logger->error(
+                               "Deferred update {type} failed: {message}",
+                               [
+                                       'type' => $name . $suffix,
+                                       'message' => $e->getMessage(),
+                                       'trace' => $e->getTraceAsString()
+                               ]
+                       );
+                       $lbFactory->rollbackMasterChanges( __METHOD__ );
                        // VW-style hack to work around T190178, so we can make sure
                        // PageMetaDataUpdater doesn't throw exceptions.
                        if ( defined( 'MW_PHPUNIT_TEST' ) ) {
@@ -278,7 +298,46 @@ class DeferredUpdates {
                        }
                }
 
-               return $guiError;
+               return $e;
+       }
+
+       /**
+        * Push a task into the job queue system and catch/log any exceptions
+        *
+        * @param EnqueueableDataUpdate $update
+        * @param LBFactory $lbFactory
+        * @param LoggerInterface $logger
+        * @param StatsdDataFactoryInterface $stats
+        * @param string $httpMethod
+        */
+       private static function jobify(
+               EnqueueableDataUpdate $update,
+               LBFactory $lbFactory,
+               LoggerInterface $logger,
+               StatsdDataFactoryInterface $stats,
+               $httpMethod
+       ) {
+               $stats->increment( "deferred_updates.$httpMethod." . get_class( $update ) );
+
+               $e = null;
+               try {
+                       $spec = $update->getAsJobSpecification();
+                       JobQueueGroup::singleton( $spec['domain'] ?? $spec['wiki'] )->push( $spec['job'] );
+               } catch ( Exception $e ) {
+               } catch ( Throwable $e ) {
+               }
+
+               if ( $e ) {
+                       $logger->error(
+                               "Job insertion of deferred update {type} failed: {message}",
+                               [
+                                       'type' => get_class( $update ),
+                                       'message' => $e->getMessage(),
+                                       'trace' => $e->getTraceAsString()
+                               ]
+                       );
+                       $lbFactory->rollbackMasterChanges( __METHOD__ );
+               }
        }
 
        /**
@@ -301,12 +360,18 @@ class DeferredUpdates {
                        $update instanceof TransactionRoundAwareUpdate &&
                        $update->getTransactionRoundRequirement() == $update::TRX_ROUND_ABSENT
                ) {
-                       $update->doUpdate();
+                       $fnameTrxOwner = null;
                } else {
-                       // Run the bulk of the update now
                        $fnameTrxOwner = get_class( $update ) . '::doUpdate';
+               }
+
+               if ( $fnameTrxOwner !== null ) {
                        $lbFactory->beginMasterChanges( $fnameTrxOwner );
-                       $update->doUpdate();
+               }
+
+               $update->doUpdate();
+
+               if ( $fnameTrxOwner !== null ) {
                        $lbFactory->commitMasterChanges( $fnameTrxOwner );
                }
        }
index 266d768..603e49c 100644 (file)
@@ -203,7 +203,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
        }
 
        /**
-        * Acquire a lock for performing link table updates for a page on a DB
+        * Acquire a session-level lock for performing link table updates for a page on a DB
         *
         * @param IDatabase $dbw
         * @param int $pageId
@@ -212,7 +212,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
         * @since 1.27
         */
        public static function acquirePageLock( IDatabase $dbw, $pageId, $why = 'atomicity' ) {
-               $key = "LinksUpdate:$why:pageid:$pageId";
+               $key = "{$dbw->getDomainID()}:LinksUpdate:$why:pageid:$pageId"; // per-wiki
                $scopedLock = $dbw->getScopedLockAndFlush( $key, __METHOD__, 15 );
                if ( !$scopedLock ) {
                        $logger = LoggerFactory::getInstance( 'SecondaryDataUpdate' );
index 7cb2950..11e9337 100644 (file)
@@ -25,8 +25,6 @@ use Wikimedia\Rdbms\IDatabase;
  * Class for handling updates to the site_stats table
  */
 class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
-       /** @var BagOStuff */
-       protected $stash;
        /** @var int */
        protected $edits = 0;
        /** @var int */
@@ -38,7 +36,14 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
        /** @var int */
        protected $images = 0;
 
-       private static $counters = [ 'edits', 'pages', 'articles', 'users', 'images' ];
+       /** @var string[] Map of (table column => counter type) */
+       private static $counters = [
+               'ss_total_edits'   => 'edits',
+               'ss_total_pages'   => 'pages',
+               'ss_good_articles' => 'articles',
+               'ss_users'         => 'users',
+               'ss_images'        => 'images'
+       ];
 
        // @todo deprecate this constructor
        function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) {
@@ -46,8 +51,6 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
                $this->articles = $good;
                $this->pages = $pages;
                $this->users = $users;
-
-               $this->stash = MediaWikiServices::getInstance()->getMainObjectStash();
        }
 
        public function merge( MergeableUpdate $update ) {
@@ -60,8 +63,9 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
        }
 
        /**
-        * @param array $deltas
+        * @param int[] $deltas Map of (counter type => integer delta)
         * @return SiteStatsUpdate
+        * @throws UnexpectedValueException
         */
        public static function factory( array $deltas ) {
                $update = new self( 0, 0, 0 );
@@ -73,73 +77,46 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
                }
 
                foreach ( self::$counters as $field ) {
-                       if ( isset( $deltas[$field] ) && $deltas[$field] ) {
-                               $update->$field = $deltas[$field];
-                       }
+                       $update->$field = $deltas[$field] ?? 0;
                }
 
                return $update;
        }
 
        public function doUpdate() {
-               $this->doUpdateContextStats();
-
-               $rate = MediaWikiServices::getInstance()->getMainConfig()->get( 'SiteStatsAsyncFactor' );
-               // If set to do so, only do actual DB updates 1 every $rate times.
-               // The other times, just update "pending delta" values in memcached.
-               if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) {
-                       $this->doUpdatePendingDeltas();
-               } else {
-                       // Need a separate transaction because this a global lock
-                       DeferredUpdates::addCallableUpdate( [ $this, 'tryDBUpdateInternal' ] );
-               }
-       }
-
-       /**
-        * Do not call this outside of SiteStatsUpdate
-        */
-       public function tryDBUpdateInternal() {
                $services = MediaWikiServices::getInstance();
-               $config = $services->getMainConfig();
-
-               $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
-               $lockKey = $dbw->getDomainID() . ':site_stats'; // prepend wiki ID
-               $pd = [];
-               if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
-                       // Lock the table so we don't have double DB/memcached updates
-                       if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) {
-                               $this->doUpdatePendingDeltas();
+               $stats = $services->getStatsdDataFactory();
 
-                               return;
+               $deltaByType = [];
+               foreach ( self::$counters as $type ) {
+                       $delta = $this->$type;
+                       if ( $delta !== 0 ) {
+                               $stats->updateCount( "site.$type", $delta );
                        }
-                       $pd = $this->getPendingDeltas();
-                       // Piggy-back the async deltas onto those of this stats update....
-                       $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] );
-                       $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] );
-                       $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] );
-                       $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] );
-                       $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] );
-               }
-
-               // Build up an SQL query of deltas and apply them...
-               $updates = '';
-               $this->appendUpdate( $updates, 'ss_total_edits', $this->edits );
-               $this->appendUpdate( $updates, 'ss_good_articles', $this->articles );
-               $this->appendUpdate( $updates, 'ss_total_pages', $this->pages );
-               $this->appendUpdate( $updates, 'ss_users', $this->users );
-               $this->appendUpdate( $updates, 'ss_images', $this->images );
-               if ( $updates != '' ) {
-                       $dbw->update( 'site_stats', [ $updates ], [], __METHOD__ );
+                       $deltaByType[$type] = $delta;
                }
 
-               if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
-                       // Decrement the async deltas now that we applied them
-                       $this->removePendingDeltas( $pd );
-                       // Commit the updates and unlock the table
-                       $dbw->unlock( $lockKey, __METHOD__ );
-               }
+               ( new AutoCommitUpdate(
+                       $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER ),
+                       __METHOD__,
+                       function ( IDatabase $dbw, $fname ) use ( $deltaByType ) {
+                               $set = [];
+                               foreach ( self::$counters as $column => $type ) {
+                                       $delta = (int)$deltaByType[$type];
+                                       if ( $delta > 0 ) {
+                                               $set[] = "$column=$column+" . abs( $delta );
+                                       } elseif ( $delta < 0 ) {
+                                               $set[] = "$column=$column-" . abs( $delta );
+                                       }
+                               }
+
+                               if ( $set ) {
+                                       $dbw->update( 'site_stats', $set, [ 'ss_row_id' => 1 ], $fname );
+                               }
+                       }
+               ) )->doUpdate();
 
-               // Invalid cache used by parser functions
+               // Invalidate cache used by parser functions
                SiteStats::unload();
        }
 
@@ -151,7 +128,7 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
                $services = MediaWikiServices::getInstance();
                $config = $services->getMainConfig();
 
-               $dbr = $services->getDBLoadBalancer()->getConnection( DB_REPLICA, 'vslow' );
+               $dbr = $services->getDBLoadBalancer()->getConnectionRef( DB_REPLICA, 'vslow' );
                # Get non-bot users than did some recent action other than making accounts.
                # If account creation is included, the number gets inflated ~20+ fold on enwiki.
                $rcQuery = RecentChange::getQueryInfo();
@@ -182,105 +159,4 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
 
                return $activeUsers;
        }
-
-       protected function doUpdateContextStats() {
-               $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
-               foreach ( [ 'edits', 'articles', 'pages', 'users', 'images' ] as $type ) {
-                       $delta = $this->$type;
-                       if ( $delta !== 0 ) {
-                               $stats->updateCount( "site.$type", $delta );
-                       }
-               }
-       }
-
-       protected function doUpdatePendingDeltas() {
-               $this->adjustPending( 'ss_total_edits', $this->edits );
-               $this->adjustPending( 'ss_good_articles', $this->articles );
-               $this->adjustPending( 'ss_total_pages', $this->pages );
-               $this->adjustPending( 'ss_users', $this->users );
-               $this->adjustPending( 'ss_images', $this->images );
-       }
-
-       /**
-        * @param string &$sql
-        * @param string $field
-        * @param int $delta
-        */
-       protected function appendUpdate( &$sql, $field, $delta ) {
-               if ( $delta ) {
-                       if ( $sql ) {
-                               $sql .= ',';
-                       }
-                       if ( $delta < 0 ) {
-                               $sql .= "$field=$field-" . abs( $delta );
-                       } else {
-                               $sql .= "$field=$field+" . abs( $delta );
-                       }
-               }
-       }
-
-       /**
-        * @param BagOStuff $stash
-        * @param string $type
-        * @param string $sign ('+' or '-')
-        * @return string
-        */
-       private function getTypeCacheKey( BagOStuff $stash, $type, $sign ) {
-               return $stash->makeKey( 'sitestatsupdate', 'pendingdelta', $type, $sign );
-       }
-
-       /**
-        * Adjust the pending deltas for a stat type.
-        * Each stat type has two pending counters, one for increments and decrements
-        * @param string $type
-        * @param int $delta Delta (positive or negative)
-        */
-       protected function adjustPending( $type, $delta ) {
-               if ( $delta < 0 ) { // decrement
-                       $key = $this->getTypeCacheKey( $this->stash, $type, '-' );
-               } else { // increment
-                       $key = $this->getTypeCacheKey( $this->stash, $type, '+' );
-               }
-
-               $magnitude = abs( $delta );
-               $this->stash->incrWithInit( $key, 0, $magnitude, $magnitude );
-       }
-
-       /**
-        * Get pending delta counters for each stat type
-        * @return array Positive and negative deltas for each type
-        */
-       protected function getPendingDeltas() {
-               $pending = [];
-               foreach ( [ 'ss_total_edits',
-                       'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ] as $type
-               ) {
-                       // Get pending increments and pending decrements
-                       $flg = BagOStuff::READ_LATEST;
-                       $pending[$type]['+'] = (int)$this->stash->get(
-                               $this->getTypeCacheKey( $this->stash, $type, '+' ),
-                               $flg
-                       );
-                       $pending[$type]['-'] = (int)$this->stash->get(
-                               $this->getTypeCacheKey( $this->stash, $type, '-' ),
-                               $flg
-                       );
-               }
-
-               return $pending;
-       }
-
-       /**
-        * Reduce pending delta counters after updates have been applied
-        * @param array $pd Result of getPendingDeltas(), used for DB update
-        */
-       protected function removePendingDeltas( array $pd ) {
-               foreach ( $pd as $type => $deltas ) {
-                       foreach ( $deltas as $sign => $magnitude ) {
-                               // Lower the pending counter now that we applied these changes
-                               $key = $this->getTypeCacheKey( $this->stash, $type, $sign );
-                               $this->stash->decr( $key, $magnitude );
-                       }
-               }
-       }
 }
index ed7e00c..687dfbe 100644 (file)
@@ -67,7 +67,7 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate {
         */
        public function doUpdate() {
                $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-               $dbw = $lb->getConnection( DB_MASTER );
+               $dbw = $lb->getConnectionRef( DB_MASTER );
                $fname = __METHOD__;
 
                ( new AutoCommitUpdate( $dbw, __METHOD__, function () use ( $lb, $dbw, $fname ) {
@@ -85,8 +85,8 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate {
                                        // The user_editcount is probably NULL (e.g. not initialized).
                                        // Since this update runs after the new revisions were committed,
                                        // wait for the replica DB to catch up so they will be counted.
-                                       $dbr = $lb->getConnection( DB_REPLICA );
-                                       // If $dbr is actually the master DB, then clearing the snapshot is
+                                       $dbr = $lb->getConnectionRef( DB_REPLICA );
+                                       // If $dbr is actually the master DB, then clearing the snapshot
                                        // is harmless and waitForMasterPos() will just no-op.
                                        $dbr->flushSnapshot( $fname );
                                        $lb->waitForMasterPos( $dbr );
index f6ca2f5..7824872 100644 (file)
@@ -142,5 +142,6 @@ if ( false ) {
        // autoload entries for the lowercase variants of these classes (T166759).
        // The code below is never executed, but it is picked up by the AutoloadGenerator
        // parser, which scans for class_alias() calls.
+       // @phan-suppress-next-line PhanRedefineClassAlias
        class_alias( ConcatenatedGzipHistoryBlob::class, 'concatenatedgziphistoryblob' );
 }
index 8858c8d..cd2a935 100644 (file)
@@ -69,5 +69,6 @@ if ( false ) {
        // autoload entries for the lowercase variants of these classes (T166759).
        // The code below is never executed, but it is picked up by the AutoloadGenerator
        // parser, which scans for class_alias() calls.
+       // @phan-suppress-next-line PhanRedefineClassAlias
        class_alias( HistoryBlobCurStub::class, 'historyblobcurstub' );
 }
index 9a4df1f..c92e1b5 100644 (file)
@@ -149,5 +149,6 @@ if ( false ) {
        // autoload entries for the lowercase variants of these classes (T166759).
        // The code below is never executed, but it is picked up by the AutoloadGenerator
        // parser, which scans for class_alias() calls.
+       // @phan-suppress-next-line PhanRedefineClassAlias
        class_alias( HistoryBlobStub::class, 'historyblobstub' );
 }
index 17332ff..cf91ccd 100644 (file)
@@ -406,6 +406,7 @@ EOT;
                'type' => 'sqlite',
                'dbname' => \"{\$wgDBname}_jobqueue\",
                'tablePrefix' => '',
+               'variables' => [ 'synchronous' => 'NORMAL' ],
                'dbDirectory' => \$wgSQLiteDataDir,
                'trxMode' => 'IMMEDIATE',
                'flags' => 0
index 42e7db0..136f3a2 100644 (file)
        "config-welcome": "== Праверка асяродзьдзя ==\nЗараз будуць праведзеныя праверкі для запэўніваньня, што гэтае асяродзьдзе адпаведнае для ўсталяваньня MediaWiki.\nНе забудзьце далучыць гэтую інфармацыю, калі вам спатрэбіцца дапамога для завяршэньня ўсталяваньня.",
        "config-copyright": "== Аўтарскае права і ўмовы ==\n\n$1\n\nThis 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.\n\nThis 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'''.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. or [https://www.gnu.org/copyleft/gpl.html read it online].",
        "config-sidebar": "* [https://www.mediawiki.org Хатняя старонка MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Даведка для ўдзельнікаў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Даведка для адміністратараў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Адказы на частыя пытаньні]",
+       "config-sidebar-readme": "Прачытай мяне",
+       "config-sidebar-relnotes": "Заўвагі да выпуску",
+       "config-sidebar-license": "Капіяваньне",
+       "config-sidebar-upgrade": "Абнаўленьне",
        "config-env-good": "Асяродзьдзе было праверанае.\nВы можаце ўсталёўваць MediaWiki.",
        "config-env-bad": "Асяродзьдзе было праверанае.\nУсталяваньне MediaWiki немагчымае.",
        "config-env-php": "Усталяваны PHP $1.",
index 946955e..3798468 100644 (file)
@@ -44,6 +44,7 @@
        "config-page-existingwiki": "Eksisterende wiki",
        "config-help-restart": "Vil du rydde alle gemte data, du har indtastet og genstarte installationen?",
        "config-restart": "Ja, genstart den",
+       "config-sidebar-upgrade": "Opgraderer",
        "config-env-php": "PHP $1 er installeret.",
        "config-env-hhvm": "HHVM $1 er installeret.",
        "config-apc": "[https://www.php.net/apc APC] er installeret",
index daa4de9..a36635d 100644 (file)
@@ -17,7 +17,8 @@
                        "Alifakoor",
                        "Seb35",
                        "Ahmad252",
-                       "FarsiNevis"
+                       "FarsiNevis",
+                       "กิ๊ฟ เลิกล่ะ สายแข็ง"
                ]
        },
        "config-desc": "نصب‌کنندهٔ مدیاویکی",
@@ -58,7 +59,7 @@
        "config-restart": "بله، دوباره شروع کن",
        "config-welcome": "===بررسی‌های محیطی===\nبرای فهمیدن اینکه این محیط برای نصب مدیاویکی مناسب است، اکنون بررسی‌های اساسی انجام خواهد‌شد.\nاگر به دنبال پشتیبانی در چگونگی تکمیل نصب هستید،به یاد داشته باشید این اطلاعات را بگنجانید.",
        "config-copyright": "=== حق رونوشت و شرایط ===\n\n$1\n\nاین برنامه، نرم‌افزاری آزاد است. می‌توانید تحت شرایط نگارش ۲ یا (بنا به نظر خود) هر نگارش جدیدتری از پروانهٔ جامع همگانی گنو که توسط بنیاد نرم‌افزار آزاد منتشر شده، بازنشرش کرده و/یا تغییرش دهید.\n\n\nاین برنامه با این امید توزیع شده که مفید باشد، ولی <strong>بدون هیچ ضمانتی</strong>، حتا ضمانت ضمنی <strong>معامله‌پذیری</strong> یا <strong>تناسب برای کاربردی خاص </strong>.\n\nبرای جزئیات بیشتر، پروانهٔ جامع همگانی گنو را ببینید.\n\n\nباید همراه این برنامه، <doclink href=Copying>نگارشی از پروانهٔ جامع همگانی گنو</doclink> را گرفته باشید. اگر چنین نیست، با بنیاد نرم‌افزار آزاد به نشانی 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA مکاتبه کرده یا [https://www.gnu.org/copyleft/gpl.html پروانه را برخط بخوانید].",
-       "config-sidebar": "* [https://www.mediawiki.org صفحهٔ اصلی مدیاویکی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents راهنمای کاربر]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents راهنمای مدیر]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ پرسش‌های رایج]\n----\n* <doclink href=Readme>مرا بخوان</doclink>\n* <doclink href=ReleaseNotes>یادداشت‌های انتشار</doclink>\n* <doclink href=Copying>نسخه برداری</doclink>\n* <doclink href=UpgradeDoc>ارتقا</doclink>",
+       "config-sidebar": "* [//www.mediawiki.org صفحهٔ اصلی مدیاویکی]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents راهنمای کاربر]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents راهنمای مدیر]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ پرسش‌های رایج]\n----\n* <doclink href=Readme>مرا بخوان</doclink>\n* <doclink href=ReleaseNotes>یادداشت‌های انتشار</doclink>\n* <doclink href=Copying>نسخه برداری</doclink>\n* <doclink href=UpgradeDoc>ارتقا</doclink>",
        "config-env-good": "محیط بررسی شده‌است.\nشما می‌توانید مدیاویکی را نصب کنید.",
        "config-env-bad": "محیط بررسی شده‌است.\nشما نمی‌توانید مدیاویکی را نصب کنید.",
        "config-env-php": "پی‌اچ‌پی $1 نصب شده‌است.",
index dd9c2ec..ce38338 100644 (file)
@@ -22,7 +22,8 @@
                        "Tosky",
                        "Selven",
                        "Sarah Bernabei",
-                       "ArTrix"
+                       "ArTrix",
+                       "Annibale covini gerolamo"
                ]
        },
        "config-desc": "Programma di installazione per MediaWiki",
        "config-restart": "Sì, riavvia",
        "config-welcome": "=== Controllo dell'ambiente ===\nSaranno eseguiti controlli di base per vedere se questo ambiente è adatto per l'installazione di MediaWiki.\nRicordati di includere queste informazioni se chiedi assistenza su come completare l'installazione.",
        "config-copyright": "=== Copyright e termini ===\n\n$1\n\nQuesto programma è un software libero; puoi redistribuirlo e/o modificarlo secondo i termini della GNU General Public License, come pubblicata dalla Free Software Foundation; o la versione 2 della Licenza o (a propria scelta) qualunque versione successiva.\n\nQuesto programma è distribuito nella speranza che sia utile, ma SENZA ALCUNA GARANZIA; senza neppure la garanzia implicita di NEGOZIABILITÀ o di APPLICABILITÀ PER UN PARTICOLARE SCOPO.\nSi veda la GNU General Public License per maggiori dettagli.\n\nQuesto programma deve essere distribuito assieme ad <doclink href=Copying>una copia della GNU General Public License</doclink>; in caso contrario, se ne può ottenere una scrivendo alla Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA oppure [https://www.gnu.org/copyleft/gpl.html leggerla in rete].",
-       "config-sidebar": "* [https://www.mediawiki.org Pagina principale MediaWiki]\n* [https://www.mediawiki.org/wiki/Aiuto:Guida ai contenuti per utenti]\n* [https://www.mediawiki.org/wiki/Manuale:Guida ai contenuti per admin]\n* [https://www.mediawiki.org/wiki/Manuale:FAQ FAQ]\n----\n* <doclink href=Readme>Leggimi</doclink>\n* <doclink href=ReleaseNotes>Note di versione</doclink>\n* <doclink href=Copying>Copie</doclink>\n* <doclink href=UpgradeDoc>Aggiornamenti</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org Pagina principale MediaWiki]\n* [https://www.mediawiki.org/Special:MyLanguage/Help:Contents Guida ai contenuti per utenti]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:Contents Guida ai contenuti per admin]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:FAQ FAQ]",
+       "config-sidebar-readme": "Leggimi",
+       "config-sidebar-relnotes": "Note di versione",
+       "config-sidebar-license": "Licenza",
+       "config-sidebar-upgrade": "Aggiornamento",
        "config-env-good": "L'ambiente è stato controllato.\nÈ possibile installare MediaWiki.",
        "config-env-bad": "L'ambiente è stato controllato.\nNon è possibile installare MediaWiki.",
        "config-env-php": "PHP $1 è installato.",
index 450fa8b..64cce74 100644 (file)
        "config-restart": "Да, почни одново",
        "config-welcome": "=== Проверки на околината ===\nСега ќе се извршиме основни проверки за да се востанови дали околината е погодна за воспоставкa на МедијаВики. Не заборавајте да ги приложите овие информации ако барате помош со довршување на воспоставката.",
        "config-copyright": "=== Авторски права и услови ===\n\n$1\n\nОва е слободна програмска опрема (free software); можете да го редистрибуирате и/или менувате согласно условите на ГНУ-овата општа јавна лиценца (GNU General Public License) на Фондацијата за слободна програмска опрема (Free Software Foundation); верзија 2 или било која понова верзија на лиценцата (по ваш избор).\n\nОвој програм се нуди со надеж дека ќе биде корисен, но '''без никаква гаранција'''; дури ни подразбраната гаранција за '''продажна способност''' или '''погодност за определена цел'''.\nПовеќе информации ќе најдете во текстот на ГНУ-овата општа јавна лиценца.\n\nБи требало да имате добиено <doclink href=Copying>примерок од ГНУ-овата општа јавна лиценца</doclink> заедно со програмов; ако немате добиено, тогаш пишете ни на Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. или [https://www.gnu.org/copyleft/gpl.html прочитајте ја тука].",
-       "config-sidebar": "* [https://www.mediawiki.org Домашна страница на МедијаВики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Водич за корисници]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Водич за администратори]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ЧПП]\n----\n* <doclink href=Readme>Прочитај ме</doclink>\n* <doclink href=ReleaseNotes>Белешки за изданието</doclink>\n* <doclink href=Copying>Копирање</doclink>\n* <doclink href=UpgradeDoc>Надградување</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org Домашна страница на МедијаВики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Водич за корисници]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Водич за администратори]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ЧПП]",
+       "config-sidebar-readme": "Прочитај ме",
+       "config-sidebar-relnotes": "Белешки за изданието",
+       "config-sidebar-license": "Копирање",
+       "config-sidebar-upgrade": "Надградба",
        "config-env-good": "Околината е проверена.\nМожете да го воспоставите МедијаВики.",
        "config-env-bad": "Околината е проверена.\nНе можете да го воспоставите МедијаВики.",
        "config-env-php": "PHP $1 е воспоставен.",
        "config-env-hhvm": "HHVM $1 е воспоставен.",
-       "config-unicode-using-intl": "Со додатокот [https://pecl.php.net/intl intl PECL] за уникодна нормализација.",
-       "config-unicode-pure-php-warning": "'''Предупредување''': Додатокот [https://pecl.php.net/intl intl PECL] не е достапен за врши уникодна нормализација, враќајќи се на бавна примена на чист PHP.\n\nАко имате високопрометно мрежно место, тогаш ќе треба да прочитате повеќе за [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations уникодната нормализација].",
+       "config-unicode-using-intl": "Со додатокот [https://php.net/manual/en/book.intl.php intl PECL] за уникодна нормализација.",
+       "config-unicode-pure-php-warning": "'''Предупредување''': Додатокот [https://php.net/manual/en/book.intl.php intl PECL] не е достапен за врши уникодна нормализација, враќајќи се на бавна примена на чист PHP.\n\nАко имате високопрометно мрежно место, тогаш ќе треба да прочитате повеќе за [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations уникодната нормализација].",
        "config-unicode-update-warning": "'''Предупредување:''' Воспоставената верзија на обвивката за уникодна нормализација користи постара верзија на библиотеката на [http://site.icu-project.org/ проектот ICU].\nЗа да користите Уникод, ќе треба да направите [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations надградба].",
        "config-no-db": "Не можев да најдам соодветен двигател за базата на податоци! Ќе треба да воспоставите двигател за PHP-база.\n{{PLURAL:$2|Поддржан се следниов вид|Поддржани се следниве видови}} бази: $1.\n\nДоколку самите го срочивте овој PHP, овозможете го базниот клиент во поставките — на пр. со <code>./configure --with-mysqli</code>.\nАко овој PHP го воспоставите од пакет на Debian или Ubuntu, тогаш ќе треба исто така да го воспоставите, на пр., пакетот <code>php-mysql</code>.",
-       "config-outdated-sqlite": "'''Предупредување''': имате SQLite $1. Најстарата допуштена верзија е $2. Затоа, SQLite ќе биде недостапен.",
+       "config-outdated-sqlite": "<strong>Предупредување</strong>: имате SQLite $2. Најстарата допуштена верзија е $1. Затоа, SQLite ќе биде недостапен.",
        "config-no-fts3": "'''Предупредување''': SQLite iе составен без модулот [//sqlite.org/fts3.html FTS3] - за оваа база нема да има можност за пребарување.",
        "config-pcre-old": "'''Кобно:''' Се бара PCRE $1 или понова верзија.\nВашиот PHP-бинарен е сврзан со PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Повеќе информации].",
        "config-pcre-no-utf8": "<strong>Кобно</strong>: PCRE-модулот на PHP е срочен без поддршка за PCRE_UTF8.\nМедијаВики бара поддршка за UTF-8 за да може да работи правилно.",
        "config-license-help": "Многу јавни викија ги ставаат сите придонеси под [https://freedomdefined.org/Definition слободна лиценца].\nСо ова се создава атмосфера на општа сопственост и поттикнува долгорочно учество.\nОва не е неопходно за викија на поединечни физички или правни лица.\n\nАко сакате да користите текст од Википедија, и сакате Википедија да прифаќа текст прекопиран од вашето вики, тогаш треба да ја одберете лиценцата <strong>{{int:config-license-cc-by-sa}}</strong>..\n\nГНУ-овата лиценца за слободна документација (ГЛСД) е старата лиценца на Википедија.\nОваа лиценца сè уште важи, но е тешка за разбирање.\nИсто така треба да се има на ум дека пренамената на содржините под ГЛСД не е лесна.",
        "config-email-settings": "Нагодувања за е-пошта",
        "config-enable-email": "Овозможи излезна е-пошта",
-       "config-enable-email-help": "Ако сакате да работи е-поштата, [Config-dbsupport-oracle/manual/en/mail.configuration.php поштенските нагодувања на PHP] треба да се правилно наместени.\nАко воопшто не сакате никакви функции за е-пошта, тогаш можете да ги оневозможите тука.",
+       "config-enable-email-help": "Ако сакате да работи е-поштата, [https://www.php.net/manual/en/mail.configuration.php поштенските нагодувања на PHP] треба да се правилно наместени.\nАко воопшто не сакате никакви функции за е-пошта, тогаш можете да ги оневозможите тука.",
        "config-email-user": "Овозможи е-пошта од корисник до корисник",
        "config-email-user-help": "Дозволи сите корисници да можат да си праќаат е-пошта ако ја имаат овозможено во нагодувањата.",
        "config-email-usertalk": "Овозможи известувања за промени во кориснички страници за разговор",
index e9bb22b..1b0b054 100644 (file)
        "config-restart": "Sim, reiniciar",
        "config-welcome": "=== Verificações de ambiente ===\nSerão realizadas verificações básicas para determinar se este ambiente é apropriado para a instalação do MediaWiki.\nLembre-se de incluir estas informações se for procurar por suporte para como concluir a instalação.",
        "config-copyright": "=== Direitos autorais e Termos de uso ===\n\n$1\n\nEste programa é software livre; você pode redistribuí-lo e/ou modificá-lo nos termos da licença GNU General Public License tal como publicada pela Free Software Foundation; tanto a versão 2 da Licença, como (por opção sua) qualquer versão posterior.\n\nEste programa é distribuído na esperança de que seja útil, mas <strong>sem qualquer garantia</strong>; inclusive, sem a garantia implícita da <strong>possibilidade de ser comercializado</strong> ou de <strong>adequação para qualquer finalidade específica</strong>.\nConsulte a licença GNU General Public License para mais detalhes.\n\nEm conjunto com este programa você deve ter recebido <doclink href=Copying>uma cópia da licença GNU General Public License</doclink>; se não a recebeu, peça-a por escrito para Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ou [https://www.gnu.org/copyleft/gpl.html leia-a na internet].",
-       "config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki Página principal do MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Manual do usuário]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Manual do administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Leia-me</doclink>\n* <doclink href=ReleaseNotes>Notas de lançamento</doclink>\n* <doclink href=Copying>Licença</doclink>\n* <doclink href=UpgradeDoc>Atualizando</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/pt-br Página principal do MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/pt-br Ajuda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/pt-br Manual técnico]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/pt-br FAQ]",
+       "config-sidebar-readme": "Leia-me",
+       "config-sidebar-relnotes": "Notas de lançamento",
+       "config-sidebar-license": "Copiar",
+       "config-sidebar-upgrade": "Atualizar",
        "config-env-good": "O ambiente foi verificado.\nVocê pode instalar o MediaWiki.",
        "config-env-bad": "O ambiente foi verificado.\nVocê não pode instalar o MediaWiki.",
        "config-env-php": "O PHP $1 está instalado.",
index 12b5620..344bd23 100644 (file)
@@ -55,7 +55,7 @@
        "config-env-bad": "Окружење је проверено.\nНе можете да инсталирате MediaWiki.",
        "config-env-php": "PHP $1 је инсталиран.",
        "config-env-hhvm": "HHVM $1 је инсталиран.",
-       "config-unicode-using-intl": "Користи се [https://php.net/manual/en/book.intl.php PHP intl додатак] за нормализацију Уникода.",
+       "config-unicode-using-intl": "Користи се [https://php.net/manual/en/book.intl.php додатак PHP intl] за нормализацију Уникода.",
        "config-outdated-sqlite": "<strong>Упозорење:</strong> имате SQLite $2, који је нижи од најмање тражене верзије $1. SQLite ће бити недоступан.",
        "config-no-fts3": "<strong>Упозорење:</strong> SQLite је компајлиран без [//sqlite.org/fts3.html FTS3 модула], функције претраге биће недоступне на овој бази података.",
        "config-pcre-old": "<strong>Неотклоњива грешка:</strong> Неопходан је PCRE $1 или новији.\nВаш бинарни PHP је повезан са PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Више информација].",
index e2cb99e..1db3fca 100644 (file)
        "config-restart": "Ja, starta om",
        "config-welcome": "=== Miljökontroller ===\nGrundläggande kontroller kommer nu att utföras för att se om denna miljö är lämplig för installation av MediaWiki.\nKom ihåg att ta med denna information om du söker stöd för hur du skall slutföra installationen.",
        "config-copyright": "=== Upphovsrätt och Villkor ===\n\n$1\n\nDetta program är fri programvara; du kan vidaredistribuera den och/eller modifiera det enligt villkoren i GNU General Public License som publicerats av Free Software Foundation; antingen genom version 2 av licensen, eller (på ditt initiativ) någon senare version.\n\nDetta program är distribuerat i hopp om att det kommer att vara användbart, men '''utan någon garanti'''; utan att ens ha en underförstådd garanti om '''säljbarhet''' eller '''lämplighet för ett särskilt ändamål'''.\nSe GNU General Public License för mer detaljer.\n\nDu bör ha fått <doclink href=Copying>en kopia av GNU General Public License</doclink> tillsammans med detta program; om inte, skriv till Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, eller [https://www.gnu.org/copyleft/gpl.html läs den online].",
-       "config-sidebar": "* [https://www.mediawiki.org MediaWikis webbplats]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Användarguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratörguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Frågor och svar]\n----\n* <doclink href=Readme>Läs mig</doclink>\n* <doclink href=ReleaseNotes>Utgivningsanteckningar</doclink>\n* <doclink href=Copying>Kopiering</doclink>\n* <doclink href=UpgradeDoc>Uppgradering</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org MediaWikis webbplats]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Användarguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratörsguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Frågor och svar]",
+       "config-sidebar-readme": "Läs mig",
+       "config-sidebar-relnotes": "Utgivningsanteckningar",
+       "config-sidebar-license": "Kopierar",
+       "config-sidebar-upgrade": "Uppgradering",
        "config-env-good": "Miljön har kontrollerats.\nDu kan installera MediaWiki.",
        "config-env-bad": "Miljön har kontrollerats.\nDu kan inte installera MediaWiki.",
        "config-env-php": "PHP $1 är installerat.",
index 321caef..bfb0e64 100644 (file)
        "config-restart": "Так, перезапустити установку",
        "config-welcome": "=== Перевірка оточення ===\nБудуть проведені базові перевірки, щоб виявити, чи можлива установка MediaWiki у даній системі.\nНе забудьте включити цю інформацію, якщо ви звернетеся по підтримку, як завершити установку.",
        "config-copyright": "=== Авторське право і умови ===\n\n$1\n\nЦя програма є вільним програмним забезпеченням; Ви можете розповсюджувати та/або змінювати її під ліцензією GNU General Public License, опублікованою Фондом вільного програмного забезпечення; версією 2 цієї ліцензії або будь-якою пізнішою на Ваш вибір.\n\nЦя програма поширюється з надією на те, що вона буде корисною, однак '''без жодних гарантій'''; навіть без неявної гарантії '''комерційної цінності''' або '''придатності для певних цілей'''.\nДив. GNU General Public License для детальної інформації.\n\nВи повинні були отримати <doclink href=Copying>копію GNU General Public License</doclink> разом із цією програмою; якщо ж ні, зверніться до Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. або [https://www.gnu.org/copyleft/gpl.html ознайомтесь з нею онлайн].",
-       "config-sidebar": "* [https://www.mediawiki.org Сайт MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Посібник користувача]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Посібник адміністратора]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Інформація про випуск</doclink>\n* <doclink href=Copying>Ліцензія</doclink>\n* <doclink href=UpgradeDoc>Оновлення</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org Сайт MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Посібник користувача]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Посібник адміністратора]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]",
+       "config-sidebar-readme": "Прочитай мене",
+       "config-sidebar-relnotes": "Інформація про версію",
+       "config-sidebar-license": "Копіювання",
+       "config-sidebar-upgrade": "Оновлення",
        "config-env-good": "Перевірку середовища успішно завершено.\nВи можете встановити MediaWiki.",
        "config-env-bad": "Було проведено перевірку середовища. Ви не можете встановити MediaWiki.",
        "config-env-php": "Встановлено версію PHP: $1.",
        "config-env-hhvm": "HHVM $1  встановлено.",
-       "config-unicode-using-intl": "Ð\92икоÑ\80иÑ\81Ñ\82овÑ\83ваÑ\82и [https://pecl.php.net/intl Ð¼Ñ\96жнаÑ\80одне Ñ\80озÑ\88иÑ\80еннÑ\8f PECL] для нормалізації Юнікоду.",
-       "config-unicode-pure-php-warning": "'''Увага''': [https://pecl.php.net/intl міжнародне розширення PECL] не може провести нормалізацію Юнікоду.\nЯкщо ваш сайт має високий трафік, вам варто почитати про [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормалізацію Юнікоду].",
+       "config-unicode-using-intl": "Ð\97а Ð´Ð¾Ð¿Ð¾Ð¼Ð¾Ð³Ð¾Ñ\8e [https://php.net/manual/en/book.intl.php PHP-Ñ\80озÑ\88иÑ\80еннÑ\8f intl] для нормалізації Юнікоду.",
+       "config-unicode-pure-php-warning": "'''Увага''': [https://php.net/manual/en/book.intl.php PHP-розширення intl] не може провести нормалізацію Юнікоду.\nЯкщо ваш сайт має високий трафік, вам варто почитати про [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормалізацію Юнікоду].",
        "config-unicode-update-warning": "'''Увага''': Встановлена версія обгортки нормалізації Юнікоду використовує стару версію бібліотеки [http://site.icu-project.org/ проекту ICU].\nВи маєте [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations оновити версію], якщо плануєте повноцінно використовувати Юнікод.",
        "config-no-db": "Не вдалося знайти потрібний драйвер бази даних! Вам необхідно встановити драйвер бази даних для PHP. Підтримуються {{PLURAL:$2|такий тип|такі типи}} баз даних: $1.\n\nЯкщо ви скомпілювали PHP самостійно, переналаштуйте його з увімкненим клієнтом бази даних, наприклад за допомогою <code>./configure --with-mysqli</code>.\n\nЯкщо установлено PHP з пакетів Debian або Ubuntu, тоді ви також повинні встановити, наприклад, пакунок <code>php-mysql</code>.",
        "config-outdated-sqlite": "<strong>Увага:</strong> у Вас встановлена версія SQLite $2, а це нижче, ніж мінімально необхідна версія $1. SQLite буде недоступним.",
        "config-support-info": "MediaWiki підтримує такі системи баз даних:\n\n$1\n\nЯкщо Ви не бачите серед перерахованих систему баз даних, яку використовуєте, виконайте вказівки, вказані вище, щоб увімкнути підтримку.",
        "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] є основною ціллю для MediaWiki і найкраще підтримується.  MediaWiki також працює з [{{int:version-db-mysql-url}} MySQL] та [{{int:version-db-percona-url}} Percona Server], які сумісні з MariaDB.  ([https://www.php.net/manual/en/mysqli.installation.php Як зібрати PHP з підтримкою MySQL])",
        "config-dbsupport-postgres": "*  [{{int:version-db-postgres-url}} PostgreSQL] — популярна відкрита СУБД, альтернатива MySQL. ([https://www.php.net/manual/en/pgsql.installation.php як зібрати PHP з допомогою PostgreSQL]).",
-       "config-dbsupport-sqlite": "*  [{{int:version-db-sqlite-url}} SQLite] — легка система баз даних, яка дуже добре підтримується. ([http://www.php.net/manual/en/pdo.installation.php Як зібрати PHP з допомогою SQLite], що використовує PDO)",
-       "config-dbsupport-oracle": "*  [{{int:version-db-oracle-url}} Oracle] — комерційна база даних масштабу підприємства. ([http://www.php.net/manual/en/oci8.installation.php Як зібрати PHP з підтримкою OCI8])",
+       "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] — легка система баз даних, яка дуже добре підтримується. ([http://www.php.net/manual/en/pdo.installation.php Як зібрати PHP з допомогою SQLite], використовує PDO)",
+       "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] — комерційна база даних масштабу підприємства. ([http://www.php.net/manual/en/oci8.installation.php Як зібрати PHP з підтримкою OCI8])",
        "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] — це комерційна база даних для Windows масштабу підприємства. ([https://www.php.net/manual/en/sqlsrv.installation.php Як зібрати PHP з підтримкою SQLSRV])",
        "config-header-mysql": "Налаштування MariaDB/MySQL",
        "config-header-postgres": "Налаштування PostgreSQL",
        "config-license-help": "Чимало загальнодоступних вікі публікують увесь свій вміст під [https://freedomdefined.org/Definition вільною ліцензією]. Це розвиває відчуття спільної власності і заохочує довготривалу участь. У загальному випадку для приватної чи корпоративної вікі у цьому немає необхідності.\n\nЯкщо Ви хочете мати змогу використовувати текст з Вікіпедії і дати Вікіпедії змогу використовувати текст, скопійований з Вашої вікі, вам необхідно обрати <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nРаніше Вікіпедія використовувала GNU Free Documentation License.\nGFDL — допустима ліцензія, але у ній важко розібратися, а контент під GFDL важко використовувати повторно.",
        "config-email-settings": "Налаштування електронної пошти",
        "config-enable-email": "Увімкнути вихідну електронну пошту",
-       "config-enable-email-help": "Якщо Ви хочете, що електронна пошта працювала, необхідно виставити коректні [Config-dbsupport-oracle/manual/en/mail.configuration.php налаштування пошти у PHP].\nЯкщо Вам не потрібні жодні можливості електронної пошти у вікі, можете тут їх відімкнути.",
+       "config-enable-email-help": "Якщо Ви хочете, що електронна пошта працювала, необхідно виставити коректні [https://www.php.net/manual/en/mail.configuration.php налаштування пошти у PHP].\nЯкщо Вам не потрібні жодні можливості електронної пошти у вікі, можете тут їх відімкнути.",
        "config-email-user": "Увімкнути електронну пошту користувач-користувачеві",
        "config-email-user-help": "Дозволити усім користувачам надсилати один одному електронну пошту, якщо вони увімкнули цю можливість у своїх налаштуваннях.",
        "config-email-usertalk": "Увімкнути сповіщення про повідомлення на сторінці обговорення користувача",
index 4f083c6..03fb785 100644 (file)
@@ -8,7 +8,8 @@
                        "Nguyên Lê",
                        "Macofe",
                        "Leducthn",
-                       "Vinhtantran"
+                       "Vinhtantran",
+                       "Tuanminh01"
                ]
        },
        "config-desc": "Trình cài đặt MediaWiki",
        "config-restart": "Có, khởi động lại nó",
        "config-welcome": "=== Kiểm tra môi trường ===\nBây giờ sẽ kiểm tra sơ qua môi trường này có phù hợp cho việc cài đặt MediaWiki.\nHãy nhớ bao gồm thông tin này khi nào xin hỗ trợ hoàn thành việc cài đặt.",
        "config-copyright": "=== Bản quyền và Điều khoản ===\n\n$1\n\nChương trình này là phần mềm tự do; bạn được phép tái phân phối và/hoặc sửa đổi nó theo những điều khoản của Giấy phép Công cộng GNU do Quỹ Phần mềm Tự do xuất bản; phiên bản 2 hay bất kỳ phiên bản nào mới hơn nào của Giấy phép (tùy bạn lựa chọn).\n\nChương trình này được phân phối với hy vọng rằng nó sẽ hữu ích, nhưng <strong>không có bất kỳ một đảm bảo nào</strong>, ngay cả những bảo đảm ngụ ý cho <strong>tính thương mại</strong> hoặc <strong>phù hợp với mục đích đặc biệt nào đó</strong>. \nXem Giấy phép Công cộng GNU để biết thêm chi tiết.\n\nCó lẽ bạn đã nhận được <doclink href=Copying>bản sao Giấy phép Công cộng GNU</doclink> đi kèm với chương trình này; nếu không, hãy viết thư đến:\n Free Software Foundation, Inc.\n 51 Franklin St., Fifth Floor\n Boston, MA 02110-1301\n USA\nhoặc [https://www.gnu.org/copyleft/gpl.html đọc nó trực tuyến].",
-       "config-sidebar": "* [https://www.mediawiki.org/wiki/Special:MyLanguage/MediaWiki Trang chủ MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Hướng dẫn sử dụng]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Hướng dẫn quản lý]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Câu thường hỏi]\n----\n* <doclink href=Readme>Cần đọc trước</doclink>\n* <doclink href=ReleaseNotes>Ghi chú phát hành</doclink>\n* <doclink href=Copying>Sao chép</doclink>\n* <doclink href=UpgradeDoc>Nâng cấp</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org/wiki/Special:MyLanguage/MediaWiki Trang chủ MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Hướng dẫn sử dụng]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Hướng dẫn quản lý]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Câu thường hỏi]",
+       "config-sidebar-readme": "Đọc thêm",
+       "config-sidebar-relnotes": "Thông báo phát hành",
+       "config-sidebar-license": "Sao chép",
+       "config-sidebar-upgrade": "Nâng cấp",
        "config-env-good": "Đã kiểm tra môi trường.\nBạn có thể cài đặt MediaWiki.",
        "config-env-bad": "Đã kiểm tra môi trường.\nBạn không thể cài đặt MediaWiki.",
        "config-env-php": "PHP $1 đã được cài đặt.",
index f5ed7b9..e52f295 100644 (file)
@@ -44,8 +44,8 @@ abstract class JobQueue {
        /** @var StatsdDataFactoryInterface */
        protected $stats;
 
-       /** @var BagOStuff */
-       protected $dupCache;
+       /** @var WANObjectCache */
+       protected $wanCache;
 
        const QOS_ATOMIC = 1; // integer; "all-or-nothing" job insertions
 
@@ -53,6 +53,14 @@ abstract class JobQueue {
 
        /**
         * @param array $params
+        *       - type           : A job type
+        *   - domain         : A DB domain ID
+        *   - wanCache       : An instance of WANObjectCache to use for caching [default: none]
+        *   - stats          : An instance of StatsdDataFactoryInterface [default: none]
+        *   - claimTTL       : Seconds a job can be claimed for exclusive execution [default: forever]
+        *   - maxTries       : Total times a job can be tried, assuming claims expire [default: 3]
+        *   - order          : Queue order, one of ("fifo", "timestamp", "random") [default: variable]
+        *   - readOnlyReason : Mark the queue as read-only with this reason [default: false]
         * @throws JobQueueError
         */
        protected function __construct( array $params ) {
@@ -70,7 +78,7 @@ abstract class JobQueue {
                }
                $this->readOnlyReason = $params['readOnlyReason'] ?? false;
                $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
-               $this->dupCache = $params['stash'] ?? new EmptyBagOStuff();
+               $this->wanCache = $params['wanCache'] ?? WANObjectCache::newEmpty();
        }
 
        /**
@@ -459,24 +467,23 @@ abstract class JobQueue {
         * @return bool
         */
        protected function doDeduplicateRootJob( IJobSpecification $job ) {
-               if ( !$job->hasRootJobParams() ) {
+               $params = $job->hasRootJobParams() ? $job->getRootJobParams() : null;
+               if ( !$params ) {
                        throw new JobQueueError( "Cannot register root job; missing parameters." );
                }
-               $params = $job->getRootJobParams();
 
                $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
-               // Callers should call JobQueueGroup::push() before this method so that if the insert
-               // fails, the de-duplication registration will be aborted. Since the insert is
-               // deferred till "transaction idle", do the same here, so that the ordering is
-               // maintained. Having only the de-duplication registration succeed would cause
-               // jobs to become no-ops without any actual jobs that made them redundant.
-               $timestamp = $this->dupCache->get( $key ); // current last timestamp of this job
-               if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
+               // Callers should call JobQueueGroup::push() before this method so that if the
+               // insert fails, the de-duplication registration will be aborted. Having only the
+               // de-duplication registration succeed would cause jobs to become no-ops without
+               // any actual jobs that made them redundant.
+               $timestamp = $this->wanCache->get( $key ); // last known timestamp of such a root job
+               if ( $timestamp !== false && $timestamp >= $params['rootJobTimestamp'] ) {
                        return true; // a newer version of this root job was enqueued
                }
 
                // Update the timestamp of the last root job started at the location...
-               return $this->dupCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
+               return $this->wanCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
        }
 
        /**
@@ -490,9 +497,8 @@ abstract class JobQueue {
                if ( $job->getType() !== $this->type ) {
                        throw new JobQueueError( "Got '{$job->getType()}' job; expected '{$this->type}'." );
                }
-               $isDuplicate = $this->doIsRootJobOldDuplicate( $job );
 
-               return $isDuplicate;
+               return $this->doIsRootJobOldDuplicate( $job );
        }
 
        /**
@@ -501,14 +507,18 @@ abstract class JobQueue {
         * @return bool
         */
        protected function doIsRootJobOldDuplicate( IJobSpecification $job ) {
-               if ( !$job->hasRootJobParams() ) {
+               $params = $job->hasRootJobParams() ? $job->getRootJobParams() : null;
+               if ( !$params ) {
                        return false; // job has no de-deplication info
                }
-               $params = $job->getRootJobParams();
 
                $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
                // Get the last time this root job was enqueued
-               $timestamp = $this->dupCache->get( $key );
+               $timestamp = $this->wanCache->get( $key );
+               if ( $timestamp === false || $params['rootJobTimestamp'] > $timestamp ) {
+                       // Update the timestamp of the last known root job started at the location...
+                       $this->wanCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
+               }
 
                // Check if a new root job was started at the location after this one's...
                return ( $timestamp && $timestamp > $params['rootJobTimestamp'] );
@@ -519,7 +529,7 @@ abstract class JobQueue {
         * @return string
         */
        protected function getRootJobCacheKey( $signature ) {
-               return $this->dupCache->makeGlobalKey(
+               return $this->wanCache->makeGlobalKey(
                        'jobqueue',
                        $this->domain,
                        $this->type,
index 7c78f40..f7b8ed2 100644 (file)
@@ -24,6 +24,7 @@ use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\DBConnectionError;
 use Wikimedia\Rdbms\DBError;
 use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\IMaintainableDatabase;
 use Wikimedia\ScopedCallback;
 
 /**
@@ -38,9 +39,7 @@ class JobQueueDB extends JobQueue {
        const MAX_JOB_RANDOM = 2147483647; // integer; 2^31 - 1, used for job_random
        const MAX_OFFSET = 255; // integer; maximum number of rows to skip
 
-       /** @var WANObjectCache */
-       protected $cache;
-       /** @var IDatabase|DBError|null */
+       /** @var IMaintainableDatabase|DBError|null */
        protected $conn;
 
        /** @var array|null Server configuration array */
@@ -55,7 +54,6 @@ class JobQueueDB extends JobQueue {
         *               If not specified, the primary DB cluster for the wiki will be used.
         *               This can be overridden with a custom cluster so that DB handles will
         *               be retrieved via LBFactory::getExternalLB() and getConnection().
-        *   - wanCache : An instance of WANObjectCache to use for caching.
         * @param array $params
         */
        protected function __construct( array $params ) {
@@ -66,8 +64,6 @@ class JobQueueDB extends JobQueue {
                } elseif ( isset( $params['cluster'] ) && is_string( $params['cluster'] ) ) {
                        $this->cluster = $params['cluster'];
                }
-
-               $this->cache = $params['wanCache'] ?? WANObjectCache::newEmpty();
        }
 
        protected function supportedOrders() {
@@ -104,7 +100,7 @@ class JobQueueDB extends JobQueue {
        protected function doGetSize() {
                $key = $this->getCacheKey( 'size' );
 
-               $size = $this->cache->get( $key );
+               $size = $this->wanCache->get( $key );
                if ( is_int( $size ) ) {
                        return $size;
                }
@@ -120,7 +116,7 @@ class JobQueueDB extends JobQueue {
                } catch ( DBError $e ) {
                        throw $this->getDBException( $e );
                }
-               $this->cache->set( $key, $size, self::CACHE_TTL_SHORT );
+               $this->wanCache->set( $key, $size, self::CACHE_TTL_SHORT );
 
                return $size;
        }
@@ -136,7 +132,7 @@ class JobQueueDB extends JobQueue {
 
                $key = $this->getCacheKey( 'acquiredcount' );
 
-               $count = $this->cache->get( $key );
+               $count = $this->wanCache->get( $key );
                if ( is_int( $count ) ) {
                        return $count;
                }
@@ -152,7 +148,7 @@ class JobQueueDB extends JobQueue {
                } catch ( DBError $e ) {
                        throw $this->getDBException( $e );
                }
-               $this->cache->set( $key, $count, self::CACHE_TTL_SHORT );
+               $this->wanCache->set( $key, $count, self::CACHE_TTL_SHORT );
 
                return $count;
        }
@@ -169,7 +165,7 @@ class JobQueueDB extends JobQueue {
 
                $key = $this->getCacheKey( 'abandonedcount' );
 
-               $count = $this->cache->get( $key );
+               $count = $this->wanCache->get( $key );
                if ( is_int( $count ) ) {
                        return $count;
                }
@@ -190,7 +186,7 @@ class JobQueueDB extends JobQueue {
                        throw $this->getDBException( $e );
                }
 
-               $this->cache->set( $key, $count, self::CACHE_TTL_SHORT );
+               $this->wanCache->set( $key, $count, self::CACHE_TTL_SHORT );
 
                return $count;
        }
@@ -345,7 +341,7 @@ class JobQueueDB extends JobQueue {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = $this->getScopedNoTrxFlag( $dbw );
                // Check cache to see if the queue has <= OFFSET items
-               $tinyQueue = $this->cache->get( $this->getCacheKey( 'small' ) );
+               $tinyQueue = $this->wanCache->get( $this->getCacheKey( 'small' ) );
 
                $invertedDirection = false; // whether one job_random direction was already scanned
                // This uses a replication safe method for acquiring jobs. One could use UPDATE+LIMIT
@@ -385,7 +381,7 @@ class JobQueueDB extends JobQueue {
                                );
                                if ( !$row ) {
                                        $tinyQueue = true; // we know the queue must have <= MAX_OFFSET rows
-                                       $this->cache->set( $this->getCacheKey( 'small' ), 1, 30 );
+                                       $this->wanCache->set( $this->getCacheKey( 'small' ), 1, 30 );
                                        continue; // use job_random
                                }
                        }
@@ -510,32 +506,17 @@ class JobQueueDB extends JobQueue {
         * @return bool
         */
        protected function doDeduplicateRootJob( IJobSpecification $job ) {
-               $params = $job->getParams();
-               if ( !isset( $params['rootJobSignature'] ) ) {
-                       throw new MWException( "Cannot register root job; missing 'rootJobSignature'." );
-               } elseif ( !isset( $params['rootJobTimestamp'] ) ) {
-                       throw new MWException( "Cannot register root job; missing 'rootJobTimestamp'." );
-               }
-               $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
-               // Callers should call JobQueueGroup::push() before this method so that if the insert
-               // fails, the de-duplication registration will be aborted. Since the insert is
-               // deferred till "transaction idle", do the same here, so that the ordering is
+               // Callers should call JobQueueGroup::push() before this method so that if the
+               // insert fails, the de-duplication registration will be aborted. Since the insert
+               // is deferred till "transaction idle", do the same here, so that the ordering is
                // maintained. Having only the de-duplication registration succeed would cause
                // jobs to become no-ops without any actual jobs that made them redundant.
                $dbw = $this->getMasterDB();
                /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = $this->getScopedNoTrxFlag( $dbw );
-
-               $cache = $this->dupCache;
                $dbw->onTransactionCommitOrIdle(
-                       function () use ( $cache, $params, $key ) {
-                               $timestamp = $cache->get( $key ); // current last timestamp of this job
-                               if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
-                                       return true; // a newer version of this root job was enqueued
-                               }
-
-                               // Update the timestamp of the last root job started at the location...
-                               return $cache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
+                       function () use ( $job ) {
+                               parent::doDeduplicateRootJob( $job );
                        },
                        __METHOD__
                );
@@ -581,7 +562,7 @@ class JobQueueDB extends JobQueue {
         */
        protected function doFlushCaches() {
                foreach ( [ 'size', 'acquiredcount' ] as $type ) {
-                       $this->cache->delete( $this->getCacheKey( $type ) );
+                       $this->wanCache->delete( $this->getCacheKey( $type ) );
                }
        }
 
@@ -789,7 +770,7 @@ class JobQueueDB extends JobQueue {
 
        /**
         * @throws JobQueueConnectionError
-        * @return IDatabase
+        * @return IMaintainableDatabase
         */
        protected function getMasterDB() {
                try {
@@ -801,7 +782,7 @@ class JobQueueDB extends JobQueue {
 
        /**
         * @param int $index (DB_REPLICA/DB_MASTER)
-        * @return IDatabase
+        * @return IMaintainableDatabase
         */
        protected function getDB( $index ) {
                if ( $this->server ) {
@@ -825,12 +806,16 @@ class JobQueueDB extends JobQueue {
                                ? $lbFactory->getExternalLB( $this->cluster )
                                : $lbFactory->getMainLB( $this->domain );
 
-                       return ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' )
+                       if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
                                // Keep a separate connection to avoid contention and deadlocks;
                                // However, SQLite has the opposite behavior due to DB-level locking.
-                               ? $lb->getConnectionRef( $index, [], $this->domain, $lb::CONN_TRX_AUTOCOMMIT )
+                               $flags = $lb::CONN_TRX_AUTOCOMMIT;
+                       } else {
                                // Jobs insertion will be defered until the PRESEND stage to reduce contention.
-                               : $lb->getConnectionRef( $index, [], $this->domain );
+                               $flags = 0;
+                       }
+
+                       return $lb->getMaintenanceConnectionRef( $index, [], $this->domain, $flags );
                }
        }
 
@@ -856,7 +841,7 @@ class JobQueueDB extends JobQueue {
        private function getCacheKey( $property ) {
                $cluster = is_string( $this->cluster ) ? $this->cluster : 'main';
 
-               return $this->cache->makeGlobalKey(
+               return $this->wanCache->makeGlobalKey(
                        'jobqueue',
                        $this->domain,
                        $cluster,
index 756724e..06cd04c 100644 (file)
@@ -121,7 +121,6 @@ class JobQueueGroup {
                $services = MediaWikiServices::getInstance();
                $conf['stats'] = $services->getStatsdDataFactory();
                $conf['wanCache'] = $services->getMainWANObjectCache();
-               $conf['stash'] = $services->getMainObjectStash();
 
                return JobQueue::factory( $conf );
        }
index cb20a76..b26129e 100644 (file)
@@ -33,9 +33,9 @@ class JobQueueMemory extends JobQueue {
        protected static $data = [];
 
        public function __construct( array $params ) {
-               parent::__construct( $params );
+               $params['wanCache'] = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
 
-               $this->dupCache = new HashBagOStuff();
+               parent::__construct( $params );
        }
 
        /**
index 2140043..569a5d4 100644 (file)
@@ -19,6 +19,8 @@
  *
  * @file
  */
+
+use MediaWiki\Logger\LoggerFactory;
 use Psr\Log\LoggerInterface;
 
 /**
@@ -100,7 +102,7 @@ class JobQueueRedis extends JobQueue {
                                "Non-daemonized mode is no longer supported. Please install the " .
                                "mediawiki/services/jobrunner service and update \$wgJobTypeConf as needed." );
                }
-               $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
+               $this->logger = LoggerFactory::getInstance( 'redis' );
        }
 
        protected function supportedOrders() {
@@ -134,7 +136,7 @@ class JobQueueRedis extends JobQueue {
                try {
                        return $conn->lSize( $this->getQueueKey( 'l-unclaimed' ) );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
        }
 
@@ -152,7 +154,7 @@ class JobQueueRedis extends JobQueue {
 
                        return array_sum( $conn->exec() );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
        }
 
@@ -166,7 +168,7 @@ class JobQueueRedis extends JobQueue {
                try {
                        return $conn->zSize( $this->getQueueKey( 'z-delayed' ) );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
        }
 
@@ -180,7 +182,7 @@ class JobQueueRedis extends JobQueue {
                try {
                        return $conn->zSize( $this->getQueueKey( 'z-abandoned' ) );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
        }
 
@@ -235,7 +237,7 @@ class JobQueueRedis extends JobQueue {
                                throw new RedisException( $err );
                        }
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
        }
 
@@ -332,7 +334,7 @@ LUA;
                                $job = $this->getJobFromFields( $item ); // may be false
                        } while ( !$job ); // job may be false if invalid
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                return $job;
@@ -426,7 +428,7 @@ LUA;
 
                        $this->incrStats( 'acks', $this->type );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                return true;
@@ -449,7 +451,7 @@ LUA;
 
                $conn = $this->getConnection();
                try {
-                       $timestamp = $conn->get( $key ); // current last timestamp of this job
+                       $timestamp = $conn->get( $key ); // last known timestamp of such a root job
                        if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
                                return true; // a newer version of this root job was enqueued
                        }
@@ -457,7 +459,7 @@ LUA;
                        // Update the timestamp of the last root job started at the location...
                        return $conn->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL ); // 2 weeks
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
        }
 
@@ -478,8 +480,7 @@ LUA;
                        // Get the last time this root job was enqueued
                        $timestamp = $conn->get( $this->getRootJobCacheKey( $params['rootJobSignature'] ) );
                } catch ( RedisException $e ) {
-                       $timestamp = false;
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                // Check if a new root job was started at the location after this one's...
@@ -507,7 +508,7 @@ LUA;
 
                        return $ok;
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
        }
 
@@ -521,7 +522,7 @@ LUA;
                try {
                        $uids = $conn->lRange( $this->getQueueKey( 'l-unclaimed' ), 0, -1 );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                return $this->getJobIterator( $conn, $uids );
@@ -537,7 +538,7 @@ LUA;
                try {
                        $uids = $conn->zRange( $this->getQueueKey( 'z-delayed' ), 0, -1 );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                return $this->getJobIterator( $conn, $uids );
@@ -553,7 +554,7 @@ LUA;
                try {
                        $uids = $conn->zRange( $this->getQueueKey( 'z-claimed' ), 0, -1 );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                return $this->getJobIterator( $conn, $uids );
@@ -569,7 +570,7 @@ LUA;
                try {
                        $uids = $conn->zRange( $this->getQueueKey( 'z-abandoned' ), 0, -1 );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                return $this->getJobIterator( $conn, $uids );
@@ -616,7 +617,7 @@ LUA;
                                }
                        }
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                return $sizes;
@@ -626,12 +627,12 @@ LUA;
         * This function should not be called outside JobQueueRedis
         *
         * @param string $uid
-        * @param RedisConnRef $conn
+        * @param RedisConnRef|Redis $conn
         * @return RunnableJob|bool Returns false if the job does not exist
         * @throws JobQueueError
         * @throws UnexpectedValueException
         */
-       public function getJobFromUidInternal( $uid, RedisConnRef $conn ) {
+       public function getJobFromUidInternal( $uid, $conn ) {
                try {
                        $data = $conn->hGet( $this->getQueueKey( 'h-data' ), $uid );
                        if ( $data === false ) {
@@ -653,7 +654,7 @@ LUA;
 
                        return $job;
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
        }
 
@@ -672,7 +673,7 @@ LUA;
                                $queues[] = $this->decodeQueueName( $queue );
                        }
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $conn, $e );
+                       throw $this->handleErrorAndMakeException( $conn, $e );
                }
 
                return $queues;
@@ -754,7 +755,7 @@ LUA;
        /**
         * Get a connection to the server that handles all sub-queues for this queue
         *
-        * @return RedisConnRef
+        * @return RedisConnRef|Redis
         * @throws JobQueueConnectionError
         */
        protected function getConnection() {
@@ -770,11 +771,11 @@ LUA;
        /**
         * @param RedisConnRef $conn
         * @param RedisException $e
-        * @throws JobQueueError
+        * @return JobQueueError
         */
-       protected function throwRedisException( RedisConnRef $conn, $e ) {
+       protected function handleErrorAndMakeException( RedisConnRef $conn, $e ) {
                $this->redisPool->handleError( $conn, $e );
-               throw new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
+               return new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
        }
 
        /**
index be76fc6..cb4b051 100644 (file)
@@ -81,7 +81,7 @@ class CategoryMembershipChangeJob extends Job {
        public function run() {
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                $lb = $lbFactory->getMainLB();
-               $dbw = $lb->getConnection( DB_MASTER );
+               $dbw = $lb->getConnectionRef( DB_MASTER );
 
                $this->ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
 
@@ -92,14 +92,14 @@ class CategoryMembershipChangeJob extends Job {
                }
 
                // Cut down on the time spent in waitForMasterPos() in the critical section
-               $dbr = $lb->getConnection( DB_REPLICA, [ 'recentchanges' ] );
+               $dbr = $lb->getConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
                if ( !$lb->waitForMasterPos( $dbr ) ) {
                        $this->setLastError( "Timed out while pre-waiting for replica DB to catch up" );
                        return false;
                }
 
                // Use a named lock so that jobs for this page see each others' changes
-               $lockKey = "CategoryMembershipUpdates:{$page->getId()}";
+               $lockKey = "{$dbw->getDomainID()}:CategoryMembershipChange:{$page->getId()}"; // per-wiki
                $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 3 );
                if ( !$scopedLock ) {
                        $this->setLastError( "Could not acquire lock '$lockKey'" );
index 1793053..e373605 100644 (file)
@@ -40,8 +40,8 @@ class ClearUserWatchlistJob extends Job implements GenericParameterJob {
                $batchSize = $wgUpdateRowsPerQuery;
 
                $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
-               $dbw = $loadBalancer->getConnection( DB_MASTER );
-               $dbr = $loadBalancer->getConnection( DB_REPLICA, [ 'watchlist' ] );
+               $dbw = $loadBalancer->getConnectionRef( DB_MASTER );
+               $dbr = $loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
 
                // Wait before lock to try to reduce time waiting in the lock.
                if ( !$loadBalancer->waitForMasterPos( $dbr ) ) {
@@ -50,7 +50,7 @@ class ClearUserWatchlistJob extends Job implements GenericParameterJob {
                }
 
                // Use a named lock so that jobs for this user see each others' changes
-               $lockKey = "ClearUserWatchlistJob:$userId";
+               $lockKey = "{{$dbw->getDomainID()}}:ClearUserWatchlist:$userId"; // per-wiki
                $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 );
                if ( !$scopedLock ) {
                        $this->setLastError( "Could not acquire lock '$lockKey'" );
index f53174a..054718d 100644 (file)
@@ -51,7 +51,7 @@ class ClearWatchlistNotificationsJob extends Job implements GenericParameterJob
                $lbFactory = $services->getDBLoadBalancerFactory();
                $rowsPerQuery = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
 
-               $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
+               $dbw = $lbFactory->getMainLB()->getConnectionRef( DB_MASTER );
                $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
                $timestamp = $this->params['timestamp'] ?? null;
                if ( $timestamp === null ) {
index 89ecb0e..b4046a6 100644 (file)
@@ -22,6 +22,8 @@
  */
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionRenderer;
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
 
 /**
  * Job to update link tables for pages
@@ -37,10 +39,8 @@ use MediaWiki\Revision\RevisionRecord;
  * @ingroup JobQueue
  */
 class RefreshLinksJob extends Job {
-       /** @var float Cache parser output when it takes this long to render */
-       const PARSE_THRESHOLD_SEC = 1.0;
        /** @var int Lag safety margin when comparing root job times to last-refresh times */
-       const CLOCK_FUDGE = 10;
+       const NORMAL_MAX_LAG = 10;
        /** @var int How many seconds to wait for replica DBs to catch up */
        const LAG_WAIT_TIMEOUT = 15;
 
@@ -54,7 +54,9 @@ class RefreshLinksJob extends Job {
                        !( isset( $params['pages'] ) && count( $params['pages'] ) != 1 )
                );
                $this->params += [ 'causeAction' => 'unknown', 'causeAgent' => 'unknown' ];
-               // This will control transaction rounds in order to run DataUpdates
+               // Tell JobRunner to not automatically wrap run() in a transaction round.
+               // Each runForTitle() call will manage its own rounds in order to run DataUpdates
+               // and to avoid contention as well.
                $this->executionFlags |= self::JOB_NO_EXPLICIT_TRX_ROUND;
        }
 
@@ -83,21 +85,21 @@ class RefreshLinksJob extends Job {
        }
 
        function run() {
-               global $wgUpdateRowsPerJob;
-
                $ok = true;
+
                // Job to update all (or a range of) backlink pages for a page
                if ( !empty( $this->params['recursive'] ) ) {
+                       $services = MediaWikiServices::getInstance();
                        // When the base job branches, wait for the replica DBs to catch up to the master.
                        // From then on, we know that any template changes at the time the base job was
                        // enqueued will be reflected in backlink page parses when the leaf jobs run.
                        if ( !isset( $this->params['range'] ) ) {
-                               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                               $lbFactory = $services->getDBLoadBalancerFactory();
                                if ( !$lbFactory->waitForReplication( [
-                                               'domain'  => $lbFactory->getLocalDomainID(),
-                                               'timeout' => self::LAG_WAIT_TIMEOUT
+                                       'domain'  => $lbFactory->getLocalDomainID(),
+                                       'timeout' => self::LAG_WAIT_TIMEOUT
                                ] ) ) { // only try so hard
-                                       $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+                                       $stats = $services->getStatsdDataFactory();
                                        $stats->increment( 'refreshlinks.lag_wait_failed' );
                                }
                        }
@@ -111,7 +113,7 @@ class RefreshLinksJob extends Job {
                        // jobs and possibly a recursive RefreshLinks job for the rest of the backlinks
                        $jobs = BacklinkJobUtils::partitionBacklinkJob(
                                $this,
-                               $wgUpdateRowsPerJob,
+                               $services->getMainConfig()->get( 'UpdateRowsPerJob' ),
                                1, // job-per-title
                                [ 'params' => $extraParams ]
                        );
@@ -121,7 +123,7 @@ class RefreshLinksJob extends Job {
                        foreach ( $this->params['pages'] as list( $ns, $dbKey ) ) {
                                $title = Title::makeTitleSafe( $ns, $dbKey );
                                if ( $title ) {
-                                       $this->runForTitle( $title );
+                                       $ok = $this->runForTitle( $title ) && $ok;
                                } else {
                                        $ok = false;
                                        $this->setLastError( "Invalid title ($ns,$dbKey)." );
@@ -129,7 +131,7 @@ class RefreshLinksJob extends Job {
                        }
                // Job to update link tables for a given title
                } else {
-                       $this->runForTitle( $this->title );
+                       $ok = $this->runForTitle( $this->title );
                }
 
                return $ok;
@@ -142,139 +144,235 @@ class RefreshLinksJob extends Job {
        protected function runForTitle( Title $title ) {
                $services = MediaWikiServices::getInstance();
                $stats = $services->getStatsdDataFactory();
-               $lbFactory = $services->getDBLoadBalancerFactory();
-               $revisionStore = $services->getRevisionStore();
                $renderer = $services->getRevisionRenderer();
+               $parserCache = $services->getParserCache();
+               $lbFactory = $services->getDBLoadBalancerFactory();
                $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
 
-               $lbFactory->beginMasterChanges( __METHOD__ );
-
+               // Load the page from the master DB
                $page = WikiPage::factory( $title );
                $page->loadPageData( WikiPage::READ_LATEST );
 
+               // Serialize link update job by page ID so they see each others' changes.
+               // The page ID and latest revision ID will be queried again after the lock
+               // is acquired to bail if they are changed from that of loadPageData() above.
                // Serialize links updates by page ID so they see each others' changes
-               $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
+               $dbw = $lbFactory->getMainLB()->getConnectionRef( DB_MASTER );
                /** @noinspection PhpUnusedLocalVariableInspection */
                $scopedLock = LinksUpdate::acquirePageLock( $dbw, $page->getId(), 'job' );
                if ( $scopedLock === null ) {
-                       $lbFactory->commitMasterChanges( __METHOD__ );
-                       // Another job is already updating the page, likely for an older revision (T170596).
+                       // Another job is already updating the page, likely for a prior revision (T170596)
                        $this->setLastError( 'LinksUpdate already running for this page, try again later.' );
+                       $stats->increment( 'refreshlinks.lock_failure' );
+
+                       return false;
+               }
+
+               if ( $this->isAlreadyRefreshed( $page ) ) {
+                       $stats->increment( 'refreshlinks.update_skipped' );
+
+                       return true;
+               }
+
+               // Parse during a fresh transaction round for better read consistency
+               $lbFactory->beginMasterChanges( __METHOD__ );
+               $output = $this->getParserOutput( $renderer, $parserCache, $page, $stats );
+               $options = $this->getDataUpdateOptions();
+               $lbFactory->commitMasterChanges( __METHOD__ );
+
+               if ( !$output ) {
+                       return false; // raced out?
+               }
+
+               // Tell DerivedPageDataUpdater to use this parser output
+               $options['known-revision-output'] = $output;
+               // Execute corresponding DataUpdates immediately
+               $page->doSecondaryDataUpdates( $options );
+               InfoAction::invalidateCache( $title );
+
+               // Commit any writes here in case this method is called in a loop.
+               // In that case, the scoped lock will fail to be acquired.
+               $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+
+               return true;
+       }
+
+       /**
+        * @param WikiPage $page
+        * @return bool Whether something updated the backlinks with data newer than this job
+        */
+       private function isAlreadyRefreshed( WikiPage $page ) {
+               // Get the timestamp of the change that triggered this job
+               $rootTimestamp = $this->params['rootJobTimestamp'] ?? null;
+               if ( $rootTimestamp === null ) {
                        return false;
                }
-               // Get the latest ID *after* acquirePageLock() flushed the transaction.
+
+               if ( !empty( $this->params['isOpportunistic'] ) ) {
+                       // Neither clock skew nor DB snapshot/replica DB lag matter much for
+                       // such updates; focus on reusing the (often recently updated) cache
+                       $lagAwareTimestamp = $rootTimestamp;
+               } else {
+                       // For transclusion updates, the template changes must be reflected
+                       $lagAwareTimestamp = wfTimestamp(
+                               TS_MW,
+                               wfTimestamp( TS_UNIX, $rootTimestamp ) + self::NORMAL_MAX_LAG
+                       );
+               }
+
+               return ( $page->getLinksTimestamp() > $lagAwareTimestamp );
+       }
+
+       /**
+        * Get the parser output if the page is unchanged from what was loaded in $page
+        *
+        * @param RevisionRenderer $renderer
+        * @param ParserCache $parserCache
+        * @param WikiPage $page Page already loaded with READ_LATEST
+        * @param StatsdDataFactoryInterface $stats
+        * @return ParserOutput|null Combined output for all slots; might only contain metadata
+        */
+       private function getParserOutput(
+               RevisionRenderer $renderer,
+               ParserCache $parserCache,
+               WikiPage $page,
+               StatsdDataFactoryInterface $stats
+       ) {
+               $revision = $this->getCurrentRevisionIfUnchanged( $page, $stats );
+               if ( !$revision ) {
+                       return null; // race condition?
+               }
+
+               $cachedOutput = $this->getParserOutputFromCache( $parserCache, $page, $revision, $stats );
+               if ( $cachedOutput ) {
+                       return $cachedOutput;
+               }
+
+               $renderedRevision = $renderer->getRenderedRevision(
+                       $revision,
+                       $page->makeParserOptions( 'canonical' ),
+                       null,
+                       [ 'audience' => $revision::RAW ]
+               );
+
+               $parseTimestamp = wfTimestampNow(); // timestamp that parsing started
+               $output = $renderedRevision->getRevisionParserOutput( [ 'generate-html' => false ] );
+               $output->setCacheTime( $parseTimestamp ); // notify LinksUpdate::doUpdate()
+
+               return $output;
+       }
+
+       /**
+        * Get the current revision record if it is unchanged from what was loaded in $page
+        *
+        * @param WikiPage $page Page already loaded with READ_LATEST
+        * @param StatsdDataFactoryInterface $stats
+        * @return RevisionRecord|null The same instance that $page->getRevisionRecord() uses
+        */
+       private function getCurrentRevisionIfUnchanged(
+               WikiPage $page,
+               StatsdDataFactoryInterface $stats
+       ) {
+               $title = $page->getTitle();
+               // Get the latest ID since acquirePageLock() in runForTitle() flushed the transaction.
                // This is used to detect edits/moves after loadPageData() but before the scope lock.
-               // The works around the chicken/egg problem of determining the scope lock key.
+               // The works around the chicken/egg problem of determining the scope lock key name.
                $latest = $title->getLatestRevID( Title::GAID_FOR_UPDATE );
 
-               if ( !empty( $this->params['triggeringRevisionId'] ) ) {
-                       // Fetch the specified revision; lockAndGetLatest() below detects if the page
-                       // was edited since and aborts in order to avoid corrupting the link tables
-                       $revision = $revisionStore->getRevisionById(
-                               (int)$this->params['triggeringRevisionId'],
-                               Revision::READ_LATEST
-                       );
-               } else {
-                       // Fetch current revision; READ_LATEST reduces lockAndGetLatest() check failures
-                       $revision = $revisionStore->getRevisionByTitle( $title, 0, Revision::READ_LATEST );
+               $triggeringRevisionId = $this->params['triggeringRevisionId'] ?? null;
+               if ( $triggeringRevisionId && $triggeringRevisionId !== $latest ) {
+                       // This job is obsolete and one for the latest revision will handle updates
+                       $stats->increment( 'refreshlinks.rev_not_current' );
+                       $this->setLastError( "Revision $triggeringRevisionId is not current" );
+
+                       return null;
                }
 
+               // Load the current revision. Note that $page should have loaded with READ_LATEST.
+               // This instance will be reused in WikiPage::doSecondaryDataUpdates() later on.
+               $revision = $page->getRevisionRecord();
                if ( !$revision ) {
-                       $lbFactory->commitMasterChanges( __METHOD__ );
                        $stats->increment( 'refreshlinks.rev_not_found' );
                        $this->setLastError( "Revision not found for {$title->getPrefixedDBkey()}" );
-                       return false; // just deleted?
-               } elseif ( $revision->getId() != $latest || $revision->getPageId() !== $page->getId() ) {
-                       $lbFactory->commitMasterChanges( __METHOD__ );
+
+                       return null; // just deleted?
+               } elseif ( $revision->getId() !== $latest || $revision->getPageId() !== $page->getId() ) {
                        // Do not clobber over newer updates with older ones. If all jobs where FIFO and
                        // serialized, it would be OK to update links based on older revisions since it
                        // would eventually get to the latest. Since that is not the case (by design),
                        // only update the link tables to a state matching the current revision's output.
                        $stats->increment( 'refreshlinks.rev_not_current' );
                        $this->setLastError( "Revision {$revision->getId()} is not current" );
-                       return false;
+
+                       return null;
                }
 
-               $parserOutput = false;
-               $parserOptions = $page->makeParserOptions( 'canonical' );
+               return $revision;
+       }
+
+       /**
+        * Get the parser output from cache if it reflects the change that triggered this job
+        *
+        * @param ParserCache $parserCache
+        * @param WikiPage $page
+        * @param RevisionRecord $currentRevision
+        * @param StatsdDataFactoryInterface $stats
+        * @return ParserOutput|null
+        */
+       private function getParserOutputFromCache(
+               ParserCache $parserCache,
+               WikiPage $page,
+               RevisionRecord $currentRevision,
+               StatsdDataFactoryInterface $stats
+       ) {
+               $cachedOutput = null;
                // If page_touched changed after this root job, then it is likely that
                // any views of the pages already resulted in re-parses which are now in
                // cache. The cache can be reused to avoid expensive parsing in some cases.
-               if ( isset( $this->params['rootJobTimestamp'] ) ) {
+               $rootTimestamp = $this->params['rootJobTimestamp'] ?? null;
+               if ( $rootTimestamp !== null ) {
                        $opportunistic = !empty( $this->params['isOpportunistic'] );
-
-                       $skewedTimestamp = $this->params['rootJobTimestamp'];
                        if ( $opportunistic ) {
-                               // Neither clock skew nor DB snapshot/replica DB lag matter much for such
-                               // updates; focus on reusing the (often recently updated) cache
+                               // Neither clock skew nor DB snapshot/replica DB lag matter much for
+                               // such updates; focus on reusing the (often recently updated) cache
+                               $lagAwareTimestamp = $rootTimestamp;
                        } else {
                                // For transclusion updates, the template changes must be reflected
-                               $skewedTimestamp = wfTimestamp( TS_MW,
-                                       wfTimestamp( TS_UNIX, $skewedTimestamp ) + self::CLOCK_FUDGE
+                               $lagAwareTimestamp = wfTimestamp(
+                                       TS_MW,
+                                       wfTimestamp( TS_UNIX, $rootTimestamp ) + self::NORMAL_MAX_LAG
                                );
                        }
 
-                       if ( $page->getLinksTimestamp() > $skewedTimestamp ) {
-                               $lbFactory->commitMasterChanges( __METHOD__ );
-                               // Something already updated the backlinks since this job was made
-                               $stats->increment( 'refreshlinks.update_skipped' );
-                               return true;
-                       }
-
-                       if ( $page->getTouched() >= $this->params['rootJobTimestamp'] || $opportunistic ) {
-                               // Cache is suspected to be up-to-date. As long as the cache rev ID matches
-                               // and it reflects the job's triggering change, then it is usable.
-                               $parserOutput = $services->getParserCache()->getDirty( $page, $parserOptions );
-                               if ( !$parserOutput
-                                       || $parserOutput->getCacheRevisionId() != $revision->getId()
-                                       || $parserOutput->getCacheTime() < $skewedTimestamp
+                       if ( $page->getTouched() >= $rootTimestamp || $opportunistic ) {
+                               // Cache is suspected to be up-to-date so it's worth the I/O of checking.
+                               // As long as the cache rev ID matches the current rev ID and it reflects
+                               // the job's triggering change, then it is usable.
+                               $parserOptions = $page->makeParserOptions( 'canonical' );
+                               $output = $parserCache->getDirty( $page, $parserOptions );
+                               if (
+                                       $output &&
+                                       $output->getCacheRevisionId() == $currentRevision->getId() &&
+                                       $output->getCacheTime() >= $lagAwareTimestamp
                                ) {
-                                       $parserOutput = false; // too stale
+                                       $cachedOutput = $output;
                                }
                        }
                }
 
-               // Fetch the current revision and parse it if necessary...
-               if ( $parserOutput ) {
+               if ( $cachedOutput ) {
                        $stats->increment( 'refreshlinks.parser_cached' );
                } else {
-                       $start = microtime( true );
-
-                       $checkCache = $page->shouldCheckParserCache( $parserOptions, $revision->getId() );
-
-                       // Revision ID must be passed to the parser output to get revision variables correct
-                       $renderedRevision = $renderer->getRenderedRevision(
-                               $revision,
-                               $parserOptions,
-                               null,
-                               [
-                                       // use master, for consistency with the getRevisionByTitle call above.
-                                       'use-master' => true,
-                                       // bypass audience checks, since we know that this is the current revision.
-                                       'audience' => RevisionRecord::RAW
-                               ]
-                       );
-                       $parserOutput = $renderedRevision->getRevisionParserOutput(
-                               // HTML is only needed if the output is to be placed in the parser cache
-                               [ 'generate-html' => $checkCache ]
-                       );
-
-                       // If it took a long time to render, then save this back to the cache to avoid
-                       // wasted CPU by other apaches or job runners. We don't want to always save to
-                       // cache as this can cause high cache I/O and LRU churn when a template changes.
-                       $elapsed = microtime( true ) - $start;
-
-                       $parseThreshold = $this->params['parseThreshold'] ?? self::PARSE_THRESHOLD_SEC;
-
-                       if ( $checkCache && $elapsed >= $parseThreshold && $parserOutput->isCacheable() ) {
-                               $ctime = wfTimestamp( TS_MW, (int)$start ); // cache time
-                               $services->getParserCache()->save(
-                                       $parserOutput, $page, $parserOptions, $ctime, $revision->getId()
-                               );
-                       }
                        $stats->increment( 'refreshlinks.parser_uncached' );
                }
 
+               return $cachedOutput;
+       }
+
+       /**
+        * @return array
+        */
+       private function getDataUpdateOptions() {
                $options = [
                        'recursive' => !empty( $this->params['useRecursiveLinksUpdate'] ),
                        // Carry over cause so the update can do extra logging
@@ -291,17 +389,7 @@ class RefreshLinksJob extends Job {
                        }
                }
 
-               $lbFactory->commitMasterChanges( __METHOD__ );
-
-               $page->doSecondaryDataUpdates( $options );
-
-               InfoAction::invalidateCache( $title );
-
-               // Commit any writes here in case this method is called in a loop.
-               // In that case, the scoped lock will fail to be acquired.
-               $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
-
-               return true;
+               return $options;
        }
 
        public function getDeduplicationInfo() {
index 3a549af..e0c81ed 100644 (file)
@@ -48,6 +48,7 @@ class MapCacheLRU implements IExpiringStore, Serializable {
        /** @var float|null */
        private $wallClockOverride;
 
+       /** @var float */
        const RANK_TOP = 1.0;
 
        /** @var int Array key that holds the entry's main timestamp (flat key use) */
@@ -103,7 +104,7 @@ class MapCacheLRU implements IExpiringStore, Serializable {
         *
         * @param string $key
         * @param mixed $value
-        * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
+        * @param float $rank Bottom fraction of the list where keys start off [default: 1.0]
         * @return void
         */
        public function set( $key, $value, $rank = self::RANK_TOP ) {
@@ -135,10 +136,11 @@ class MapCacheLRU implements IExpiringStore, Serializable {
         * Check if a key exists
         *
         * @param string $key
-        * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+        * @param float $maxAge Ignore items older than this many seconds [default: INF]
         * @return bool
+        * @since 1.32 Added $maxAge
         */
-       public function has( $key, $maxAge = 0.0 ) {
+       public function has( $key, $maxAge = INF ) {
                if ( !is_int( $key ) && !is_string( $key ) ) {
                        throw new UnexpectedValueException(
                                __METHOD__ . ': invalid key; must be string or integer.' );
@@ -157,12 +159,15 @@ class MapCacheLRU implements IExpiringStore, Serializable {
         * If the item is already set, it will be pushed to the top of the cache.
         *
         * @param string $key
-        * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
-        * @return mixed Returns null if the key was not found or is older than $maxAge
+        * @param float $maxAge Ignore items older than this many seconds [default: INF]
+        * @param mixed|null $default Value to return if no key is found [default: null]
+        * @return mixed Returns $default if the key was not found or is older than $maxAge
+        * @since 1.32 Added $maxAge
+        * @since 1.34 Added $default
         */
-       public function get( $key, $maxAge = 0.0 ) {
+       public function get( $key, $maxAge = INF, $default = null ) {
                if ( !$this->has( $key, $maxAge ) ) {
-                       return null;
+                       return $default;
                }
 
                $this->ping( $key );
@@ -201,10 +206,11 @@ class MapCacheLRU implements IExpiringStore, Serializable {
        /**
         * @param string|int $key
         * @param string|int $field
-        * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+        * @param float $maxAge Ignore items older than this many seconds [default: INF]
         * @return bool
+        * @since 1.32 Added $maxAge
         */
-       public function hasField( $key, $field, $maxAge = 0.0 ) {
+       public function hasField( $key, $field, $maxAge = INF ) {
                $value = $this->get( $key );
 
                if ( !is_int( $field ) && !is_string( $field ) ) {
@@ -222,10 +228,11 @@ class MapCacheLRU implements IExpiringStore, Serializable {
        /**
         * @param string|int $key
         * @param string|int $field
-        * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+        * @param float $maxAge Ignore items older than this many seconds [default: INF]
         * @return mixed Returns null if the key was not found or is older than $maxAge
+        * @since 1.32 Added $maxAge
         */
-       public function getField( $key, $field, $maxAge = 0.0 ) {
+       public function getField( $key, $field, $maxAge = INF ) {
                if ( !$this->hasField( $key, $field, $maxAge ) ) {
                        return null;
                }
@@ -249,12 +256,13 @@ class MapCacheLRU implements IExpiringStore, Serializable {
         * @since 1.28
         * @param string $key
         * @param callable $callback Callback that will produce the value
-        * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
-        * @param float $maxAge Ignore items older than this many seconds [Default: 0.0] (since 1.32)
+        * @param float $rank Bottom fraction of the list where keys start off [default: 1.0]
+        * @param float $maxAge Ignore items older than this many seconds [default: INF]
         * @return mixed The cached value if found or the result of $callback otherwise
+        * @since 1.32 Added $maxAge
         */
        public function getWithSetCallback(
-               $key, callable $callback, $rank = self::RANK_TOP, $maxAge = 0.0
+               $key, callable $callback, $rank = self::RANK_TOP, $maxAge = INF
        ) {
                if ( $this->has( $key, $maxAge ) ) {
                        $value = $this->get( $key );
index 53a0ca0..4ad48c7 100644 (file)
@@ -30,6 +30,7 @@
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 use Wikimedia\ScopedCallback;
+use Psr\Log\NullLogger;
 
 /**
  * @brief Base class for all file backend classes (including multi-write backends).
@@ -190,7 +191,7 @@ abstract class FileBackend implements LoggerAwareInterface {
                if ( !is_callable( $this->profiler ) ) {
                        $this->profiler = null;
                }
-               $this->logger = $config['logger'] ?? new \Psr\Log\NullLogger();
+               $this->logger = $config['logger'] ?? new NullLogger();
                $this->statusWrapper = $config['statusWrapper'] ?? null;
                $this->tmpDirectory = $config['tmpDirectory'] ?? null;
        }
index 3663637..e2a25fc 100644 (file)
@@ -747,7 +747,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::getFileXAttributes()
         * @param array $params
-        * @return array[][]
+        * @return array[][]|false
         */
        protected function doGetFileXAttributes( array $params ) {
                return [ 'headers' => [], 'metadata' => [] ]; // not supported
index e161254..7a11aeb 100644 (file)
@@ -39,6 +39,27 @@ class HTTPFileStreamer {
        // Do not try to tear down any PHP output buffers
        const STREAM_ALLOW_OB = 2;
 
+       /**
+        * Takes HTTP headers in a name => value format and converts them to the weird format
+        * expected by stream().
+        * @param string[] $headers
+        * @return array[] [ $headers, $optHeaders ]
+        * @since 1.34
+        */
+       public static function preprocessHeaders( $headers ) {
+               $rawHeaders = [];
+               $optHeaders = [];
+               foreach ( $headers as $name => $header ) {
+                       $nameLower = strtolower( $name );
+                       if ( in_array( $nameLower, [ 'range', 'if-modified-since' ], true ) ) {
+                               $optHeaders[$nameLower] = $header;
+                       } else {
+                               $rawHeaders[] = "$name: $header";
+                       }
+               }
+               return [ $rawHeaders, $optHeaders ];
+       }
+
        /**
         * @param string $path Local filesystem path to a file
         * @param array $params Options map, which includes:
index a6135ae..2e418b9 100644 (file)
@@ -57,7 +57,7 @@ class MultiHttpClient implements LoggerAwareInterface {
        /** @var float */
        protected $connTimeout = 10;
        /** @var float */
-       protected $reqTimeout = 300;
+       protected $reqTimeout = 900;
        /** @var bool */
        protected $usePipelining = false;
        /** @var int */
index d152c65..b8d3ad2 100644 (file)
@@ -4,6 +4,7 @@
  * @ingroup FileBackend
  */
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 use Wikimedia\WaitConditionLoop;
 
 /**
@@ -101,7 +102,7 @@ abstract class LockManager {
                }
                $this->session = md5( implode( '-', $random ) );
 
-               $this->logger = $config['logger'] ?? new \Psr\Log\NullLogger();
+               $this->logger = $config['logger'] ?? new NullLogger();
        }
 
        /**
index 42146f4..2462174 100644 (file)
@@ -21,6 +21,7 @@
  */
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 /**
  * Implements functions related to MIME types such as detection and mapping to file extension
@@ -199,7 +200,7 @@ EOT;
                $this->detectCallback = $params['detectCallback'] ?? null;
                $this->guessCallback = $params['guessCallback'] ?? null;
                $this->extCallback = $params['extCallback'] ?? null;
-               $this->logger = $params['logger'] ?? new \Psr\Log\NullLogger();
+               $this->logger = $params['logger'] ?? new NullLogger();
 
                $this->loadFiles();
        }
index d13626a..dce49c4 100644 (file)
@@ -407,7 +407,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @param int $flags Bitfield of BagOStuff::WRITE_* constants
         * @return bool Success
         */
-       protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
+       final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) {
                do {
                        $casToken = null; // passed by reference
                        // Get the old value and CAS token from cache
@@ -665,43 +665,50 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
        /**
         * Delete all objects expiring before a certain date.
         * @param string|int $timestamp The reference date in MW or TS_UNIX format
-        * @param callable|null $progressCallback Optional, a function which will be called
+        * @param callable|null $progress Optional, a function which will be called
         *     regularly during long-running operations with the percentage progress
         *     as the first parameter. [optional]
         * @param int $limit Maximum number of keys to delete [default: INF]
         *
-        * @return bool Success, false if unimplemented
+        * @return bool Success; false if unimplemented
         */
        public function deleteObjectsExpiringBefore(
                $timestamp,
-               callable $progressCallback = null,
+               callable $progress = null,
                $limit = INF
        ) {
-               // stub
                return false;
        }
 
        /**
         * Get an associative array containing the item for each of the keys that have items.
-        * @param string[] $keys List of keys
+        * @param string[] $keys List of keys; can be a map of (unused => key) for convenience
         * @param int $flags Bitfield; supports READ_LATEST [optional]
-        * @return array Map of (key => value) for existing keys
+        * @return mixed[] Map of (key => value) for existing keys; preserves the order of $keys
         */
        public function getMulti( array $keys, $flags = 0 ) {
-               $valuesBykey = $this->doGetMulti( $keys, $flags );
-               foreach ( $valuesBykey as $key => $value ) {
+               $foundByKey = $this->doGetMulti( $keys, $flags );
+
+               $res = [];
+               foreach ( $keys as $key ) {
                        // Resolve one blob at a time (avoids too much I/O at once)
-                       $valuesBykey[$key] = $this->resolveSegments( $key, $value );
+                       if ( array_key_exists( $key, $foundByKey ) ) {
+                               // A value should not appear in the key if a segment is missing
+                               $value = $this->resolveSegments( $key, $foundByKey[$key] );
+                               if ( $value !== false ) {
+                                       $res[$key] = $value;
+                               }
+                       }
                }
 
-               return $valuesBykey;
+               return $res;
        }
 
        /**
         * Get an associative array containing the item for each of the keys that have items.
         * @param string[] $keys List of keys
         * @param int $flags Bitfield; supports READ_LATEST [optional]
-        * @return array Map of (key => value) for existing keys
+        * @return mixed[] Map of (key => value) for existing keys
         */
        protected function doGetMulti( array $keys, $flags = 0 ) {
                $res = [];
@@ -726,11 +733,10 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @return bool Success
         * @since 1.24
         */
-       final public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
+       public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
                if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
                        throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
                }
-
                return $this->doSetMulti( $data, $exptime, $flags );
        }
 
@@ -745,7 +751,6 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
                foreach ( $data as $key => $value ) {
                        $res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
                }
-
                return $res;
        }
 
@@ -759,11 +764,10 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @return bool Success
         * @since 1.33
         */
-       final public function deleteMulti( array $keys, $flags = 0 ) {
+       public function deleteMulti( array $keys, $flags = 0 ) {
                if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
                        throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
                }
-
                return $this->doDeleteMulti( $keys, $flags );
        }
 
@@ -777,7 +781,6 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
                foreach ( $keys as $key ) {
                        $res = $this->doDelete( $key, $flags ) && $res;
                }
-
                return $res;
        }
 
@@ -853,7 +856,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @param mixed $mainValue
         * @return string|null|bool The combined string, false if missing, null on error
         */
-       protected function resolveSegments( $key, $mainValue ) {
+       final protected function resolveSegments( $key, $mainValue ) {
                if ( SerializedValueContainer::isUnified( $mainValue ) ) {
                        return $this->unserialize( $mainValue->{SerializedValueContainer::UNIFIED_DATA} );
                }
@@ -929,7 +932,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @param callable $workCallback
         * @since 1.28
         */
-       public function addBusyCallback( callable $workCallback ) {
+       final public function addBusyCallback( callable $workCallback ) {
                $this->busyCallbacks[] = $workCallback;
        }
 
@@ -938,9 +941,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         */
        protected function debug( $text ) {
                if ( $this->debugMode ) {
-                       $this->logger->debug( "{class} debug: $text", [
-                               'class' => static::class,
-                       ] );
+                       $this->logger->debug( "{class} debug: $text", [ 'class' => static::class ] );
                }
        }
 
@@ -948,7 +949,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @param int $exptime
         * @return bool
         */
-       protected function expiryIsRelative( $exptime ) {
+       final protected function expiryIsRelative( $exptime ) {
                return ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) );
        }
 
@@ -964,9 +965,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @param int $exptime Absolute TTL or 0 for indefinite
         * @return int
         */
-       protected function convertToExpiry( $exptime ) {
-               $exptime = (int)$exptime; // sanity
-
+       final protected function convertToExpiry( $exptime ) {
                return $this->expiryIsRelative( $exptime )
                        ? (int)$this->getCurrentTime() + $exptime
                        : $exptime;
@@ -979,16 +978,10 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @param int $exptime
         * @return int
         */
-       protected function convertToRelative( $exptime ) {
-               if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
-                       $exptime -= (int)$this->getCurrentTime();
-                       if ( $exptime <= 0 ) {
-                               $exptime = 1;
-                       }
-                       return $exptime;
-               } else {
-                       return $exptime;
-               }
+       final protected function convertToRelative( $exptime ) {
+               return $this->expiryIsRelative( $exptime )
+                       ? (int)$exptime
+                       : max( $exptime - (int)$this->getCurrentTime(), 1 );
        }
 
        /**
@@ -997,7 +990,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @param mixed $value
         * @return bool
         */
-       protected function isInteger( $value ) {
+       final protected function isInteger( $value ) {
                if ( is_int( $value ) ) {
                        return true;
                } elseif ( !is_string( $value ) ) {
@@ -1080,7 +1073,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @param BagOStuff[] $bags
         * @return int[] Resulting flag map (class ATTR_* constant => class QOS_* constant)
         */
-       protected function mergeFlagMaps( array $bags ) {
+       final protected function mergeFlagMaps( array $bags ) {
                $map = [];
                foreach ( $bags as $bag ) {
                        foreach ( $bag->attrMap as $attr => $rank ) {
index e193497..ea434e0 100644 (file)
  *   up going to the HashBagOStuff used for the in-memory cache).
  *
  * @ingroup Cache
- * @TODO: Make this class use composition instead of calling super
  */
-class CachedBagOStuff extends HashBagOStuff {
+class CachedBagOStuff extends BagOStuff {
        /** @var BagOStuff */
        protected $backend;
+       /** @var HashBagOStuff */
+       protected $procCache;
 
        /**
         * @param BagOStuff $backend Permanent backend to use
         * @param array $params Parameters for HashBagOStuff
         */
-       function __construct( BagOStuff $backend, $params = [] ) {
+       public function __construct( BagOStuff $backend, $params = [] ) {
                unset( $params['reportDupes'] ); // useless here
 
                parent::__construct( $params );
 
                $this->backend = $backend;
+               $this->procCache = new HashBagOStuff( $params );
                $this->attrMap = $backend->attrMap;
        }
 
-       public function get( $key, $flags = 0 ) {
-               $ret = parent::get( $key, $flags );
-               if ( $ret === false && !$this->hasKey( $key ) ) {
+       protected function doGet( $key, $flags = 0, &$casToken = null ) {
+               $ret = $this->procCache->get( $key, $flags );
+               if ( $ret === false && !$this->procCache->hasKey( $key ) ) {
                        $ret = $this->backend->get( $key, $flags );
-                       $this->set( $key, $ret, 0, self::WRITE_CACHE_ONLY );
+                       $this->set( $key, $ret, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
                }
+
                return $ret;
        }
 
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
-               parent::set( $key, $value, $exptime, $flags );
-               if ( !( $flags & self::WRITE_CACHE_ONLY ) ) {
-                       $this->backend->set( $key, $value, $exptime, $flags & ~self::WRITE_CACHE_ONLY );
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+               $this->procCache->set( $key, $value, $exptime, $flags );
+               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+                       $this->backend->set( $key, $value, $exptime, $flags );
                }
+
                return true;
        }
 
-       public function delete( $key, $flags = 0 ) {
-               parent::delete( $key, $flags );
-               if ( !( $flags & self::WRITE_CACHE_ONLY ) ) {
-                       $this->backend->delete( $key );
+       protected function doDelete( $key, $flags = 0 ) {
+               $this->procCache->delete( $key, $flags );
+               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+                       $this->backend->delete( $key, $flags );
                }
 
                return true;
        }
 
-       public function setDebug( $bool ) {
-               parent::setDebug( $bool );
-               $this->backend->setDebug( $bool );
-       }
-
        public function deleteObjectsExpiringBefore(
                $timestamp,
-               callable $progressCallback = null,
+               callable $progress = null,
                $limit = INF
        ) {
-               parent::deleteObjectsExpiringBefore( $timestamp, $progressCallback, $limit );
+               $this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
 
-               return $this->backend->deleteObjectsExpiringBefore(
-                       $timestamp,
-                       $progressCallback,
-                       $limit
-               );
-       }
-
-       public function makeKeyInternal( $keyspace, $args ) {
-               return $this->backend->makeKeyInternal( ...func_get_args() );
-       }
-
-       public function makeKey( $class, $component = null ) {
-               return $this->backend->makeKey( ...func_get_args() );
-       }
-
-       public function makeGlobalKey( $class, $component = null ) {
-               return $this->backend->makeGlobalKey( ...func_get_args() );
+               return $this->backend->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
        }
 
        // These just call the backend (tested elsewhere)
@@ -121,7 +104,8 @@ class CachedBagOStuff extends HashBagOStuff {
 
        public function incr( $key, $value = 1 ) {
                $n = $this->backend->incr( $key, $value );
-               parent::delete( $key );
+
+               $this->procCache->delete( $key );
 
                return $n;
        }
@@ -134,6 +118,23 @@ class CachedBagOStuff extends HashBagOStuff {
                return $this->backend->unlock( $key );
        }
 
+       public function makeKeyInternal( $keyspace, $args ) {
+               return $this->backend->makeKeyInternal( ...func_get_args() );
+       }
+
+       public function makeKey( $class, $component = null ) {
+               return $this->backend->makeKey( ...func_get_args() );
+       }
+
+       public function makeGlobalKey( $class, $component = null ) {
+               return $this->backend->makeGlobalKey( ...func_get_args() );
+       }
+
+       public function setDebug( $bool ) {
+               parent::setDebug( $bool );
+               $this->backend->setDebug( $bool );
+       }
+
        public function getLastError() {
                return $this->backend->getLastError();
        }
index 575bc58..6dc1363 100644 (file)
@@ -33,7 +33,7 @@ class EmptyBagOStuff extends BagOStuff {
                return false;
        }
 
-       protected function doSet( $key, $value, $exp = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
                return true;
        }
 
index 016bdfe..c74bb6e 100644 (file)
@@ -149,7 +149,7 @@ class HashBagOStuff extends BagOStuff {
         * @return bool
         * @since 1.27
         */
-       protected function hasKey( $key ) {
+       public function hasKey( $key ) {
                return isset( $this->bag[$key] );
        }
 }
index cfbf2b3..f75e780 100644 (file)
@@ -34,20 +34,6 @@ abstract class MemcachedBagOStuff extends BagOStuff {
                $this->segmentationSize = $params['maxPreferedKeySize'] ?? 917504; // < 1MiB
        }
 
-       /**
-        * Fill in some defaults for missing keys in $params.
-        *
-        * @param array $params
-        * @return array
-        */
-       protected function applyDefaultParams( $params ) {
-               return $params + [
-                       'compress_threshold' => 1500,
-                       'connect_timeout' => 0.5,
-                       'debug' => false
-               ];
-       }
-
        /**
         * Construct a cache key.
         *
index ea0090f..221bc82 100644 (file)
@@ -32,83 +32,104 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
 
        /**
         * Available parameters are:
-        *   - servers:             The list of IP:port combinations holding the memcached servers.
-        *   - persistent:          Whether to use a persistent connection
-        *   - compress_threshold:  The minimum size an object must be before it is compressed
-        *   - timeout:             The read timeout in microseconds
-        *   - connect_timeout:     The connect timeout in seconds
-        *   - retry_timeout:       Time in seconds to wait before retrying a failed connect attempt
-        *   - server_failure_limit:  Limit for server connect failures before it is removed
-        *   - serializer:          May be either "php" or "igbinary". Igbinary produces more compact
-        *                          values, but serialization is much slower unless the php.ini option
-        *                          igbinary.compact_strings is off.
-        *   - use_binary_protocol  Whether to enable the binary protocol (default is ASCII) (boolean)
+        *   - servers:              List of IP:port combinations holding the memcached servers.
+        *   - persistent:           Whether to use a persistent connection
+        *   - compress_threshold:   The minimum size an object must be before it is compressed
+        *   - timeout:              The read timeout in microseconds
+        *   - connect_timeout:      The connect timeout in seconds
+        *   - retry_timeout:        Time in seconds to wait before retrying a failed connect attempt
+        *   - server_failure_limit: Limit for server connect failures before it is removed
+        *   - serializer:           Either "php" or "igbinary". Igbinary produces more compact
+        *                           values, but serialization is much slower unless the php.ini
+        *                           option igbinary.compact_strings is off.
+        *   - use_binary_protocol   Whether to enable the binary protocol (default is ASCII)
+        *   - allow_tcp_nagle_delay Whether to permit Nagle's algorithm for reducing packet count
         * @param array $params
-        * @throws InvalidArgumentException
         */
        function __construct( $params ) {
                parent::__construct( $params );
-               $params = $this->applyDefaultParams( $params );
+
+               // Default class-specific parameters
+               $params += [
+                       'compress_threshold' => 1500,
+                       'connect_timeout' => 0.5,
+                       'serializer' => 'php',
+                       'use_binary_protocol' => false,
+                       'allow_tcp_nagle_delay' => true
+               ];
 
                if ( $params['persistent'] ) {
                        // The pool ID must be unique to the server/option combination.
                        // The Memcached object is essentially shared for each pool ID.
                        // We can only reuse a pool ID if we keep the config consistent.
-                       $this->client = new Memcached( md5( serialize( $params ) ) );
-                       if ( count( $this->client->getServerList() ) ) {
-                               $this->logger->debug( __METHOD__ . ": persistent Memcached object already loaded." );
-                               return; // already initialized; don't add duplicate servers
-                       }
+                       $connectionPoolId = md5( serialize( $params ) );
+                       $client = new Memcached( $connectionPoolId );
+                       $this->initializeClient( $client, $params );
                } else {
-                       $this->client = new Memcached;
+                       $client = new Memcached;
+                       $this->initializeClient( $client, $params );
                }
 
-               if ( $params['use_binary_protocol'] ) {
-                       $this->client->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
-               }
-
-               if ( isset( $params['retry_timeout'] ) ) {
-                       $this->client->setOption( Memcached::OPT_RETRY_TIMEOUT, $params['retry_timeout'] );
-               }
-
-               if ( isset( $params['server_failure_limit'] ) ) {
-                       $this->client->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, $params['server_failure_limit'] );
-               }
+               $this->client = $client;
 
                // The compression threshold is an undocumented php.ini option for some
                // reason. There's probably not much harm in setting it globally, for
                // compatibility with the settings for the PHP client.
                ini_set( 'memcached.compression_threshold', $params['compress_threshold'] );
+       }
 
-               // Set timeouts
-               $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 );
-               $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] );
-               $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] );
-               $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 );
+       /**
+        * Initialize the client only if needed and reuse it otherwise.
+        * This avoids duplicate servers in the list and new connections.
+        *
+        * @param Memcached $client
+        * @param array $params
+        * @throws RuntimeException
+        */
+       private function initializeClient( Memcached $client, array $params ) {
+               if ( $client->getServerList() ) {
+                       $this->logger->debug( __METHOD__ . ": pre-initialized client instance." );
 
-               // Set libketama mode since it's recommended by the documentation and
-               // is as good as any. There's no way to configure libmemcached to use
-               // hashes identical to the ones currently in use by the PHP client, and
-               // even implementing one of the libmemcached hashes in pure PHP for
-               // forwards compatibility would require MemcachedClient::get_sock() to be
-               // rewritten.
-               $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
+                       return; // preserve persistent handle
+               }
 
-               // Set the serializer
-               $ok = false;
+               $this->logger->debug( __METHOD__ . ": initializing new client instance." );
+
+               $options = [
+                       // Network protocol (ASCII or binary)
+                       Memcached::OPT_BINARY_PROTOCOL => $params['use_binary_protocol'],
+                       // Set various network timeouts
+                       Memcached::OPT_CONNECT_TIMEOUT => $params['connect_timeout'] * 1000,
+                       Memcached::OPT_SEND_TIMEOUT => $params['timeout'],
+                       Memcached::OPT_RECV_TIMEOUT => $params['timeout'],
+                       Memcached::OPT_POLL_TIMEOUT => $params['timeout'] / 1000,
+                       // Avoid pointless delay when sending/fetching large blobs
+                       Memcached::OPT_TCP_NODELAY => !$params['allow_tcp_nagle_delay'],
+                       // Set libketama mode since it's recommended by the documentation
+                       Memcached::OPT_LIBKETAMA_COMPATIBLE => true
+               ];
+               if ( isset( $params['retry_timeout'] ) ) {
+                       $options[Memcached::OPT_RETRY_TIMEOUT] = $params['retry_timeout'];
+               }
+               if ( isset( $params['server_failure_limit'] ) ) {
+                       $options[Memcached::OPT_SERVER_FAILURE_LIMIT] = $params['server_failure_limit'];
+               }
                if ( $params['serializer'] === 'php' ) {
-                       $ok = $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
+                       $options[Memcached::OPT_SERIALIZER] = Memcached::SERIALIZER_PHP;
                } elseif ( $params['serializer'] === 'igbinary' ) {
                        if ( !Memcached::HAVE_IGBINARY ) {
-                               throw new InvalidArgumentException(
+                               throw new RuntimeException(
                                        __CLASS__ . ': the igbinary extension is not available ' .
                                        'but igbinary serialization was requested.'
                                );
                        }
-                       $ok = $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
+                       $options[Memcached::OPT_SERIALIZER] = Memcached::SERIALIZER_IGBINARY;
                }
-               if ( !$ok ) {
-                       throw new InvalidArgumentException( __CLASS__ . ': invalid serializer parameter' );
+
+               if ( !$client->setOptions( $options ) ) {
+                       throw new RuntimeException(
+                               "Invalid options: " . json_encode( $options, JSON_PRETTY_PRINT )
+                       );
                }
 
                $servers = [];
@@ -121,26 +142,16 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                                $servers[] = [ $host, false ]; // (ip or path, port)
                        }
                }
-               $this->client->addServers( $servers );
-       }
-
-       protected function applyDefaultParams( $params ) {
-               $params = parent::applyDefaultParams( $params );
-
-               if ( !isset( $params['use_binary_protocol'] ) ) {
-                       $params['use_binary_protocol'] = false;
-               }
 
-               if ( !isset( $params['serializer'] ) ) {
-                       $params['serializer'] = 'php';
+               if ( !$client->addServers( $servers ) ) {
+                       throw new RuntimeException( "Failed to inject server address list" );
                }
-
-               return $params;
        }
 
        protected function doGet( $key, $flags = 0, &$casToken = null ) {
                $this->debug( "get($key)" );
                if ( defined( Memcached::class . '::GET_EXTENDED' ) ) { // v3.0.0
+                       /** @noinspection PhpUndefinedClassConstantInspection */
                        $flags = Memcached::GET_EXTENDED;
                        $res = $this->client->get( $this->validateKeyEncoding( $key ), null, $flags );
                        if ( is_array( $res ) ) {
@@ -250,7 +261,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $result;
        }
 
-       public function doGetMulti( array $keys, $flags = 0 ) {
+       protected function doGetMulti( array $keys, $flags = 0 ) {
                $this->debug( 'getMulti(' . implode( ', ', $keys ) . ')' );
                foreach ( $keys as $key ) {
                        $this->validateKeyEncoding( $key );
@@ -259,7 +270,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $this->checkResult( false, $result );
        }
 
-       public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+       protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
                $this->debug( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
                foreach ( array_keys( $data ) as $key ) {
                        $this->validateKeyEncoding( $key );
@@ -268,7 +279,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $this->checkResult( false, $result );
        }
 
-       public function doDeleteMulti( array $keys, $flags = 0 ) {
+       protected function doDeleteMulti( array $keys, $flags = 0 ) {
                $this->debug( 'deleteMulti(' . implode( ', ', $keys ) . ')' );
                foreach ( $keys as $key ) {
                        $this->validateKeyEncoding( $key );
index 5a67a0d..f8b91bc 100644 (file)
@@ -33,7 +33,6 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
        /**
         * Available parameters are:
         *   - servers:             The list of IP:port combinations holding the memcached servers.
-        *   - debug:               Whether to set the debug flag in the underlying client.
         *   - persistent:          Whether to use a persistent connection
         *   - compress_threshold:  The minimum size an object must be before it is compressed
         *   - timeout:             The read timeout in microseconds
@@ -43,11 +42,15 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
         */
        function __construct( $params ) {
                parent::__construct( $params );
-               $params = $this->applyDefaultParams( $params );
+
+               // Default class-specific parameters
+               $params += [
+                       'compress_threshold' => 1500,
+                       'connect_timeout' => 0.5
+               ];
 
                $this->client = new MemcachedClient( $params );
                $this->client->set_servers( $params['servers'] );
-               $this->client->set_debug( $params['debug'] );
        }
 
        public function setDebug( $debug ) {
@@ -108,7 +111,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
                );
        }
 
-       public function doGetMulti( array $keys, $flags = 0 ) {
+       protected function doGetMulti( array $keys, $flags = 0 ) {
                foreach ( $keys as $key ) {
                        $this->validateKeyEncoding( $key );
                }
index 2df8f0c..8e791ba 100644 (file)
@@ -210,12 +210,12 @@ class MultiWriteBagOStuff extends BagOStuff {
 
        public function deleteObjectsExpiringBefore(
                $timestamp,
-               callable $progressCallback = null,
+               callable $progress = null,
                $limit = INF
        ) {
                $ret = false;
                foreach ( $this->caches as $cache ) {
-                       if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progressCallback, $limit ) ) {
+                       if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit ) ) {
                                $ret = true;
                        }
                }
@@ -236,7 +236,7 @@ class MultiWriteBagOStuff extends BagOStuff {
                return $res;
        }
 
-       public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+       public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
                return $this->doWrite(
                        $this->cacheIndexes,
                        $this->usesAsyncWritesGivenFlags( $flags ),
@@ -245,7 +245,16 @@ class MultiWriteBagOStuff extends BagOStuff {
                );
        }
 
-       public function doDeleteMulti( array $data, $flags = 0 ) {
+       public function deleteMulti( array $data, $flags = 0 ) {
+               return $this->doWrite(
+                       $this->cacheIndexes,
+                       $this->usesAsyncWritesGivenFlags( $flags ),
+                       __FUNCTION__,
+                       func_get_args()
+               );
+       }
+
+       public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
                return $this->doWrite(
                        $this->cacheIndexes,
                        $this->usesAsyncWritesGivenFlags( $flags ),
@@ -370,11 +379,19 @@ class MultiWriteBagOStuff extends BagOStuff {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
 
+       protected function doSetMulti( array $keys, $exptime = 0, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function doDeleteMulti( array $keys, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
        protected function serialize( $value ) {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
 
-       protected function unserialize( $value ) {
+       protected function unserialize( $blob ) {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
 }
diff --git a/includes/libs/objectcache/README.md b/includes/libs/objectcache/README.md
new file mode 100644 (file)
index 0000000..42bf636
--- /dev/null
@@ -0,0 +1,116 @@
+# wikimedia/objectcache
+
+## Statistics
+
+Sent to StatsD under MediaWiki's namespace.
+
+### WANObjectCache
+
+The default WANObjectCache provided by MediaWikiServices disables these
+statistics in processes where `$wgCommandLineMode` is true.
+
+#### `wanobjectcache.{kClass}.{cache_action_and_result}`
+
+Call counter from `WANObjectCache::getWithSetCallback()`.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+* Variable `result`: One of:
+  * `"hit.good"`,
+  * `"hit.refresh"`,
+  * `"hit.volatile"`,
+  * `"hit.stale"`,
+  * `"miss.busy"` (or `"renew.busy"`, if the `minAsOf` is used),
+  * `"miss.compute"` (or `"renew.busy"`, if the `minAsOf` is used).
+
+#### `wanobjectcache.{kClass}.regen_set_delay`
+
+Upon cache miss, this measures the time spent in `WANObjectCache::getWithSetCallback()`,
+from the start of the method to right after the new value has been computed by the callback.
+
+This essentially measures the whole method (including retrieval of any old value,
+validation, any locks for `lockTSE`, and the callbacks), except for the time spent
+in sending the value to the backend server.
+
+* Type: Measure (in milliseconds).
+* Variable `kClass`: The first part of your cache key.
+
+#### `wanobjectcache.{kClass}.regen_walltime`
+
+Upon cache miss, this measures the time spent in `WANObjectCache::getWithSetCallback()`
+from the start of the callback to right after the new value has been computed.
+
+* Type: Measure (in milliseconds).
+* Variable `kClass`: The first part of your cache key.
+
+#### `wanobjectcache.{kClass}.ck_touch.{result}`
+
+Call counter from `WANObjectCache::touchCheckKey()`.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+* Variable `result`: One of `"ok"` or `"error"`.
+
+#### `wanobjectcache.{kClass}.ck_reset.{result}`
+
+Call counter from `WANObjectCache::resetCheckKey()`.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+* Variable `result`: One of `"ok"` or `"error"`.
+
+#### `wanobjectcache.{kClass}.delete.{result}`
+
+Call counter from `WANObjectCache::delete()`.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+* Variable `result`: One of `"ok"` or `"error"`.
+
+#### `wanobjectcache.{kClass}.cooloff_bounce`
+
+Upon a cache miss, the `WANObjectCache::getWithSetCallback()` method generally
+recomputes the value from the callback, and stores it for re-use.
+
+If regenerating the value costs more than a certain threshold of time (e.g. 50ms),
+then for popular keys it is likely that many web servers will generate and store
+the value simultaneously when the key is entirely absent from the cache. In this case,
+the cool-off feature can be used to protect backend cache servers against network
+congestion. This protection is implemented with a lock and subsequent cool-off period.
+The winner stores their value, while other web server return their value directly.
+
+This counter is incremented whenever a new value was regenerated but not stored.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+
+When the regeneration callback is slow, these scenarios may use the cool-off feature:
+
+* Storing the first interim value for tombstoned keys.
+
+  If a key is currently tombstoned due to a recent `delete()` action, and thus in "hold-off", then
+  the key may not be written to. A mutex lock will let one web server generate the new value and
+  (until the hold-off is over) the generated value will be considered an interim (temporary) value
+  only. Requests that cannot get the lock will use the last stored interim value.
+  If there is no interim value yet, then requests that cannot get the lock may still generate their
+  own value. Here, the cool-off feature is used to decide which requests stores their interim value.
+
+* Storing the first interim value for stale keys.
+
+  If a key is currently in "hold-off" due to a recent `touchCheckKey()` action, then the key may
+  not be written to. A mutex lock will let one web request generate the new value and (until the
+  hold-off is over) such value will be considered an interim (temporary) value only. Requests that
+  lose the lock, will instead return the last stored interim value, or (if it remained in cache) the
+  stale value preserved from before `touchCheckKey()` was called.
+  If there is no stale value and no interim value yet, then multiple requests may need to
+  generate the value simultaneously. In this case, the cool-off feature is used to decide
+  which requests store their interim value.
+
+  The same logic applies when the callback passed to getWithSetCallback() in the "touchedCallback"
+  parameter starts returning an updated timestamp due to a dependency change.
+
+* Storing the first value when `lockTSE` is used.
+
+  When `lockTSE` is in use, and no stale value is found on the backend, and no `busyValue`
+  callback is provided, then multiple requests may generate the value simultaneously;
+  the cool-off is used to decide which requests store their interim value.
index f67b887..a72b3ff 100644 (file)
@@ -106,17 +106,16 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       protected function doSet( $key, $value, $expiry = 0, $flags = 0 ) {
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
                }
-               $expiry = $this->convertToRelative( $expiry );
+               $ttl = $this->convertToRelative( $exptime );
                try {
-                       if ( $expiry ) {
-                               $result = $conn->setex( $key, $expiry, $this->serialize( $value ) );
+                       if ( $ttl ) {
+                               $result = $conn->setex( $key, $ttl, $this->serialize( $value ) );
                        } else {
-                               // No expiry, that is very different from zero expiry in Redis
                                $result = $conn->set( $key, $this->serialize( $value ) );
                        }
                } catch ( RedisException $e ) {
@@ -146,7 +145,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function doGetMulti( array $keys, $flags = 0 ) {
+       protected function doGetMulti( array $keys, $flags = 0 ) {
                $batches = [];
                $conns = [];
                foreach ( $keys as $key ) {
@@ -185,7 +184,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function doSetMulti( array $data, $expiry = 0, $flags = 0 ) {
+       protected function doSetMulti( array $data, $expiry = 0, $flags = 0 ) {
                $batches = [];
                $conns = [];
                foreach ( $data as $key => $value ) {
@@ -215,11 +214,7 @@ class RedisBagOStuff extends BagOStuff {
                                        $this->debug( "setMulti request to $server failed" );
                                        continue;
                                }
-                               foreach ( $batchResult as $value ) {
-                                       if ( $value === false ) {
-                                               $result = false;
-                                       }
-                               }
+                               $result = $result && !in_array( false, $batchResult, true );
                        } catch ( RedisException $e ) {
                                $this->handleException( $conn, $e );
                                $result = false;
@@ -229,7 +224,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function doDeleteMulti( array $keys, $flags = 0 ) {
+       protected function doDeleteMulti( array $keys, $flags = 0 ) {
                $batches = [];
                $conns = [];
                foreach ( $keys as $key ) {
@@ -254,11 +249,7 @@ class RedisBagOStuff extends BagOStuff {
                                        $this->debug( "deleteMulti request to $server failed" );
                                        continue;
                                }
-                               foreach ( $batchResult as $value ) {
-                                       if ( $value === false ) {
-                                               $result = false;
-                                       }
-                               }
+                               $result = $result && !in_array( false, $batchResult, true );
                        } catch ( RedisException $e ) {
                                $this->handleException( $conn, $e );
                                $result = false;
@@ -273,55 +264,86 @@ class RedisBagOStuff extends BagOStuff {
                if ( !$conn ) {
                        return false;
                }
-               $expiry = $this->convertToRelative( $expiry );
+
+               $ttl = $this->convertToRelative( $expiry );
                try {
-                       if ( $expiry ) {
-                               $result = $conn->set(
-                                       $key,
-                                       $this->serialize( $value ),
-                                       [ 'nx', 'ex' => $expiry ]
-                               );
-                       } else {
-                               $result = $conn->setnx( $key, $this->serialize( $value ) );
-                       }
+                       $result = $conn->set(
+                               $key,
+                               $this->serialize( $value ),
+                               $ttl ? [ 'nx', 'ex' => $ttl ] : [ 'nx' ]
+                       );
                } catch ( RedisException $e ) {
                        $result = false;
                        $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'add', $key, $server, $result );
+
                return $result;
        }
 
-       /**
-        * Non-atomic implementation of incr().
-        *
-        * Probably all callers actually want incr() to atomically initialise
-        * values to zero if they don't exist, as provided by the Redis INCR
-        * command. But we are constrained by the memcached-like interface to
-        * return null in that case. Once the key exists, further increments are
-        * atomic.
-        * @param string $key Key to increase
-        * @param int $value Value to add to $key (Default 1)
-        * @return int|bool New value or false on failure
-        */
        public function incr( $key, $value = 1 ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
                }
+
                try {
-                       if ( !$conn->exists( $key ) ) {
-                               return false;
+                       $conn->watch( $key );
+                       if ( $conn->exists( $key ) ) {
+                               $conn->multi( Redis::MULTI );
+                               $conn->incrBy( $key, $value );
+                               $batchResult = $conn->exec();
+                               if ( $batchResult === false ) {
+                                       $result = false;
+                               } else {
+                                       $result = end( $batchResult );
+                               }
+                       } else {
+                               $result = false;
+                               $conn->unwatch();
                        }
-                       // @FIXME: on races, the key may have a 0 TTL
-                       $result = $conn->incrBy( $key, $value );
                } catch ( RedisException $e ) {
+                       try {
+                               $conn->unwatch(); // sanity
+                       } catch ( RedisException $ex ) {
+                               // already errored
+                       }
                        $result = false;
                        $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'incr', $key, $server, $result );
+
+               return $result;
+       }
+
+       public function incrWithInit( $key, $exptime, $value = 1, $init = 1 ) {
+               list( $server, $conn ) = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+
+               $ttl = $this->convertToRelative( $exptime );
+               $preIncrInit = $init - $value;
+               try {
+                       $conn->multi( Redis::MULTI );
+                       $conn->set( $key, $preIncrInit, $ttl ? [ 'nx', 'ex' => $ttl ] : [ 'nx' ] );
+                       $conn->incrBy( $key, $value );
+                       $batchResult = $conn->exec();
+                       if ( $batchResult === false ) {
+                               $result = false;
+                               $this->debug( "incrWithInit request to $server failed" );
+                       } else {
+                               $result = end( $batchResult );
+                       }
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'incr', $key, $server, $result );
+
                return $result;
        }
 
index 8c2b3f6..295ec30 100644 (file)
@@ -110,14 +110,10 @@ class ReplicatedBagOStuff extends BagOStuff {
 
        public function deleteObjectsExpiringBefore(
                $timestamp,
-               callable $progressCallback = null,
+               callable $progress = null,
                $limit = INF
        ) {
-               return $this->writeStore->deleteObjectsExpiringBefore(
-                       $timestamp,
-                       $progressCallback,
-                       $limit
-               );
+               return $this->writeStore->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
        }
 
        public function getMulti( array $keys, $flags = 0 ) {
@@ -126,14 +122,18 @@ class ReplicatedBagOStuff extends BagOStuff {
                        : $this->readStore->getMulti( $keys, $flags );
        }
 
-       public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+       public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
                return $this->writeStore->setMulti( $data, $exptime, $flags );
        }
 
-       public function doDeleteMulti( array $keys, $flags = 0 ) {
+       public function deleteMulti( array $keys, $flags = 0 ) {
                return $this->writeStore->deleteMulti( $keys, $flags );
        }
 
+       public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+               return $this->writeStore->changeTTLMulti( $keys, $exptime, $flags );
+       }
+
        public function incr( $key, $value = 1 ) {
                return $this->writeStore->incr( $key, $value );
        }
@@ -189,6 +189,14 @@ class ReplicatedBagOStuff extends BagOStuff {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
 
+       protected function doSetMulti( array $keys, $exptime = 0, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
+       protected function doDeleteMulti( array $keys, $flags = 0 ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
        protected function serialize( $value ) {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
index 2c533b9..660d850 100644 (file)
@@ -134,6 +134,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        protected $asyncHandler;
        /** @var float Unix timestamp of the oldest possible valid values */
        protected $epoch;
+       /** @var string Stable secret used for hasing long strings into key components */
+       protected $secret;
 
        /** @var int Callback stack depth for getWithSetCallback() */
        private $callbackDepth = 0;
@@ -188,31 +190,40 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
        /** Idiom for getWithSetCallback() meaning "no minimum required as-of timestamp" */
        const MIN_TIMESTAMP_NONE = 0.0;
+       /** @var int One second into the UNIX timestamp epoch */
+       const EPOCH_UNIX_ONE_SECOND = 1.0;
 
        /** Tiny negative float to use when CTL comes up >= 0 due to clock skew */
        const TINY_NEGATIVE = -0.000001;
        /** Tiny positive float to use when using "minTime" to assert an inequality */
        const TINY_POSTIVE = 0.000001;
 
-       /** Milliseconds of delay after get() where set() storms are a consideration with 'lockTSE' */
+       /** Milliseconds of delay after get() where set() storms are a consideration with "lockTSE" */
        const SET_DELAY_HIGH_MS = 50;
        /** Min millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */
        const RECENT_SET_LOW_MS = 50;
        /** Max millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */
        const RECENT_SET_HIGH_MS = 100;
 
+       /** @var int Seconds needed for value generation considered slow */
+       const GENERATION_SLOW_SEC = 3;
+
        /** Parameter to get()/getMulti() to return extra information by reference */
        const PASS_BY_REF = -1;
 
        /** Cache format version number */
        const VERSION = 1;
 
-       const FLD_VERSION = 0; // key to cache version number
+       const FLD_FORMAT_VERSION = 0; // key to WAN cache version number
        const FLD_VALUE = 1; // key to the cached value
        const FLD_TTL = 2; // key to the original TTL
-       const FLD_TIME = 3; // key to the cache time
+       const FLD_TIME = 3; // key to the cache timestamp
        const FLD_FLAGS = 4; // key to the flags bitfield (reserved number)
-       const FLD_HOLDOFF = 5; // key to any hold-off TTL
+       const FLD_VALUE_VERSION = 5; // key to collection cache version number
+       const FLD_GENERATION_TIME = 6; // key to how long it took to generate the value
+
+       const PURGE_TIME = 0; // key to the tombstone entry timestamp
+       const PURGE_HOLDOFF = 1; // key to the tombstone entry hold-off TTL
 
        const VALUE_KEY_PREFIX = 'WANCache:v:';
        const INTERIM_KEY_PREFIX = 'WANCache:i:';
@@ -222,9 +233,6 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
        const PURGE_VAL_PREFIX = 'PURGED:';
 
-       const VFLD_DATA = 'WOC:d'; // key to the value of versioned data
-       const VFLD_VERSION = 'WOC:v'; // key to the version of the value present
-
        const PC_PRIMARY = 'primary:1000'; // process cache name and max key count
 
        /**
@@ -250,13 +258,15 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *       is configured to interpret /<region>/<cluster>/ key prefixes as routes. This
         *       requires that "region" and "cluster" are both set above. [optional]
         *   - epoch: lowest UNIX timestamp a value/tombstone must have to be valid. [optional]
+        *   - secret: stable secret used for hashing long strings into key components. [optional]
         */
        public function __construct( array $params ) {
                $this->cache = $params['cache'];
                $this->region = $params['region'] ?? 'main';
                $this->cluster = $params['cluster'] ?? 'wan-main';
                $this->mcrouterAware = !empty( $params['mcrouterAware'] );
-               $this->epoch = $params['epoch'] ?? 1.0;
+               $this->epoch = $params['epoch'] ?? self::EPOCH_UNIX_ONE_SECOND;
+               $this->secret = $params['secret'] ?? (string)$this->epoch;
 
                $this->setLogger( $params['logger'] ?? new NullLogger() );
                $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
@@ -314,17 +324,18 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * Consider using getWithSetCallback() instead of get() and set() cycles.
         * That method has cache slam avoiding features for hot/expensive keys.
         *
-        * Pass $info as WANObjectCache::PASS_BY_REF to transform it into a cache key info map.
+        * Pass $info as WANObjectCache::PASS_BY_REF to transform it into a cache key metadata map.
         * This map includes the following metadata:
         *   - asOf: UNIX timestamp of the value or null if the key is nonexistant
         *   - tombAsOf: UNIX timestamp of the tombstone or null if the key is not tombstoned
         *   - lastCKPurge: UNIX timestamp of the highest check key or null if none provided
+        *   - version: cached value version number or null if the key is nonexistant
         *
         * Otherwise, $info will transform into the cached value timestamp.
         *
         * @param string $key Cache key made from makeKey() or makeGlobalKey()
         * @param mixed|null &$curTTL Approximate TTL left on the key if present/tombstoned [returned]
-        * @param array $checkKeys List of "check" keys
+        * @param string[] $checkKeys The "check" keys used to validate the value
         * @param mixed|null &$info Key info if WANObjectCache::PASS_BY_REF [returned]
         * @return mixed Value of cache key or false on failure
         */
@@ -339,7 +350,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                        $info = [
                                'asOf' => $infoByKey[$key]['asOf'] ?? null,
                                'tombAsOf' => $infoByKey[$key]['tombAsOf'] ?? null,
-                               'lastCKPurge' => $infoByKey[$key]['lastCKPurge'] ?? null
+                               'lastCKPurge' => $infoByKey[$key]['lastCKPurge'] ?? null,
+                               'version' => $infoByKey[$key]['version'] ?? null
                        ];
                } else {
                        $info = $infoByKey[$key]['asOf'] ?? null; // b/c
@@ -352,20 +364,23 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * Fetch the value of several keys from cache
         *
         * Pass $info as WANObjectCache::PASS_BY_REF to transform it into a map of cache keys
-        * to cache key info maps, each having the same style as those of WANObjectCache::get().
+        * to cache key metadata maps, each having the same style as those of WANObjectCache::get().
         * All the cache keys listed in $keys will have an entry.
         *
         * Othwerwise, $info will transform into a map of (cache key => cached value timestamp).
         * Only the cache keys listed in $keys that exists or are tombstoned will have an entry.
         *
+        * $checkKeys holds the "check" keys used to validate values of applicable keys. The integer
+        * indexes hold "check" keys that apply to all of $keys while the string indexes hold "check"
+        * keys that only apply to the cache key with that name.
+        *
         * @see WANObjectCache::get()
         *
-        * @param array $keys List of cache keys made from makeKey() or makeGlobalKey()
+        * @param string[] $keys List of cache keys made from makeKey() or makeGlobalKey()
         * @param mixed|null &$curTTLs Map of (key => TTL left) for existing/tombstoned keys [returned]
-        * @param array $checkKeys List of check keys to apply to all $keys. May also apply "check"
-        *  keys to specific cache keys only by using cache keys as keys in the $checkKeys array.
+        * @param string[]|string[][] $checkKeys Map of (integer or cache key => "check" key(s))
         * @param mixed|null &$info Map of (key => info) if WANObjectCache::PASS_BY_REF [returned]
-        * @return array Map of (key => value) for keys that exist and are not tombstoned
+        * @return mixed[] Map of (key => value) for existing values; order of $keys is preserved
         */
        final public function getMulti(
                array $keys,
@@ -420,9 +435,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                // Get the main cache value for each key and validate them
                foreach ( $valueKeys as $vKey ) {
                        $key = substr( $vKey, $vPrefixLen ); // unprefix
-                       list( $value, $curTTL, $asOf, $tombAsOf ) = isset( $wrappedValues[$vKey] )
-                               ? $this->unwrap( $wrappedValues[$vKey], $now )
-                               : [ false, null, null, null ]; // not found
+                       list( $value, $keyInfo ) = $this->unwrap( $wrappedValues[$vKey] ?? false, $now );
                        // Force dependent keys to be seen as stale for a while after purging
                        // to reduce race conditions involving stale data getting cached
                        $purgeValues = $purgeValuesForAll;
@@ -432,26 +445,27 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
                        $lastCKPurge = null; // timestamp of the highest check key
                        foreach ( $purgeValues as $purge ) {
-                               $lastCKPurge = max( $purge[self::FLD_TIME], $lastCKPurge );
-                               $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF];
-                               if ( $value !== false && $safeTimestamp >= $asOf ) {
+                               $lastCKPurge = max( $purge[self::PURGE_TIME], $lastCKPurge );
+                               $safeTimestamp = $purge[self::PURGE_TIME] + $purge[self::PURGE_HOLDOFF];
+                               if ( $value !== false && $safeTimestamp >= $keyInfo['asOf'] ) {
                                        // How long ago this value was invalidated by *this* check key
-                                       $ago = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
+                                       $ago = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
                                        // How long ago this value was invalidated by *any* known check key
-                                       $curTTL = min( $curTTL, $ago );
+                                       $keyInfo['curTTL'] = min( $keyInfo['curTTL'], $ago );
                                }
                        }
+                       $keyInfo[ 'lastCKPurge'] = $lastCKPurge;
 
                        if ( $value !== false ) {
                                $result[$key] = $value;
                        }
-                       if ( $curTTL !== null ) {
-                               $curTTLs[$key] = $curTTL;
+                       if ( $keyInfo['curTTL'] !== null ) {
+                               $curTTLs[$key] = $keyInfo['curTTL'];
                        }
 
                        $infoByKey[$key] = ( $info === self::PASS_BY_REF )
-                               ? [ 'asOf' => $asOf, 'tombAsOf' => $tombAsOf, 'lastCKPurge' => $lastCKPurge ]
-                               : $asOf; // b/c
+                               ? $keyInfo
+                               : $keyInfo['asOf']; // b/c
                }
 
                $info = $infoByKey;
@@ -461,10 +475,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
        /**
         * @since 1.27
-        * @param array $timeKeys List of prefixed time check keys
-        * @param array $wrappedValues
+        * @param string[] $timeKeys List of prefixed time check keys
+        * @param mixed[] $wrappedValues
         * @param float $now
-        * @return array List of purge value arrays
+        * @return array[] List of purge value arrays
         */
        private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
                $purgeValues = [];
@@ -520,8 +534,9 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @param mixed $value
         * @param int $ttl Seconds to live. Special values are:
         *   - WANObjectCache::TTL_INDEFINITE: Cache forever (default)
+        *   - WANObjectCache::TTL_UNCACHEABLE: Do not cache (if the key exists, it is not deleted)
         * @param array $opts Options map:
-        *   - lag: seconds of replica DB lag. Typically, this is either the replica DB lag
+        *   - lag: Seconds of replica DB lag. Typically, this is either the replica DB lag
         *      before the data was read or, if applicable, the replica DB lag before
         *      the snapshot-isolated transaction the data was read from started.
         *      Use false to indicate that replication is not running.
@@ -530,37 +545,48 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *      the current time the data was read or (if applicable) the time when
         *      the snapshot-isolated transaction the data was read from started.
         *      Default: 0 seconds
-        *   - pending: whether this data is possibly from an uncommitted write transaction.
+        *   - pending: Whether this data is possibly from an uncommitted write transaction.
         *      Generally, other threads should not see values from the future and
         *      they certainly should not see ones that ended up getting rolled back.
         *      Default: false
-        *   - lockTSE: if excessive replication/snapshot lag is detected, then store the value
+        *   - lockTSE: If excessive replication/snapshot lag is detected, then store the value
         *      with this TTL and flag it as stale. This is only useful if the reads for this key
         *      use getWithSetCallback() with "lockTSE" set. Note that if "staleTTL" is set
         *      then it will still add on to this TTL in the excessive lag scenario.
         *      Default: WANObjectCache::TSE_NONE
-        *   - staleTTL: seconds to keep the key around if it is stale. The get()/getMulti()
+        *   - staleTTL: Seconds to keep the key around if it is stale. The get()/getMulti()
         *      methods return such stale values with a $curTTL of 0, and getWithSetCallback()
         *      will call the regeneration callback in such cases, passing in the old value
         *      and its as-of time to the callback. This is useful if adaptiveTTL() is used
         *      on the old value's as-of time when it is verified as still being correct.
-        *      Default: WANObjectCache::STALE_TTL_NONE.
-        *   - creating: optimize for the case where the key does not already exist.
+        *      Default: WANObjectCache::STALE_TTL_NONE
+        *   - creating: Optimize for the case where the key does not already exist.
         *      Default: false
+        *   - version: Integer version number signifiying the format of the value.
+        *      Default: null
+        *   - walltime: How long the value took to generate in seconds. Default: 0.0
         * @note Options added in 1.28: staleTTL
         * @note Options added in 1.33: creating
+        * @note Options added in 1.34: version, walltime
         * @return bool Success
         */
        final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
                $now = $this->getCurrentTime();
+               $lag = $opts['lag'] ?? 0;
+               $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
+               $pending = $opts['pending'] ?? false;
                $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
                $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
-               $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
                $creating = $opts['creating'] ?? false;
-               $lag = $opts['lag'] ?? 0;
+               $version = $opts['version'] ?? null;
+               $walltime = $opts['walltime'] ?? 0.0;
+
+               if ( $ttl < 0 ) {
+                       return true;
+               }
 
                // Do not cache potentially uncommitted data as it might get rolled back
-               if ( !empty( $opts['pending'] ) ) {
+               if ( $pending ) {
                        $this->logger->info(
                                'Rejected set() for {cachekey} due to pending writes.',
                                [ 'cachekey' => $key ]
@@ -619,7 +645,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                }
 
                // Wrap that value with time/TTL/version metadata
-               $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $now );
+               $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
                $storeTTL = $ttl + $staleTTL;
 
                if ( $creating ) {
@@ -795,7 +821,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @see WANObjectCache::getCheckKeyTime()
         * @see WANObjectCache::getWithSetCallback()
         *
-        * @param array $keys
+        * @param string[] $keys
         * @return float[] Map of (key => UNIX timestamp)
         * @since 1.31
         */
@@ -812,7 +838,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                foreach ( $rawKeys as $key => $rawKey ) {
                        $purge = $this->parsePurgeValue( $rawValues[$rawKey] );
                        if ( $purge !== false ) {
-                               $time = $purge[self::FLD_TIME];
+                               $time = $purge[self::PURGE_TIME];
                        } else {
                                // Casting assures identical floats for the next getCheckKeyTime() calls
                                $now = (string)$this->getCurrentTime();
@@ -1216,62 +1242,36 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                $version = $opts['version'] ?? null;
                $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
 
-               // Try the process cache if enabled and the cache callback is not within a cache callback.
-               // Process cache use in nested callbacks is not lag-safe with regard to HOLDOFF_TTL since
-               // the in-memory value is further lagged than the shared one since it uses a blind TTL.
+               // Use the process cache if requested as long as no outer cache callback is running.
+               // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
+               // process cached values are more lagged than persistent ones as they are not purged.
                if ( $pcTTL >= 0 && $this->callbackDepth == 0 ) {
-                       $procCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
-                       if ( $procCache->has( $key, $pcTTL ) ) {
-                               return $procCache->get( $key );
+                       $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
+                       $cached = $pCache->get( $this->getProcessCacheKey( $key, $version ), INF, false );
+                       if ( $cached !== false ) {
+                               return $cached;
                        }
                } else {
-                       $procCache = null;
+                       $pCache = null;
                }
 
-               if ( $version !== null ) {
-                       $curAsOf = self::PASS_BY_REF;
-                       $curValue = $this->doGetWithSetCallback(
-                               $key,
+               $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
+               list( $value, $valueVersion, $curAsOf ) = $res;
+               if ( $valueVersion !== $version ) {
+                       // Current value has a different version; use the variant key for this version.
+                       // Regenerate the variant value if it is not newer than the main value at $key
+                       // so that purges to the main key propagate to the variant value.
+                       list( $value ) = $this->fetchOrRegenerate(
+                               $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
                                $ttl,
-                               // Wrap the value in an array with version metadata but hide it from $callback
-                               function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, $version ) {
-                                       if ( $this->isVersionedValue( $oldValue, $version ) ) {
-                                               $oldData = $oldValue[self::VFLD_DATA];
-                                       } else {
-                                               // VFLD_DATA is not set if an old, unversioned, key is present
-                                               $oldData = false;
-                                               $oldAsOf = null;
-                                       }
-
-                                       return [
-                                               self::VFLD_DATA => $callback( $oldData, $ttl, $setOpts, $oldAsOf ),
-                                               self::VFLD_VERSION => $version
-                                       ];
-                               },
-                               $opts,
-                               $curAsOf
+                               $callback,
+                               [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts
                        );
-                       if ( $this->isVersionedValue( $curValue, $version ) ) {
-                               // Current value has the requested version; use it
-                               $value = $curValue[self::VFLD_DATA];
-                       } else {
-                               // Current value has a different version; use the variant key for this version.
-                               // Regenerate the variant value if it is not newer than the main value at $key
-                               // so that purges to they key propagate to the variant value.
-                               $value = $this->doGetWithSetCallback(
-                                       $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
-                                       $ttl,
-                                       $callback,
-                                       [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts
-                               );
-                       }
-               } else {
-                       $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
                }
 
                // Update the process cache if enabled
-               if ( $procCache && $value !== false ) {
-                       $procCache->set( $key, $value );
+               if ( $pCache && $value !== false ) {
+                       $pCache->set( $this->getProcessCacheKey( $key, $version ), $value );
                }
 
                return $value;
@@ -1286,77 +1286,80 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @param int $ttl
         * @param callable $callback
         * @param array $opts Options map for getWithSetCallback()
-        * @param float|null &$asOf Cache generation timestamp of returned value [returned]
-        * @return mixed
+        * @return array Ordered list of the following:
+        *   - Cached or regenerated value
+        *   - Cached or regenerated value version number or null if not versioned
+        *   - Timestamp of the cached value or null if there is no value
         * @note Callable type hints are not used to avoid class-autoloading
         */
-       protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf = null ) {
-               $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
-               $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
-               $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
-               $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
+       private function fetchOrRegenerate( $key, $ttl, $callback, array $opts ) {
                $checkKeys = $opts['checkKeys'] ?? [];
-               $busyValue = $opts['busyValue'] ?? null;
-               $popWindow = $opts['hotTTR'] ?? self::HOT_TTR;
-               $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
+               $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
                $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
+               $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
+               $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
+               $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
                $touchedCb = $opts['touchedCallback'] ?? null;
                $initialTime = $this->getCurrentTime();
 
                $kClass = $this->determineKeyClassForStats( $key );
 
-               // Get the current key value and metadata
+               // Get the current key value and its metadata
                $curTTL = self::PASS_BY_REF;
                $curInfo = self::PASS_BY_REF; /** @var array $curInfo */
                $curValue = $this->get( $key, $curTTL, $checkKeys, $curInfo );
                // Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
                list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
-               // Best possible return value and its corresponding "as of" timestamp
-               $value = $curValue;
-               $asOf = $curInfo['asOf'];
-
-               // Determine if a cached value regeneration is needed or desired
+               // Use the cached value if it exists and is not due for synchronous regeneration
                if (
-                       $this->isValid( $value, $asOf, $minAsOf ) &&
+                       $this->isValid( $curValue, $curInfo['asOf'], $minAsOf ) &&
                        $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
                ) {
                        $preemptiveRefresh = (
                                $this->worthRefreshExpiring( $curTTL, $lowTTL ) ||
-                               $this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $initialTime )
+                               $this->worthRefreshPopular( $curInfo['asOf'], $ageNew, $hotTTR, $initialTime )
                        );
-
                        if ( !$preemptiveRefresh ) {
                                $this->stats->increment( "wanobjectcache.$kClass.hit.good" );
 
-                               return $value;
+                               return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
                        } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) ) {
                                $this->stats->increment( "wanobjectcache.$kClass.hit.refresh" );
 
-                               return $value;
+                               return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
                        }
                }
 
+               // Determine if there is stale or volatile cached value that is still usable
                $isKeyTombstoned = ( $curInfo['tombAsOf'] !== null );
                if ( $isKeyTombstoned ) {
-                       // Get the interim key value since the key is tombstoned (write-holed)
-                       list( $value, $asOf ) = $this->getInterimValue( $key, $minAsOf );
+                       // Key is write-holed; use the (volatile) interim key as an alternative
+                       list( $possValue, $possInfo ) = $this->getInterimValue( $key, $minAsOf );
                        // Update the "last purge time" since the $touchedCb timestamp depends on $value
-                       $LPT = $this->resolveTouched( $value, $LPT, $touchedCb );
+                       $LPT = $this->resolveTouched( $possValue, $LPT, $touchedCb );
+               } else {
+                       $possValue = $curValue;
+                       $possInfo = $curInfo;
                }
 
-               // Reduce mutex and cache set spam while keys are in the tombstone/holdoff period by
-               // checking if $value was genereated by a recent thread much less than a second ago.
+               // Avoid overhead from callback runs, regeneration locks, and cache sets during
+               // hold-off periods for the key by reusing very recently generated cached values
                if (
-                       $this->isValid( $value, $asOf, $minAsOf, $LPT ) &&
-                       $this->isVolatileValueAgeNegligible( $initialTime - $asOf )
+                       $this->isValid( $possValue, $possInfo['asOf'], $minAsOf, $LPT ) &&
+                       $this->isVolatileValueAgeNegligible( $initialTime - $possInfo['asOf'] )
                ) {
                        $this->stats->increment( "wanobjectcache.$kClass.hit.volatile" );
 
-                       return $value;
+                       return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
                }
 
-               // Decide if only one thread should handle regeneration at a time
-               $useMutex =
+               $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
+               $busyValue = $opts['busyValue'] ?? null;
+               $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
+               $version = $opts['version'] ?? null;
+
+               // Determine whether one thread per datacenter should handle regeneration at a time
+               $useRegenerationLock =
                        // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
                        // deduce the key hotness because |$curTTL| will always keep increasing until the
                        // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
@@ -1369,67 +1372,78 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                        ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
                        // Assume a key is hot if there is no value and a busy fallback is given.
                        // This avoids stampedes on eviction or preemptive regeneration taking too long.
-                       ( $busyValue !== null && $value === false );
-
-               $hasLock = false;
-               if ( $useMutex ) {
-                       // Attempt to acquire a non-blocking lock specific to the local datacenter
-                       if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
-                               // Lock acquired; this thread will recompute the value and update cache
-                               $hasLock = true;
-                       } elseif ( $this->isValid( $value, $asOf, $minAsOf ) ) {
-                               // Not acquired and stale cache value exists; use the stale value
-                               $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
-
-                               return $value;
-                       } else {
-                               // Lock not acquired and no stale value exists
-                               if ( $busyValue !== null ) {
-                                       // Use the busy fallback value if nothing else
+                       ( $busyValue !== null && $possValue === false );
+
+               // If a regeneration lock is required, threads that do not get the lock will use any
+               // available stale or volatile value. If there is none, then the cheap/placeholder
+               // value from $busyValue will be used if provided; failing that, all threads will try
+               // to regenerate the value and ignore the lock.
+               if ( $useRegenerationLock ) {
+                       $hasLock = $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL );
+                       if ( !$hasLock ) {
+                               if ( $this->isValid( $possValue, $possInfo['asOf'], $minAsOf ) ) {
+                                       $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
+
+                                       return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
+                               } elseif ( $busyValue !== null ) {
                                        $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
                                        $this->stats->increment( "wanobjectcache.$kClass.$miss.busy" );
 
-                                       return is_callable( $busyValue ) ? $busyValue() : $busyValue;
+                                       return [
+                                               is_callable( $busyValue ) ? $busyValue() : $busyValue,
+                                               $version,
+                                               $curInfo['asOf']
+                                       ];
                                }
                        }
+               } else {
+                       $hasLock = false;
                }
 
-               if ( !is_callable( $callback ) ) {
-                       throw new InvalidArgumentException( "Invalid cache miss callback provided." );
-               }
-
-               $preCallbackTime = $this->getCurrentTime();
-               // Generate the new value from the callback...
+               // Generate the new value given any prior value with a matching version
                $setOpts = [];
+               $preCallbackTime = $this->getCurrentTime();
                ++$this->callbackDepth;
                try {
-                       $value = call_user_func_array( $callback, [ $curValue, &$ttl, &$setOpts, $asOf ] );
+                       $value = $callback(
+                               ( $curInfo['version'] === $version ) ? $curValue : false,
+                               $ttl,
+                               $setOpts,
+                               ( $curInfo['version'] === $version ) ? $curInfo['asOf'] : null
+                       );
                } finally {
                        --$this->callbackDepth;
                }
-               $valueIsCacheable = ( $value !== false && $ttl >= 0 );
+               $postCallbackTime = $this->getCurrentTime();
 
-               if ( $valueIsCacheable ) {
-                       $ago = max( $this->getCurrentTime() - $initialTime, 0.0 );
-                       $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $ago );
+               // How long it took to fetch, validate, and generate the value
+               $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
 
+               // Attempt to save the newly generated value if applicable
+               if (
+                       // Callback yielded a cacheable value
+                       ( $value !== false && $ttl >= 0 ) &&
+                       // Current thread was not raced out of a regeneration lock or key is tombstoned
+                       ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
+                       // Key does not appear to be undergoing a set() stampede
+                       $this->checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock )
+               ) {
+                       // How long it took to generate the value
+                       $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
+                       $this->stats->timing( "wanobjectcache.$kClass.regen_walltime", 1e3 * $walltime );
+                       // If the key is write-holed then use the (volatile) interim key as an alternative
                        if ( $isKeyTombstoned ) {
-                               if ( $this->checkAndSetCooloff( $key, $kClass, $ago, $lockTSE, $hasLock ) ) {
-                                       // Use the interim key value since the key is tombstoned (write-holed)
-                                       $tempTTL = max( self::INTERIM_KEY_TTL, (int)$lockTSE );
-                                       $this->setInterimValue( $key, $value, $tempTTL, $this->getCurrentTime() );
-                               }
-                       } elseif ( !$useMutex || $hasLock ) {
-                               if ( $this->checkAndSetCooloff( $key, $kClass, $ago, $lockTSE, $hasLock ) ) {
-                                       $setOpts['creating'] = ( $curValue === false );
-                                       // Save the value unless a lock-winning thread is already expected to do that
-                                       $setOpts['lockTSE'] = $lockTSE;
-                                       $setOpts['staleTTL'] = $staleTTL;
-                                       // Use best known "since" timestamp if not provided
-                                       $setOpts += [ 'since' => $preCallbackTime ];
-                                       // Update the cache; this will fail if the key is tombstoned
-                                       $this->set( $key, $value, $ttl, $setOpts );
-                               }
+                               $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
+                       } else {
+                               $finalSetOpts = [
+                                       'since' => $setOpts['since'] ?? $preCallbackTime,
+                                       'version' => $version,
+                                       'staleTTL' => $staleTTL,
+                                       'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
+                                       'creating' => ( $curValue === false ), // optimization
+                                       'walltime' => $walltime
+                               ] + $setOpts;
+                               $this->set( $key, $value, $ttl, $finalSetOpts );
                        }
                }
 
@@ -1440,7 +1454,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
                $this->stats->increment( "wanobjectcache.$kClass.$miss.compute" );
 
-               return $value;
+               return [ $value, $version, $curInfo['asOf'] ];
        }
 
        /**
@@ -1460,6 +1474,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @return bool Whether it is OK to proceed with a key set operation
         */
        private function checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock ) {
+               $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
+
                // If $lockTSE is set, the lock was bypassed because there was no stale/interim value,
                // and $elapsed indicates that regeration is slow, then there is a risk of set()
                // stampedes with large blobs. With a typical scale-out infrastructure, CPU and query
@@ -1494,15 +1510,11 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @return array (current time left or null, UNIX timestamp of last purge or null)
         * @note Callable type hints are not used to avoid class-autoloading
         */
-       protected function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
+       private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
                if ( $touchedCallback === null || $value === false ) {
                        return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'] ) ];
                }
 
-               if ( !is_callable( $touchedCallback ) ) {
-                       throw new InvalidArgumentException( "Invalid expiration callback provided." );
-               }
-
                $touched = $touchedCallback( $value );
                if ( $touched !== null && $touched >= $curInfo['asOf'] ) {
                        $curTTL = min( $curTTL, self::TINY_NEGATIVE, $curInfo['asOf'] - $touched );
@@ -1518,53 +1530,49 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @return float|null UNIX timestamp of last purge or null
         * @note Callable type hints are not used to avoid class-autoloading
         */
-       protected function resolveTouched( $value, $lastPurge, $touchedCallback ) {
-               if ( $touchedCallback === null || $value === false ) {
-                       return $lastPurge;
-               }
-
-               if ( !is_callable( $touchedCallback ) ) {
-                       throw new InvalidArgumentException( "Invalid expiration callback provided." );
-               }
-
-               return max( $touchedCallback( $value ), $lastPurge );
+       private function resolveTouched( $value, $lastPurge, $touchedCallback ) {
+               return ( $touchedCallback === null || $value === false )
+                       ? $lastPurge // nothing to derive the "touched timestamp" from
+                       : max( $touchedCallback( $value ), $lastPurge );
        }
 
        /**
         * @param string $key
         * @param float $minAsOf Minimum acceptable "as of" timestamp
-        * @return array (cached value or false, cached value timestamp or null)
+        * @return array (cached value or false, cache key metadata map)
         */
-       protected function getInterimValue( $key, $minAsOf ) {
-               if ( !$this->useInterimHoldOffCaching ) {
-                       return [ false, null ]; // disabled
-               }
+       private function getInterimValue( $key, $minAsOf ) {
+               $now = $this->getCurrentTime();
+
+               if ( $this->useInterimHoldOffCaching ) {
+                       $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
 
-               $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
-               list( $value ) = $this->unwrap( $wrapped, $this->getCurrentTime() );
-               $valueAsOf = $wrapped[self::FLD_TIME] ?? null;
-               if ( $this->isValid( $value, $valueAsOf, $minAsOf ) ) {
-                       return [ $value, $valueAsOf ];
+                       list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now );
+                       if ( $this->isValid( $value, $keyInfo['asOf'], $minAsOf ) ) {
+                               return [ $value, $keyInfo ];
+                       }
                }
 
-               return [ false, null ];
+               return $this->unwrap( false, $now );
        }
 
        /**
         * @param string $key
         * @param mixed $value
-        * @param int $tempTTL
-        * @param float $newAsOf
+        * @param int $ttl
+        * @param int|null $version Value version number
+        * @param float $walltime How long it took to generate the value in seconds
         */
-       protected function setInterimValue( $key, $value, $tempTTL, $newAsOf ) {
-               $wrapped = $this->wrap( $value, $tempTTL, $newAsOf );
+       private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
+               $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl );
 
+               $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime );
                $this->cache->merge(
                        self::INTERIM_KEY_PREFIX . $key,
                        function () use ( $wrapped ) {
                                return $wrapped;
                        },
-                       $tempTTL,
+                       $ttl,
                        1
                );
        }
@@ -1593,7 +1601,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *         // Map of cache keys to entity IDs
         *         $cache->makeMultiKeys(
         *             $this->fileVersionIds(),
-        *             function ( $id, WANObjectCache $cache ) {
+        *             function ( $id ) use ( $cache ) {
         *                 return $cache->makeKey( 'file-version', $id );
         *             }
         *         ),
@@ -1632,20 +1640,16 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @param int $ttl Seconds to live for key updates
         * @param callable $callback Callback the yields entity regeneration callbacks
         * @param array $opts Options map
-        * @return array Map of (cache key => value) in the same order as $keyedIds
+        * @return mixed[] Map of (cache key => value) in the same order as $keyedIds
         * @since 1.28
         */
        final public function getMultiWithSetCallback(
                ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
        ) {
-               $valueKeys = array_keys( $keyedIds->getArrayCopy() );
-               $checkKeys = $opts['checkKeys'] ?? [];
-               $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
-
                // Load required keys into process cache in one go
                $this->warmupCache = $this->getRawKeysForWarmup(
-                       $this->getNonProcessCachedKeys( $valueKeys, $opts, $pcTTL ),
-                       $checkKeys
+                       $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
+                       $opts['checkKeys'] ?? []
                );
                $this->warmupKeyMisses = 0;
 
@@ -1687,7 +1691,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         *         // Map of cache keys to entity IDs
         *         $cache->makeMultiKeys(
         *             $this->fileVersionIds(),
-        *             function ( $id, WANObjectCache $cache ) {
+        *             function ( $id ) use ( $cache ) {
         *                 return $cache->makeKey( 'file-version', $id );
         *             }
         *         ),
@@ -1727,22 +1731,19 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @param int $ttl Seconds to live for key updates
         * @param callable $callback Callback the yields entity regeneration callbacks
         * @param array $opts Options map
-        * @return array Map of (cache key => value) in the same order as $keyedIds
+        * @return mixed[] Map of (cache key => value) in the same order as $keyedIds
         * @since 1.30
         */
        final public function getMultiWithUnionSetCallback(
                ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
        ) {
-               $idsByValueKey = $keyedIds->getArrayCopy();
-               $valueKeys = array_keys( $idsByValueKey );
                $checkKeys = $opts['checkKeys'] ?? [];
-               $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
                unset( $opts['lockTSE'] ); // incompatible
                unset( $opts['busyValue'] ); // incompatible
 
                // Load required keys into process cache in one go
-               $keysGet = $this->getNonProcessCachedKeys( $valueKeys, $opts, $pcTTL );
-               $this->warmupCache = $this->getRawKeysForWarmup( $keysGet, $checkKeys );
+               $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
+               $this->warmupCache = $this->getRawKeysForWarmup( $keysByIdGet, $checkKeys );
                $this->warmupKeyMisses = 0;
 
                // IDs of entities known to be in need of regeneration
@@ -1751,10 +1752,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                // Find out which keys are missing/deleted/stale
                $curTTLs = [];
                $asOfs = [];
-               $curByKey = $this->getMulti( $keysGet, $curTTLs, $checkKeys, $asOfs );
-               foreach ( $keysGet as $key ) {
+               $curByKey = $this->getMulti( $keysByIdGet, $curTTLs, $checkKeys, $asOfs );
+               foreach ( $keysByIdGet as $id => $key ) {
                        if ( !array_key_exists( $key, $curByKey ) || $curTTLs[$key] < 0 ) {
-                               $idsRegen[] = $idsByValueKey[$key];
+                               $idsRegen[] = $id;
                        }
                }
 
@@ -1786,7 +1787,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
                // Run the cache-aside logic using warmupCache instead of persistent cache queries
                $values = [];
-               foreach ( $idsByValueKey as $key => $id ) { // preserve order
+               foreach ( $keyedIds as $key => $id ) { // preserve order
                        $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
                }
 
@@ -1838,7 +1839,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         */
        final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
                $purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) );
-               if ( $purge && $purge[self::FLD_TIME] < $purgeTimestamp ) {
+               if ( $purge && $purge[self::PURGE_TIME] < $purgeTimestamp ) {
                        $isStale = true;
                        $this->logger->warning( "Reaping stale check key '$key'." );
                        $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, self::TTL_SECOND );
@@ -1877,18 +1878,133 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        }
 
        /**
-        * @param array $entities List of entity IDs
-        * @param callable $keyFunc Callback yielding a key from (entity ID, this WANObjectCache)
-        * @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order
+        * Hash a possibly long string into a suitable component for makeKey()/makeGlobalKey()
+        *
+        * @param string $component A raw component used in building a cache key
+        * @return string 64 character HMAC using a stable secret for public collision resistance
+        * @since 1.34
+        */
+       public function hash256( $component ) {
+               return hash_hmac( 'sha256', $component, $this->secret );
+       }
+
+       /**
+        * Get an iterator of (cache key => entity ID) for a list of entity IDs
+        *
+        * The callback takes an ID string and returns a key via makeKey()/makeGlobalKey().
+        * There should be no network nor filesystem I/O used in the callback. The entity
+        * ID/key mapping must be 1:1 or an exception will be thrown. If hashing is needed,
+        * then use the hash256() method.
+        *
+        * Example usage for the default keyspace:
+        * @code
+        *     $keyedIds = $cache->makeMultiKeys(
+        *         $modules,
+        *         function ( $module ) use ( $cache ) {
+        *             return $cache->makeKey( 'module-info', $module );
+        *         }
+        *     );
+        * @endcode
+        *
+        * Example usage for mixed default and global keyspace:
+        * @code
+        *     $keyedIds = $cache->makeMultiKeys(
+        *         $filters,
+        *         function ( $filter ) use ( $cache ) {
+        *             return ( strpos( $filter, 'central:' ) === 0 )
+        *                 ? $cache->makeGlobalKey( 'regex-filter', $filter )
+        *                 : $cache->makeKey( 'regex-filter', $filter )
+        *         }
+        *     );
+        * @endcode
+        *
+        * Example usage with hashing:
+        * @code
+        *     $keyedIds = $cache->makeMultiKeys(
+        *         $urls,
+        *         function ( $url ) use ( $cache ) {
+        *             return $cache->makeKey( 'url-info', $cache->hash256( $url ) );
+        *         }
+        *     );
+        * @endcode
+        *
+        * @see WANObjectCache::makeKey()
+        * @see WANObjectCache::makeGlobalKey()
+        * @see WANObjectCache::hash256()
+        *
+        * @param string[]|int[] $ids List of entity IDs
+        * @param callable $keyCallback Function returning makeKey()/makeGlobalKey() on the input ID
+        * @return ArrayIterator Iterator of (cache key => ID); order of $ids is preserved
+        * @throws UnexpectedValueException
         * @since 1.28
         */
-       final public function makeMultiKeys( array $entities, callable $keyFunc ) {
-               $map = [];
-               foreach ( $entities as $entity ) {
-                       $map[$keyFunc( $entity, $this )] = $entity;
+       final public function makeMultiKeys( array $ids, $keyCallback ) {
+               $idByKey = [];
+               foreach ( $ids as $id ) {
+                       // Discourage triggering of automatic makeKey() hashing in some backends
+                       if ( strlen( $id ) > 64 ) {
+                               $this->logger->warning( __METHOD__ . ": long ID '$id'; use hash256()" );
+                       }
+                       $key = $keyCallback( $id, $this );
+                       // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
+                       if ( !isset( $idByKey[$key] ) ) {
+                               $idByKey[$key] = $id;
+                       } elseif ( (string)$id !== (string)$idByKey[$key] ) {
+                               throw new UnexpectedValueException(
+                                       "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
+                               );
+                       }
+               }
+
+               return new ArrayIterator( $idByKey );
+       }
+
+       /**
+        * Get an (ID => value) map from (i) a non-unique list of entity IDs, and (ii) the list
+        * of corresponding entity values by first appearance of each ID in the entity ID list
+        *
+        * For use with getMultiWithSetCallback() and getMultiWithUnionSetCallback().
+        *
+        * *Only* use this method if the entity ID/key mapping is trivially 1:1 without exception.
+        * Key generation method must utitilize the *full* entity ID in the key (not a hash of it).
+        *
+        * Example usage:
+        * @code
+        *     $poems = $cache->getMultiWithSetCallback(
+        *         $cache->makeMultiKeys(
+        *             $uuids,
+        *             function ( $uuid ) use ( $cache ) {
+        *                 return $cache->makeKey( 'poem', $uuid );
+        *             }
+        *         ),
+        *         $cache::TTL_DAY,
+        *         function ( $uuid ) use ( $url ) {
+        *             return $this->http->run( [ 'method' => 'GET', 'url' => "$url/$uuid" ] );
+        *         }
+        *     );
+        *     $poemsByUUID = $cache->multiRemap( $uuids, $poems );
+        * @endcode
+        *
+        * @see WANObjectCache::makeMultiKeys()
+        * @see WANObjectCache::getMultiWithSetCallback()
+        * @see WANObjectCache::getMultiWithUnionSetCallback()
+        *
+        * @param string[]|int[] $ids Entity ID list makeMultiKeys()
+        * @param mixed[] $res Result of getMultiWithSetCallback()/getMultiWithUnionSetCallback()
+        * @return mixed[] Map of (ID => value); order of $ids is preserved
+        * @since 1.34
+        */
+       final public function multiRemap( array $ids, array $res ) {
+               if ( count( $ids ) !== count( $res ) ) {
+                       // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
+                       // ArrayIterator will have less entries due to "first appearance" de-duplication
+                       $ids = array_keys( array_flip( $ids ) );
+                       if ( count( $ids ) !== count( $res ) ) {
+                               throw new UnexpectedValueException( "Multi-key result does not match ID list" );
+                       }
                }
 
-               return new ArrayIterator( $map );
+               return array_combine( $ids, $res );
        }
 
        /**
@@ -2049,7 +2165,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * This must set the key to "PURGED:<UNIX timestamp>:<holdoff>"
         *
         * @param string $key Cache key
-        * @param int $ttl How long to keep the tombstone [seconds]
+        * @param int $ttl Seconds to keep the tombstone around
         * @param int $holdoff HOLDOFF_* constant controlling how long to ignore sets for this key
         * @return bool Success
         */
@@ -2095,10 +2211,11 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
        /**
         * @param string $key
-        * @param int $ttl
+        * @param int $ttl Seconds to live
         * @param callable $callback
         * @param array $opts
         * @return bool Success
+        * @note Callable type hints are not used to avoid class-autoloading
         */
        private function scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) {
                if ( !$this->asyncHandler ) {
@@ -2108,7 +2225,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                $func = $this->asyncHandler;
                $func( function () use ( $key, $ttl, $callback, $opts ) {
                        $opts['minAsOf'] = INF; // force a refresh
-                       $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
+                       $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
                } );
 
                return true;
@@ -2127,7 +2244,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @param int $graceTTL Consider using stale values if $curTTL is greater than this
         * @return bool
         */
-       protected function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
+       private function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
                if ( $curTTL > 0 ) {
                        return true;
                } elseif ( $graceTTL <= 0 ) {
@@ -2235,68 +2352,82 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        }
 
        /**
-        * Do not use this method outside WANObjectCache
-        *
         * @param mixed $value
-        * @param int $ttl [0=forever]
+        * @param int $ttl Seconds to live or zero for "indefinite"
+        * @param int|null $version Value version number or null if not versioned
         * @param float $now Unix Current timestamp just before calling set()
+        * @param float $walltime How long it took to generate the value in seconds
         * @return array
         */
-       protected function wrap( $value, $ttl, $now ) {
-               return [
-                       self::FLD_VERSION => self::VERSION,
+       private function wrap( $value, $ttl, $version, $now, $walltime ) {
+               // Returns keys in ascending integer order for PHP7 array packing:
+               // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
+               $wrapped = [
+                       self::FLD_FORMAT_VERSION => self::VERSION,
                        self::FLD_VALUE => $value,
                        self::FLD_TTL => $ttl,
                        self::FLD_TIME => $now
                ];
+               if ( $version !== null ) {
+                       $wrapped[self::FLD_VALUE_VERSION] = $version;
+               }
+               if ( $walltime >= self::GENERATION_SLOW_SEC ) {
+                       $wrapped[self::FLD_GENERATION_TIME] = $walltime;
+               }
+
+               return $wrapped;
        }
 
        /**
-        * Do not use this method outside WANObjectCache
-        *
-        * The cached value will be false if absent/tombstoned/malformed
-        *
-        * @param array|string|bool $wrapped
+        * @param array|string|bool $wrapped The entry at a cache key
         * @param float $now Unix Current timestamp (preferrably pre-query)
-        * @return array (cached value or false, current TTL, value timestamp, tombstone timestamp)
+        * @return array (value or false if absent/tombstoned/malformed, value metadata map).
+        * The cache key metadata includes the following metadata:
+        *   - asOf: UNIX timestamp of the value or null if there is no value
+        *   - curTTL: remaining time-to-live (negative if tombstoned) or null if there is no value
+        *   - version: value version number or null if the if there is no value
+        *   - tombAsOf: UNIX timestamp of the tombstone or null if there is no tombstone
         */
-       protected function unwrap( $wrapped, $now ) {
-               // Check if the value is a tombstone
-               $purge = $this->parsePurgeValue( $wrapped );
-               if ( $purge !== false ) {
-                       // Purged values should always have a negative current $ttl
-                       $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
-                       return [ false, $curTTL, null, $purge[self::FLD_TIME] ];
-               }
-
-               if ( !is_array( $wrapped ) // not found
-                       || !isset( $wrapped[self::FLD_VERSION] ) // wrong format
-                       || $wrapped[self::FLD_VERSION] !== self::VERSION // wrong version
-               ) {
-                       return [ false, null, null, null ];
-               }
-
-               if ( $wrapped[self::FLD_TTL] > 0 ) {
-                       // Get the approximate time left on the key
-                       $age = $now - $wrapped[self::FLD_TIME];
-                       $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
+       private function unwrap( $wrapped, $now ) {
+               $value = false;
+               $info = [ 'asOf' => null, 'curTTL' => null, 'version' => null, 'tombAsOf' => null ];
+
+               if ( is_array( $wrapped ) ) {
+                       // Entry expected to be a cached value; validate it
+                       if (
+                               ( $wrapped[self::FLD_FORMAT_VERSION] ?? null ) === self::VERSION &&
+                               $wrapped[self::FLD_TIME] >= $this->epoch
+                       ) {
+                               if ( $wrapped[self::FLD_TTL] > 0 ) {
+                                       // Get the approximate time left on the key
+                                       $age = $now - $wrapped[self::FLD_TIME];
+                                       $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
+                               } else {
+                                       // Key had no TTL, so the time left is unbounded
+                                       $curTTL = INF;
+                               }
+                               $value = $wrapped[self::FLD_VALUE];
+                               $info['version'] = $wrapped[self::FLD_VALUE_VERSION] ?? null;
+                               $info['asOf'] = $wrapped[self::FLD_TIME];
+                               $info['curTTL'] = $curTTL;
+                       }
                } else {
-                       // Key had no TTL, so the time left is unbounded
-                       $curTTL = INF;
-               }
-
-               if ( $wrapped[self::FLD_TIME] < $this->epoch ) {
-                       // Values this old are ignored
-                       return [ false, null, null, null ];
+                       // Entry expected to be a tombstone; parse it
+                       $purge = $this->parsePurgeValue( $wrapped );
+                       if ( $purge !== false ) {
+                               // Tombstoned keys should always have a negative current $ttl
+                               $info['curTTL'] = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
+                               $info['tombAsOf'] = $purge[self::PURGE_TIME];
+                       }
                }
 
-               return [ $wrapped[self::FLD_VALUE], $curTTL, $wrapped[self::FLD_TIME], null ];
+               return [ $value, $info ];
        }
 
        /**
-        * @param array $keys
+        * @param string[] $keys
         * @param string $prefix
-        * @return string[]
+        * @return string[] Prefix keys; the order of $keys is preserved
         */
        protected static function prefixCacheKeys( array $keys, $prefix ) {
                $res = [];
@@ -2311,7 +2442,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @param string $key String of the format <scope>:<class>[:<class or variable>]...
         * @return string A collection name to describe this class of key
         */
-       protected function determineKeyClassForStats( $key ) {
+       private function determineKeyClassForStats( $key ) {
                $parts = explode( ':', $key, 3 );
 
                return $parts[1] ?? $parts[0]; // sanity
@@ -2322,7 +2453,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer),
         *  or false if value isn't a valid purge value
         */
-       protected function parsePurgeValue( $value ) {
+       private function parsePurgeValue( $value ) {
                if ( !is_string( $value ) ) {
                        return false;
                }
@@ -2345,8 +2476,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                }
 
                return [
-                       self::FLD_TIME => (float)$segments[1],
-                       self::FLD_HOLDOFF => (int)$segments[2],
+                       self::PURGE_TIME => (float)$segments[1],
+                       self::PURGE_HOLDOFF => (int)$segments[2],
                ];
        }
 
@@ -2355,62 +2486,58 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @param int $holdoff In seconds
         * @return string Wrapped purge value
         */
-       protected function makePurgeValue( $timestamp, $holdoff ) {
+       private function makePurgeValue( $timestamp, $holdoff ) {
                return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
        }
 
-       /**
-        * @param mixed $value
-        * @param int $version
-        * @return bool
-        */
-       protected function isVersionedValue( $value, $version ) {
-               return (
-                       is_array( $value ) &&
-                       array_key_exists( self::VFLD_DATA, $value ) &&
-                       array_key_exists( self::VFLD_VERSION, $value ) &&
-                       $value[self::VFLD_VERSION] === $version
-               );
-       }
-
        /**
         * @param string $group
         * @return MapCacheLRU
         */
-       protected function getProcessCache( $group ) {
+       private function getProcessCache( $group ) {
                if ( !isset( $this->processCaches[$group] ) ) {
-                       list( , $n ) = explode( ':', $group );
-                       $this->processCaches[$group] = new MapCacheLRU( (int)$n );
+                       list( , $size ) = explode( ':', $group );
+                       $this->processCaches[$group] = new MapCacheLRU( (int)$size );
                }
 
                return $this->processCaches[$group];
        }
 
        /**
-        * @param array $keys
+        * @param string $key
+        * @param int $version
+        * @return string
+        */
+       private function getProcessCacheKey( $key, $version ) {
+               return $key . ' ' . (int)$version;
+       }
+
+       /**
+        * @param ArrayIterator $keys
         * @param array $opts
-        * @param int $pcTTL
-        * @return array List of keys
+        * @return string[] Map of (ID => cache key)
         */
-       private function getNonProcessCachedKeys( array $keys, array $opts, $pcTTL ) {
-               $keysFound = [];
-               if ( isset( $opts['pcTTL'] ) && $opts['pcTTL'] > 0 && $this->callbackDepth == 0 ) {
-                       $pcGroup = $opts['pcGroup'] ?? self::PC_PRIMARY;
-                       $procCache = $this->getProcessCache( $pcGroup );
-                       foreach ( $keys as $key ) {
-                               if ( $procCache->has( $key, $pcTTL ) ) {
-                                       $keysFound[] = $key;
+       private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
+               $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
+
+               $keysMissing = [];
+               if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
+                       $version = $opts['version'] ?? null;
+                       $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
+                       foreach ( $keys as $key => $id ) {
+                               if ( !$pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
+                                       $keysMissing[$id] = $key;
                                }
                        }
                }
 
-               return array_diff( $keys, $keysFound );
+               return $keysMissing;
        }
 
        /**
-        * @param array $keys
-        * @param array $checkKeys
-        * @return array Map of (cache key => mixed)
+        * @param string[] $keys
+        * @param string[]|string[][] $checkKeys
+        * @return string[] List of cache keys
         */
        private function getRawKeysForWarmup( array $keys, array $checkKeys ) {
                if ( !$keys ) {
index 9d7e143..d75b344 100644 (file)
@@ -67,8 +67,8 @@ class WinCacheBagOStuff extends BagOStuff {
                return $success;
        }
 
-       protected function doSet( $key, $value, $expire = 0, $flags = 0 ) {
-               $result = wincache_ucache_set( $key, $this->serialize( $value ), $expire );
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+               $result = wincache_ucache_set( $key, $this->serialize( $value ), $exptime );
 
                // false positive, wincache_ucache_set returns an empty array
                // in some circumstances.
index 24b5402..8615cfc 100644 (file)
@@ -177,7 +177,7 @@ class ChronologyProtector implements LoggerAwareInterface {
 
                $masterName = $lb->getServerName( $lb->getWriterIndex() );
                if ( $lb->hasStreamingReplicaServers() ) {
-                       $pos = $lb->getMasterPos();
+                       $pos = $lb->getReplicaResumePos();
                        if ( $pos ) {
                                $this->logger->debug( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
                                $this->shutdownPositions[$masterName] = $pos;
index 894a262..3024b00 100644 (file)
@@ -30,7 +30,7 @@ use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 use Wikimedia\ScopedCallback;
 use Wikimedia\Timestamp\ConvertibleTimestamp;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
 use BagOStuff;
 use HashBagOStuff;
 use LogicException;
@@ -198,19 +198,23 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var int Writes to this temporary table effect lastDoneWrites() */
        private static $TEMP_PSEUDO_PERMANENT = 2;
 
-       /** Number of times to re-try an operation in case of deadlock */
+       /** @var int Number of times to re-try an operation in case of deadlock */
        private static $DEADLOCK_TRIES = 4;
-       /** Minimum time to wait before retry, in microseconds */
+       /** @var int Minimum time to wait before retry, in microseconds */
        private static $DEADLOCK_DELAY_MIN = 500000;
-       /** Maximum time to wait before retry */
+       /** @var int Maximum time to wait before retry */
        private static $DEADLOCK_DELAY_MAX = 1500000;
 
-       /** How long before it is worth doing a dummy query to test the connection */
+       /** @var int How long before it is worth doing a dummy query to test the connection */
        private static $PING_TTL = 1.0;
+       /** @var string Dummy SQL query */
        private static $PING_QUERY = 'SELECT 1 AS ping';
 
+       /** @var float Guess of how many seconds it takes to replicate a small insert */
        private static $TINY_WRITE_SEC = 0.010;
+       /** @var float Consider a write slow if it took more than this many seconds */
        private static $SLOW_WRITE_SEC = 0.500;
+       /** @var float Assume an insert of this many rows or less should be fast to replicate */
        private static $SMALL_WRITE_ROWS = 100;
 
        /**
@@ -301,10 +305,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /**
         * Open a new connection to the database (closing any existing one)
         *
-        * @param string $server Database server host
-        * @param string $user Database user name
-        * @param string $password Database user password
-        * @param string $dbName Database name
+        * @param string|null $server Database server host
+        * @param string|null $user Database user name
+        * @param string|null $password Database user password
+        * @param string|null $dbName Database name
         * @param string|null $schema Database schema name
         * @param string $tablePrefix Table prefix
         * @throws DBConnectionError
@@ -316,8 +320,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *
         * This also connects to the database immediately upon object construction
         *
-        * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
-        * @param array $p Parameter map with keys:
+        * @param string $type A possible DB type (sqlite, mysql, postgres,...)
+        * @param array $params Parameter map with keys:
         *   - host : The hostname of the DB server
         *   - user : The name of the database user the client operates under
         *   - password : The password for the database user
@@ -356,45 +360,51 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @throws InvalidArgumentException If the database driver or extension cannot be found
         * @since 1.18
         */
-       final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
-               $class = self::getClass( $dbType, $p['driver'] ?? null );
+       final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
+               $class = self::getClass( $type, $params['driver'] ?? null );
 
                if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
-                       // Resolve some defaults for b/c
-                       $p['host'] = $p['host'] ?? false;
-                       $p['user'] = $p['user'] ?? false;
-                       $p['password'] = $p['password'] ?? false;
-                       $p['dbname'] = $p['dbname'] ?? false;
-                       $p['flags'] = $p['flags'] ?? 0;
-                       $p['variables'] = $p['variables'] ?? [];
-                       $p['tablePrefix'] = $p['tablePrefix'] ?? '';
-                       $p['schema'] = $p['schema'] ?? null;
-                       $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
-                       $p['agent'] = $p['agent'] ?? '';
-                       if ( !isset( $p['connLogger'] ) ) {
-                               $p['connLogger'] = new NullLogger();
-                       }
-                       if ( !isset( $p['queryLogger'] ) ) {
-                               $p['queryLogger'] = new NullLogger();
-                       }
-                       $p['profiler'] = $p['profiler'] ?? null;
-                       if ( !isset( $p['trxProfiler'] ) ) {
-                               $p['trxProfiler'] = new TransactionProfiler();
-                       }
-                       if ( !isset( $p['errorLogger'] ) ) {
-                               $p['errorLogger'] = function ( Exception $e ) {
+                       $params += [
+                               'host' => null,
+                               'user' => null,
+                               'password' => null,
+                               'dbname' => null,
+                               'schema' => null,
+                               'tablePrefix' => '',
+                               'flags' => 0,
+                               'variables' => [],
+                               'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
+                               'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname()
+                       ];
+
+                       $normalizedParams = [
+                               // Configuration
+                               'host' => strlen( $params['host'] ) ? $params['host'] : null,
+                               'user' => strlen( $params['user'] ) ? $params['user'] : null,
+                               'password' => is_string( $params['password'] ) ? $params['password'] : null,
+                               'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
+                               'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
+                               'tablePrefix' => (string)$params['tablePrefix'],
+                               'flags' => (int)$params['flags'],
+                               'variables' => $params['variables'],
+                               'cliMode' => (bool)$params['cliMode'],
+                               'agent' => (string)$params['agent'],
+                               // Objects and callbacks
+                               'profiler' => $params['profiler'] ?? null,
+                               'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
+                               'connLogger' => $params['connLogger'] ?? new NullLogger(),
+                               'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
+                               'errorLogger' => $params['errorLogger'] ?? function ( Exception $e ) {
                                        trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
-                               };
-                       }
-                       if ( !isset( $p['deprecationLogger'] ) ) {
-                               $p['deprecationLogger'] = function ( $msg ) {
+                               },
+                               'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
                                        trigger_error( $msg, E_USER_DEPRECATED );
-                               };
-                       }
+                               }
+                       ] + $params;
 
                        /** @var Database $conn */
-                       $conn = new $class( $p );
-                       if ( $connect == self::NEW_CONNECTED ) {
+                       $conn = new $class( $normalizedParams );
+                       if ( $connect === self::NEW_CONNECTED ) {
                                $conn->initConnection();
                        }
                } else {
@@ -4290,8 +4300,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->server,
                                $this->user,
                                $this->password,
-                               $this->getDBname(),
-                               $this->dbSchema(),
+                               $this->currentDomain->getDatabase(),
+                               $this->currentDomain->getSchema(),
                                $this->tablePrefix()
                        );
                        $this->lastPing = microtime( true );
@@ -4429,9 +4439,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $fname = false,
                callable $inputCallback = null
        ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $fp = fopen( $filename, 'r' );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                if ( $fp === false ) {
                        throw new RuntimeException( "Could not open \"{$filename}\"" );
@@ -4868,8 +4878,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->server,
                                $this->user,
                                $this->password,
-                               $this->getDBname(),
-                               $this->dbSchema(),
+                               $this->currentDomain->getDatabase(),
+                               $this->currentDomain->getSchema(),
                                $this->tablePrefix()
                        );
                        $this->lastPing = microtime( true );
@@ -4903,10 +4913,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( $this->conn ) {
                        // Avoid connection leaks for sanity. Normally, resources close at script completion.
                        // The connection might already be closed in zend/hhvm by now, so suppress warnings.
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $this->closeConnection();
-                       Wikimedia\restoreWarnings();
-                       $this->conn = false;
+                       AtEase::restoreWarnings();
+                       $this->conn = null;
                }
        }
 }
index 69174f9..d06bcb9 100644 (file)
@@ -1165,7 +1165,10 @@ class DatabaseMssql extends Database {
 
        protected function doSelectDomain( DatabaseDomain $domain ) {
                if ( $domain->getSchema() !== null ) {
-                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+                       throw new DBExpectedError(
+                               $this,
+                               __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+                       );
                }
 
                $database = $domain->getDatabase();
index 417b464..1e3fa84 100644 (file)
@@ -24,7 +24,7 @@ namespace Wikimedia\Rdbms;
 
 use DateTime;
 use DateTimeZone;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
 use InvalidArgumentException;
 use Exception;
 use RuntimeException;
@@ -96,7 +96,7 @@ abstract class DatabaseMysqlBase extends Database {
         *   - sslCiphers : array list of allowable ciphers [default: null]
         * @param array $params
         */
-       function __construct( array $params ) {
+       public function __construct( array $params ) {
                $this->lagDetectionMethod = $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master';
                $this->lagDetectionOptions = $params['lagDetectionOptions'] ?? [];
                $this->useGTIDs = !empty( $params['useGTIDs' ] );
@@ -125,7 +125,7 @@ abstract class DatabaseMysqlBase extends Database {
                $this->close();
 
                if ( $schema !== null ) {
-                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+                       throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
                }
 
                $this->server = $server;
@@ -194,7 +194,10 @@ abstract class DatabaseMysqlBase extends Database {
 
        protected function doSelectDomain( DatabaseDomain $domain ) {
                if ( $domain->getSchema() !== null ) {
-                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+                       throw new DBExpectedError(
+                               $this,
+                               __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+                       );
                }
 
                $database = $domain->getDatabase();
@@ -241,9 +244,9 @@ abstract class DatabaseMysqlBase extends Database {
         * @throws DBUnexpectedError
         */
        public function freeResult( $res ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $ok = $this->mysqlFreeResult( ResultWrapper::unwrap( $res ) );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( !$ok ) {
                        throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
                }
@@ -263,9 +266,9 @@ abstract class DatabaseMysqlBase extends Database {
         * @throws DBUnexpectedError
         */
        public function fetchObject( $res ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $row = $this->mysqlFetchObject( ResultWrapper::unwrap( $res ) );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                $errno = $this->lastErrno();
                // Unfortunately, mysql_fetch_object does not reset the last errno.
@@ -296,9 +299,9 @@ abstract class DatabaseMysqlBase extends Database {
         * @throws DBUnexpectedError
         */
        public function fetchRow( $res ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $row = $this->mysqlFetchArray( ResultWrapper::unwrap( $res ) );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                $errno = $this->lastErrno();
                // Unfortunately, mysql_fetch_array does not reset the last errno.
@@ -332,9 +335,9 @@ abstract class DatabaseMysqlBase extends Database {
                if ( is_bool( $res ) ) {
                        $n = 0;
                } else {
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $n = $this->mysqlNumRows( ResultWrapper::unwrap( $res ) );
-                       Wikimedia\restoreWarnings();
+                       AtEase::restoreWarnings();
                }
 
                // Unfortunately, mysql_num_rows does not reset the last errno.
@@ -430,12 +433,12 @@ abstract class DatabaseMysqlBase extends Database {
        public function lastError() {
                if ( $this->conn ) {
                        # Even if it's non-zero, it can still be invalid
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $error = $this->mysqlError( $this->conn );
                        if ( !$error ) {
                                $error = $this->mysqlError();
                        }
-                       Wikimedia\restoreWarnings();
+                       AtEase::restoreWarnings();
                } else {
                        $error = $this->mysqlError();
                }
index 08987d9..840b428 100644 (file)
@@ -24,7 +24,7 @@ namespace Wikimedia\Rdbms;
 
 use Wikimedia\Timestamp\ConvertibleTimestamp;
 use Wikimedia\WaitConditionLoop;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
 use Exception;
 
 /**
@@ -97,6 +97,8 @@ class DatabasePostgres extends Database {
                        );
                }
 
+               $this->close();
+
                $this->server = $server;
                $this->user = $user;
                $this->password = $password;
@@ -120,9 +122,8 @@ class DatabasePostgres extends Database {
                }
 
                $this->connectString = $this->makeConnectionString( $connectVars );
-               $this->close();
-               $this->installErrorHandler();
 
+               $this->installErrorHandler();
                try {
                        // Use new connections to let LoadBalancer/LBFactory handle reuse
                        $this->conn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
@@ -130,7 +131,6 @@ class DatabasePostgres extends Database {
                        $this->restoreErrorHandler();
                        throw $ex;
                }
-
                $phpError = $this->restoreErrorHandler();
 
                if ( !$this->conn ) {
@@ -274,18 +274,18 @@ class DatabasePostgres extends Database {
        }
 
        public function freeResult( $res ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $ok = pg_free_result( ResultWrapper::unwrap( $res ) );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( !$ok ) {
                        throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
                }
        }
 
        public function fetchObject( $res ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $row = pg_fetch_object( ResultWrapper::unwrap( $res ) );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                # @todo FIXME: HACK HACK HACK HACK debug
 
                # @todo hashar: not sure if the following test really trigger if the object
@@ -302,9 +302,9 @@ class DatabasePostgres extends Database {
        }
 
        public function fetchRow( $res ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $row = pg_fetch_array( ResultWrapper::unwrap( $res ) );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                $conn = $this->getBindingHandle();
                if ( pg_last_error( $conn ) ) {
@@ -322,9 +322,9 @@ class DatabasePostgres extends Database {
                        return 0;
                }
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $n = pg_num_rows( ResultWrapper::unwrap( $res ) );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                $conn = $this->getBindingHandle();
                if ( pg_last_error( $conn ) ) {
@@ -884,9 +884,12 @@ __INDEXATTR__;
        }
 
        /**
+        * @param string $prefix Only show tables with this prefix, e.g. mw_
+        * @param string $fname Calling function name
+        * @return string[]
         * @suppress SecurityCheck-SQLInjection array_map not recognized T204911
         */
-       public function listTables( $prefix = null, $fname = __METHOD__ ) {
+       public function listTables( $prefix = '', $fname = __METHOD__ ) {
                $eschemas = implode( ',', array_map( [ $this, 'addQuotes' ], $this->getCoreSchemas() ) );
                $result = $this->query(
                        "SELECT DISTINCT tablename FROM pg_tables WHERE schemaname IN ($eschemas)", $fname );
@@ -895,7 +898,7 @@ __INDEXATTR__;
                foreach ( $result as $table ) {
                        $vars = get_object_vars( $table );
                        $table = array_pop( $vars );
-                       if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
+                       if ( $prefix == '' || strpos( $table, $prefix ) === 0 ) {
                                $endArray[] = $table;
                        }
                }
@@ -1053,7 +1056,7 @@ __INDEXATTR__;
                        // See https://www.postgresql.org/docs/8.3/sql-set.html
                        throw new DBUnexpectedError(
                                $this,
-                               __METHOD__ . ": a transaction is currently active."
+                               __METHOD__ . ": a transaction is currently active"
                        );
                }
 
index c875e56..7b3dbb3 100644 (file)
@@ -29,7 +29,6 @@ use PDOException;
 use Exception;
 use LockManager;
 use FSLockManager;
-use InvalidArgumentException;
 use RuntimeException;
 use stdClass;
 
@@ -37,12 +36,9 @@ use stdClass;
  * @ingroup Database
  */
 class DatabaseSqlite extends Database {
-       /** @var bool Whether full text is enabled */
-       private static $fulltextEnabled = null;
-
-       /** @var string|null Directory */
+       /** @var string|null Directory for SQLite database files listed under their DB name */
        protected $dbDir;
-       /** @var string File name for SQLite database file */
+       /** @var string|null Explicit path for the SQLite database file */
        protected $dbPath;
        /** @var string Transaction mode */
        protected $trxMode;
@@ -61,49 +57,40 @@ class DatabaseSqlite extends Database {
        /** @var array List of shared database already attached to this connection */
        private $alreadyAttached = [];
 
+       /** @var bool Whether full text is enabled */
+       private static $fulltextEnabled = null;
+
        /**
         * Additional params include:
         *   - dbDirectory : directory containing the DB and the lock file directory
-        *                   [defaults to $wgSQLiteDataDir]
         *   - dbFilePath  : use this to force the path of the DB file
         *   - trxMode     : one of (deferred, immediate, exclusive)
         * @param array $p
         */
-       function __construct( array $p ) {
+       public function __construct( array $p ) {
                if ( isset( $p['dbFilePath'] ) ) {
                        $this->dbPath = $p['dbFilePath'];
-                       $lockDomain = md5( $this->dbPath );
-                       // Use "X" for things like X.sqlite and ":memory:" for RAM-only DBs
-                       if ( !isset( $p['dbname'] ) || !strlen( $p['dbname'] ) ) {
-                               $p['dbname'] = preg_replace( '/\.sqlite\d?$/', '', basename( $this->dbPath ) );
+                       if ( !strlen( $p['dbname'] ) ) {
+                               $p['dbname'] = self::generateDatabaseName( $this->dbPath );
                        }
                } elseif ( isset( $p['dbDirectory'] ) ) {
                        $this->dbDir = $p['dbDirectory'];
-                       $lockDomain = $p['dbname'];
-               } else {
-                       throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
                }
 
-               $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
-               if ( $this->trxMode &&
-                       !in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] )
-               ) {
-                       $this->trxMode = null;
-                       $this->queryLogger->warning( "Invalid SQLite transaction mode provided." );
-               }
+               // Set a dummy user to make initConnection() trigger open()
+               parent::__construct( [ 'user' => '@' ] + $p );
 
-               if ( $this->hasProcessMemoryPath() ) {
-                       $this->lockMgr = new NullLockManager( [ 'domain' => $lockDomain ] );
-               } else {
+               $this->trxMode = strtoupper( $p['trxMode'] ?? '' );
+
+               $lockDirectory = $this->getLockFileDirectory();
+               if ( $lockDirectory !== null ) {
                        $this->lockMgr = new FSLockManager( [
-                               'domain' => $lockDomain,
-                               'lockDirectory' => is_string( $this->dbDir )
-                                       ? "{$this->dbDir}/locks"
-                                       : dirname( $this->dbPath ) . "/locks"
+                               'domain' => $this->getDomainID(),
+                               'lockDirectory' => $lockDirectory
                        ] );
+               } else {
+                       $this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
                }
-
-               parent::__construct( $p );
        }
 
        protected static function getAttributes() {
@@ -129,38 +116,10 @@ class DatabaseSqlite extends Database {
                return $db;
        }
 
-       protected function doInitConnection() {
-               if ( $this->dbPath !== null ) {
-                       // Standalone .sqlite file mode.
-                       $this->openFile(
-                               $this->dbPath,
-                               $this->connectionParams['dbname'],
-                               $this->connectionParams['tablePrefix']
-                       );
-               } elseif ( $this->dbDir !== null ) {
-                       // Stock wiki mode using standard file names per DB
-                       if ( strlen( $this->connectionParams['dbname'] ) ) {
-                               $this->open(
-                                       $this->connectionParams['host'],
-                                       $this->connectionParams['user'],
-                                       $this->connectionParams['password'],
-                                       $this->connectionParams['dbname'],
-                                       $this->connectionParams['schema'],
-                                       $this->connectionParams['tablePrefix']
-                               );
-                       } else {
-                               // Caller will manually call open() later?
-                               $this->connLogger->debug( __METHOD__ . ': no database opened.' );
-                       }
-               } else {
-                       throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
-               }
-       }
-
        /**
         * @return string
         */
-       function getType() {
+       public function getType() {
                return 'sqlite';
        }
 
@@ -169,31 +128,35 @@ class DatabaseSqlite extends Database {
         *
         * @return bool
         */
-       function implicitGroupby() {
+       public function implicitGroupby() {
                return false;
        }
 
        protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
                $this->close();
 
+               // Note that for SQLite, $server, $user, and $pass are ignored
+
                if ( $schema !== null ) {
-                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+                       throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
                }
 
-               // Only $dbName is used, the other parameters are irrelevant for SQLite databases
-               $this->openFile( self::generateFileName( $this->dbDir, $dbName ), $dbName, $tablePrefix );
-       }
+               if ( $this->dbPath !== null ) {
+                       $path = $this->dbPath;
+               } elseif ( $this->dbDir !== null ) {
+                       $path = self::generateFileName( $this->dbDir, $dbName );
+               } else {
+                       throw new DBExpectedError( $this, __CLASS__ . ": DB path or directory required" );
+               }
 
-       /**
-        * Opens a database file
-        *
-        * @param string $fileName
-        * @param string $dbName
-        * @param string $tablePrefix
-        * @throws DBConnectionError
-        */
-       protected function openFile( $fileName, $dbName, $tablePrefix ) {
-               if ( !$this->hasProcessMemoryPath() && !is_readable( $fileName ) ) {
+               if ( !in_array( $this->trxMode, [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ], true ) ) {
+                       throw new DBExpectedError(
+                               $this,
+                               __CLASS__ . ": invalid transaction mode '{$this->trxMode}'"
+                       );
+               }
+
+               if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) {
                        $error = "SQLite database file not readable";
                        $this->connLogger->error(
                                "Error connecting to {db_server}: {error}",
@@ -202,20 +165,17 @@ class DatabaseSqlite extends Database {
                        throw new DBConnectionError( $this, $error );
                }
 
-               $this->dbPath = $fileName;
                try {
-                       $this->conn = new PDO(
-                               "sqlite:$fileName",
+                       $conn = new PDO(
+                               "sqlite:$path",
                                '',
                                '',
                                [ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ]
                        );
-                       $error = 'unknown error';
+                       // Set error codes only, don't raise exceptions
+                       $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
                } catch ( PDOException $e ) {
                        $error = $e->getMessage();
-               }
-
-               if ( !$this->conn ) {
                        $this->connLogger->error(
                                "Error connecting to {db_server}: {error}",
                                $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
@@ -223,19 +183,17 @@ class DatabaseSqlite extends Database {
                        throw new DBConnectionError( $this, $error );
                }
 
-               try {
-                       // Set error codes only, don't raise exceptions
-                       $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
-
-                       $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
+               $this->conn = $conn;
+               $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
 
+               try {
                        $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
                        // Enforce LIKE to be case sensitive, just like MySQL
                        $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
                        // Apply an optimizations or requirements regarding fsync() usage
                        $sync = $this->connectionVariables['synchronous'] ?? null;
                        if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
-                               $this->query( "PRAGMA synchronous = $sync", __METHOD__ );
+                               $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
                        }
                } catch ( Exception $e ) {
                        // Connection was not fully initialized and is not safe for use
@@ -245,11 +203,25 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * @return string SQLite DB file path
+        * @return string|null SQLite DB file path
+        * @throws DBUnexpectedError
         * @since 1.25
         */
        public function getDbFilePath() {
-               return $this->dbPath;
+               return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
+       }
+
+       /**
+        * @return string|null Lock file directory
+        */
+       public function getLockFileDirectory() {
+               if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
+                       return dirname( $this->dbPath ) . '/locks';
+               } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
+                       return $this->dbDir . '/locks';
+               }
+
+               return null;
        }
 
        /**
@@ -265,13 +237,50 @@ class DatabaseSqlite extends Database {
        /**
         * Generates a database file name. Explicitly public for installer.
         * @param string $dir Directory where database resides
-        * @param string $dbName Database name
+        * @param string|bool $dbName Database name (or false from Database::factory, validated here)
         * @return string
+        * @throws DBUnexpectedError
         */
        public static function generateFileName( $dir, $dbName ) {
+               if ( $dir == '' ) {
+                       throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
+               } elseif ( self::isProcessMemoryPath( $dir ) ) {
+                       throw new DBUnexpectedError(
+                               null,
+                               __CLASS__ . ": cannot use process memory directory '$dir'"
+                       );
+               } elseif ( !strlen( $dbName ) ) {
+                       throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
+               }
+
                return "$dir/$dbName.sqlite";
        }
 
+       /**
+        * @param string $path
+        * @return string
+        */
+       private static function generateDatabaseName( $path ) {
+               if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
+                       // E.g. "file::memory:?cache=shared" => ":memory":
+                       return ':memory:';
+               } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
+                       // E.g. "file:memdb1?mode=memory" => ":memdb1:"
+                       return ":{$m[1]}:";
+               } else {
+                       // E.g. "/home/.../some_db.sqlite3" => "some_db"
+                       return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
+               }
+       }
+
+       /**
+        * @param string $path
+        * @return bool
+        */
+       private static function isProcessMemoryPath( $path ) {
+               return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
+       }
+
        /**
         * Check if the searchindext table is FTS enabled.
         * @return bool False if not enabled.
@@ -322,13 +331,11 @@ class DatabaseSqlite extends Database {
         * @param string $fname Calling function name
         * @return IResultWrapper
         */
-       function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
-               if ( !$file ) {
-                       $file = self::generateFileName( $this->dbDir, $name );
-               }
-               $file = $this->addQuotes( $file );
+       public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
+               $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
+               $encFile = $this->addQuotes( $file );
 
-               return $this->query( "ATTACH DATABASE $file AS $name", $fname );
+               return $this->query( "ATTACH DATABASE $encFile AS $name", $fname );
        }
 
        protected function isWriteQuery( $sql ) {
@@ -765,14 +772,9 @@ class DatabaseSqlite extends Database {
        }
 
        public function serverIsReadOnly() {
-               return ( !$this->hasProcessMemoryPath() && !is_writable( $this->dbPath ) );
-       }
+               $path = $this->getDbFilePath();
 
-       /**
-        * @return bool
-        */
-       private function hasProcessMemoryPath() {
-               return ( strpos( $this->dbPath, ':memory:' ) === 0 );
+               return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
        }
 
        /**
@@ -813,7 +815,7 @@ class DatabaseSqlite extends Database {
        }
 
        protected function doBegin( $fname = '' ) {
-               if ( $this->trxMode ) {
+               if ( $this->trxMode != '' ) {
                        $this->query( "BEGIN {$this->trxMode}", $fname );
                } else {
                        $this->query( 'BEGIN', $fname );
@@ -967,15 +969,15 @@ class DatabaseSqlite extends Database {
        }
 
        public function lock( $lockName, $method, $timeout = 5 ) {
-               // Give better error message for permission problems than just returning false
+               $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
                if (
-                       !is_dir( "{$this->dbDir}/locks" ) &&
-                       ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) )
+                       $this->lockMgr instanceof FSLockManager &&
+                       $status->hasMessage( 'lockmanager-fail-openlock' )
                ) {
-                       throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." );
+                       throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
                }
 
-               return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
+               return $status->isOK();
        }
 
        public function unlock( $lockName, $method ) {
index d94d24d..a85e1e5 100644 (file)
@@ -422,14 +422,20 @@ abstract class LBFactory implements ILBFactory {
                // time needed to wait on the next clusters.
                $masterPositions = array_fill( 0, count( $lbs ), false );
                foreach ( $lbs as $i => $lb ) {
-                       if ( !$lb->hasStreamingReplicaServers() ) {
-                               continue; // T29975: no replication; avoid getMasterPos() permissions errors
-                       } elseif (
-                               $opts['ifWritesSince'] &&
-                               $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
+                       if (
+                               // No writes to wait on getting replicated
+                               !$lb->hasMasterConnection() ||
+                               // No replication; avoid getMasterPos() permissions errors (T29975)
+                               !$lb->hasStreamingReplicaServers() ||
+                               // No writes since the last replication wait
+                               (
+                                       $opts['ifWritesSince'] &&
+                                       $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
+                               )
                        ) {
-                               continue; // no writes since the last wait
+                               continue; // no need to wait
                        }
+
                        $masterPositions[$i] = $lb->getMasterPos();
                }
 
index ba0b4a0..4a4e1bc 100644 (file)
@@ -455,11 +455,30 @@ interface ILoadBalancer {
        public function getServerAttributes( $i );
 
        /**
-        * Get the current master position for chronology control purposes
+        * Get the current master replication position
+        *
         * @return DBMasterPos|bool Returns false if not applicable
+        * @throws DBError
         */
        public function getMasterPos();
 
+       /**
+        * Get the highest DB replication position for chronology control purposes
+        *
+        * If there is only a master server then this returns false. If replication is present
+        * and correctly configured, then this returns the highest replication position of any
+        * server with an open connection. That position can later be passed to waitFor() on a
+        * new load balancer instance to make sure that queries on the new connections see data
+        * at least as up-to-date as queries (prior to this method call) on the old connections.
+        *
+        * This can be useful for implementing session consistency, where the session
+        * will be resumed accross multiple HTTP requests or CLI script instances.
+        *
+        * @return DBMasterPos|bool Replication position or false if not applicable
+        * @since 1.34
+        */
+       public function getReplicaResumePos();
+
        /**
         * Disable this load balancer. All connections are closed, and any attempt to
         * open a new connection will result in a DBAccessError.
index 184cb0e..cab0201 100644 (file)
@@ -1452,22 +1452,56 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function getMasterPos() {
-               # If this entire request was served from a replica DB without opening a connection to the
-               # master (however unlikely that may be), then we can fetch the position from the replica DB.
+               $index = $this->getWriterIndex();
+
+               $conn = $this->getAnyOpenConnection( $index );
+               if ( $conn ) {
+                       return $conn->getMasterPos();
+               }
+
+               $conn = $this->getConnection( $index, self::CONN_SILENCE_ERRORS );
+               if ( !$conn ) {
+                       $this->reportConnectionError();
+                       return null; // unreachable due to exception
+               }
+
+               try {
+                       $pos = $conn->getMasterPos();
+               } finally {
+                       $this->closeConnection( $conn );
+               }
+
+               return $pos;
+       }
+
+       public function getReplicaResumePos() {
+               // Get the position of any existing master server connection
                $masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
-               if ( !$masterConn ) {
-                       $serverCount = $this->getServerCount();
-                       for ( $i = 1; $i < $serverCount; $i++ ) {
-                               $conn = $this->getAnyOpenConnection( $i );
-                               if ( $conn ) {
-                                       return $conn->getReplicaPos();
-                               }
-                       }
-               } else {
+               if ( $masterConn ) {
                        return $masterConn->getMasterPos();
                }
 
-               return false;
+               // Get the highest position of any existing replica server connection
+               $highestPos = false;
+               $serverCount = $this->getServerCount();
+               for ( $i = 1; $i < $serverCount; $i++ ) {
+                       if ( !empty( $this->servers[$i]['is static'] ) ) {
+                               continue; // server does not use replication
+                       }
+
+                       $conn = $this->getAnyOpenConnection( $i );
+                       $pos = $conn ? $conn->getReplicaPos() : false;
+                       if ( !$pos ) {
+                               continue; // no open connection or could not get position
+                       }
+
+                       $highestPos = $highestPos ?: $pos;
+                       if ( $pos->hasReached( $highestPos ) ) {
+                               $highestPos = $pos;
+                       }
+               }
+
+               return $highestPos;
        }
 
        public function disable() {
index 9a8086f..b477855 100644 (file)
@@ -23,6 +23,7 @@
 
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 /**
  * Helper class to manage Redis connections.
@@ -81,7 +82,7 @@ class RedisConnectionPool implements LoggerAwareInterface {
                                __CLASS__ . ' requires a Redis client library. ' .
                                'See https://www.mediawiki.org/wiki/Redis#Setup' );
                }
-               $this->logger = $options['logger'] ?? new \Psr\Log\NullLogger();
+               $this->logger = $options['logger'] ?? new NullLogger();
                $this->connectTimeout = $options['connectTimeout'];
                $this->readTimeout = $options['readTimeout'];
                $this->persistent = $options['persistent'];
index e9853b1..ffbc378 100644 (file)
@@ -323,7 +323,7 @@ class ObjectCache {
         * @throws UnexpectedValueException
         */
        public static function newWANCacheFromParams( array $params ) {
-               global $wgCommandLineMode;
+               global $wgCommandLineMode, $wgSecretKey;
 
                $services = MediaWikiServices::getInstance();
                $params['cache'] = self::newFromParams( $params['store'] );
@@ -334,6 +334,7 @@ class ObjectCache {
                        // Let pre-emptive refreshes happen post-send on HTTP requests
                        $params['asyncHandler'] = [ DeferredUpdates::class, 'addCallableUpdate' ];
                }
+               $params['secret'] = $params['secret'] ?? $wgSecretKey;
                $class = $params['class'];
 
                return new $class( $params );
@@ -359,24 +360,13 @@ class ObjectCache {
         * @deprecated Since 1.28 Use MediaWikiServices::getInstance()->getMainWANObjectCache()
         */
        public static function getMainWANInstance() {
+               wfDeprecated( __METHOD__, '1.28' );
                return MediaWikiServices::getInstance()->getMainWANObjectCache();
        }
 
        /**
         * Get the cache object for the main stash.
         *
-        * Stash objects are BagOStuff instances suitable for storing light
-        * weight data that is not canonically stored elsewhere (such as RDBMS).
-        * Stashes should be configured to propagate changes to all data-centers.
-        *
-        * Callers should be prepared for:
-        *   - a) Writes to be slower in non-"primary" (e.g. HTTP GET/HEAD only) DCs
-        *   - b) Reads to be eventually consistent, e.g. for get()/getMulti()
-        * In general, this means avoiding updates on idempotent HTTP requests and
-        * avoiding an assumption of perfect serializability (or accepting anomalies).
-        * Reads may be eventually consistent or data might rollback as nodes flap.
-        * Callers can use BagOStuff:READ_LATEST to see the latest available data.
-        *
         * @return BagOStuff
         * @since 1.26
         * @deprecated Since 1.28 Use MediaWikiServices::getInstance()->getMainObjectStash()
index 8bc053d..7e5a8a4 100644 (file)
@@ -110,7 +110,7 @@ class SqlBagOStuff extends BagOStuff {
         *                  MySQL bugs 61735 <https://bugs.mysql.com/bug.php?id=61735>
         *                  and 61736 <https://bugs.mysql.com/bug.php?id=61736>.
         *
-        *   - slaveOnly:   Whether to only use replica DBs and avoid triggering
+        *   - replicaOnly: Whether to only use replica DBs and avoid triggering
         *                  garbage collection logic of expired items. This only
         *                  makes sense if the primary DB is used and only if get()
         *                  calls will be used. This is used by ReplicatedBagOStuff.
@@ -162,7 +162,8 @@ class SqlBagOStuff extends BagOStuff {
                if ( isset( $params['syncTimeout'] ) ) {
                        $this->syncTimeout = $params['syncTimeout'];
                }
-               $this->replicaOnly = !empty( $params['slaveOnly'] );
+               // Backwards-compatibility for < 1.34
+               $this->replicaOnly = $params['replicaOnly'] ?? ( $params['slaveOnly'] ?? false );
        }
 
        /**
@@ -173,19 +174,20 @@ class SqlBagOStuff extends BagOStuff {
         * @throws MWException
         */
        protected function getDB( $serverIndex ) {
-               if ( !isset( $this->conns[$serverIndex] ) ) {
-                       if ( $serverIndex >= $this->numServers ) {
-                               throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
-                       }
+               if ( $serverIndex >= $this->numServers ) {
+                       throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
+               }
 
-                       # Don't keep timing out trying to connect for each call if the DB is down
-                       if ( isset( $this->connFailureErrors[$serverIndex] )
-                               && ( time() - $this->connFailureTimes[$serverIndex] ) < 60
-                       ) {
-                               throw $this->connFailureErrors[$serverIndex];
-                       }
+               # Don't keep timing out trying to connect for each call if the DB is down
+               if (
+                       isset( $this->connFailureErrors[$serverIndex] ) &&
+                       ( time() - $this->connFailureTimes[$serverIndex] ) < 60
+               ) {
+                       throw $this->connFailureErrors[$serverIndex];
+               }
 
-                       if ( $this->serverInfos ) {
+               if ( $this->serverInfos ) {
+                       if ( !isset( $this->conns[$serverIndex] ) ) {
                                // Use custom database defined by server connection info
                                $info = $this->serverInfos[$serverIndex];
                                $type = $info['type'] ?? 'mysql';
@@ -193,25 +195,26 @@ class SqlBagOStuff extends BagOStuff {
                                $this->logger->debug( __CLASS__ . ": connecting to $host" );
                                $db = Database::factory( $type, $info );
                                $db->clearFlag( DBO_TRX ); // auto-commit mode
+                               $this->conns[$serverIndex] = $db;
+                       }
+                       $db = $this->conns[$serverIndex];
+               } else {
+                       // Use the main LB database
+                       $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+                       $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
+                       if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
+                               // Keep a separate connection to avoid contention and deadlocks
+                               $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
                        } else {
-                               // Use the main LB database
-                               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-                               $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
-                               if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
-                                       // Keep a separate connection to avoid contention and deadlocks
-                                       $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
-                               } else {
-                                       // However, SQLite has the opposite behavior due to DB-level locking.
-                                       // Stock sqlite MediaWiki installs use a separate sqlite cache DB instead.
-                                       $db = $lb->getConnection( $index );
-                               }
+                               // However, SQLite has the opposite behavior due to DB-level locking.
+                               // Stock sqlite MediaWiki installs use a separate sqlite cache DB instead.
+                               $db = $lb->getConnection( $index );
                        }
-
-                       $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) );
-                       $this->conns[$serverIndex] = $db;
                }
 
-               return $this->conns[$serverIndex];
+               $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) );
+
+               return $db;
        }
 
        /**
@@ -339,7 +342,7 @@ class SqlBagOStuff extends BagOStuff {
                return $values;
        }
 
-       public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+       protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
                return $this->modifyMulti( $data, $exptime, $flags, self::$OP_SET );
        }
 
@@ -508,7 +511,7 @@ class SqlBagOStuff extends BagOStuff {
                return (bool)$db->affectedRows();
        }
 
-       public function doDeleteMulti( array $keys, $flags = 0 ) {
+       protected function doDeleteMulti( array $keys, $flags = 0 ) {
                return $this->modifyMulti(
                        array_fill_keys( $keys, null ),
                        0,
@@ -564,7 +567,7 @@ class SqlBagOStuff extends BagOStuff {
                return $ok;
        }
 
-       public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+       protected function doChangeTTLMulti( array $keys, $exptime, $flags = 0 ) {
                return $this->modifyMulti(
                        array_fill_keys( $keys, null ),
                        $exptime,
@@ -609,8 +612,6 @@ class SqlBagOStuff extends BagOStuff {
                if (
                        // Random purging is enabled
                        $this->purgePeriod &&
-                       // This is not using a replica DB
-                       !$this->replicaOnly &&
                        // Only purge on one in every $this->purgePeriod writes
                        mt_rand( 0, $this->purgePeriod - 1 ) == 0 &&
                        // Avoid repeating the delete within a few seconds
@@ -635,7 +636,7 @@ class SqlBagOStuff extends BagOStuff {
 
        public function deleteObjectsExpiringBefore(
                $timestamp,
-               callable $progressCallback = null,
+               callable $progress = null,
                $limit = INF
        ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
@@ -654,7 +655,7 @@ class SqlBagOStuff extends BagOStuff {
                                $this->deleteServerObjectsExpiringBefore(
                                        $db,
                                        $timestamp,
-                                       $progressCallback,
+                                       $progress,
                                        $limit,
                                        $numServersDone,
                                        $keysDeletedCount
index aa38d1f..4e28085 100644 (file)
@@ -1400,11 +1400,11 @@ class Article implements Page {
                # Show delete and move logs if there were any such events.
                # The logging query can DOS the site when bots/crawlers cause 404 floods,
                # so be careful showing this. 404 pages must be cheap as they are hard to cache.
-               $cache = $services->getMainObjectStash();
-               $key = $cache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
+               $dbCache = ObjectCache::getInstance( 'db-replicated' );
+               $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
                $loggedIn = $this->getContext()->getUser()->isLoggedIn();
                $sessionExists = $this->getContext()->getRequest()->getSession()->isPersistent();
-               if ( $loggedIn || $cache->get( $key ) || $sessionExists ) {
+               if ( $loggedIn || $dbCache->get( $key ) || $sessionExists ) {
                        $logTypes = [ 'delete', 'move', 'protect' ];
 
                        $dbr = wfGetDB( DB_REPLICA );
index 9e80cf4..fdba6fb 100644 (file)
@@ -1588,7 +1588,7 @@ class WikiPage implements Page, IDBAccessObject {
                $baseRevId = null;
                if ( $edittime && $sectionId !== 'new' ) {
                        $lb = $this->getDBLoadBalancer();
-                       $dbr = $lb->getConnection( DB_REPLICA );
+                       $dbr = $lb->getConnectionRef( DB_REPLICA );
                        $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
                        // Try the master if this thread may have just added it.
                        // This could be abstracted into a Revision method, but we don't want
@@ -1597,7 +1597,7 @@ class WikiPage implements Page, IDBAccessObject {
                                && $lb->getServerCount() > 1
                                && $lb->hasOrMadeRecentMasterChanges()
                        ) {
-                               $dbw = $lb->getConnection( DB_MASTER );
+                               $dbw = $lb->getConnectionRef( DB_MASTER );
                                $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
                        }
                        if ( $rev ) {
@@ -2111,6 +2111,11 @@ class WikiPage implements Page, IDBAccessObject {
         *   - defer: one of the DeferredUpdates constants, or false to run immediately (default: false).
         *     Note that even when this is set to false, some updates might still get deferred (as
         *     some update might directly add child updates to DeferredUpdates).
+        *   - known-revision-output: a combined canonical ParserOutput for the revision, perhaps
+        *     from some cache. The caller is responsible for ensuring that the ParserOutput indeed
+        *     matched the $rev and $options. This mechanism is intended as a temporary stop-gap,
+        *     for the time until caches have been changed to store RenderedRevision states instead
+        *     of ParserOutput objects. (default: null) (since 1.33)
         * @since 1.32
         */
        public function doSecondaryDataUpdates( array $options = [] ) {
@@ -2806,9 +2811,9 @@ class WikiPage implements Page, IDBAccessObject {
                        $status->value = $logid;
 
                        // Show log excerpt on 404 pages rather than just a link
-                       $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-                       $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
-                       $cache->set( $key, 1, $cache::TTL_DAY );
+                       $dbCache = ObjectCache::getInstance( 'db-replicated' );
+                       $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
+                       $dbCache->set( $key, 1, $dbCache::TTL_DAY );
                }
 
                return $status;
index 16e4397..7379679 100644 (file)
@@ -29,6 +29,8 @@ abstract class PoolCounterWork {
        protected $type = 'generic';
        /** @var bool */
        protected $cacheable = false; // does this override getCachedWork() ?
+       /** @var PoolCounter */
+       private $poolCounter;
 
        /**
         * @param string $type The class of actions to limit concurrency for (task type)
index ec376e3..6eb9908 100644 (file)
@@ -566,7 +566,7 @@ class ResourceLoader implements LoggerAwareInterface {
                if ( isset( $info['object'] ) ) {
                        return false;
                }
-               return (
+               return !isset( $info['factory'] ) && (
                        // The implied default for 'class' is ResourceLoaderFileModule
                        !isset( $info['class'] ) ||
                        // Explicit default
index 5b39fc7..9003951 100644 (file)
@@ -226,10 +226,13 @@ class ResourceLoaderImage {
                        'image' => $this->getName(),
                        'variant' => $variant,
                        'format' => $format,
-                       'lang' => $context->getLanguage(),
-                       'skin' => $context->getSkin(),
-                       'version' => $context->getVersion(),
                ];
+               if ( $this->varyOnLanguage() ) {
+                       $query['lang'] = $context->getLanguage();
+               }
+               // The following parameters are at the end to keep the original order of the parameters.
+               $query['skin'] = $context->getSkin();
+               $query['version'] = $context->getVersion();
 
                return wfAppendQuery( $script, $query );
        }
@@ -458,4 +461,16 @@ class ResourceLoaderImage {
                        return $png ?: false;
                }
        }
+
+       /**
+        * Check if the image depends on the language.
+        *
+        * @return bool
+        */
+       private function varyOnLanguage() {
+               return is_array( $this->descriptor ) && (
+                       isset( $this->descriptor['ltr'] ) ||
+                       isset( $this->descriptor['rtl'] ) ||
+                       isset( $this->descriptor['lang'] ) );
+       }
 }
index a5d351b..7240e81 100644 (file)
@@ -240,7 +240,7 @@ class SearchOracle extends SearchDatabase {
         * @param string $text
         */
        function update( $id, $title, $text ) {
-               $dbw = $this->lb->getConnection( DB_MASTER );
+               $dbw = $this->lb->getMaintenanceConnectionRef( DB_MASTER );
                $dbw->replace( 'searchindex',
                        [ 'si_page' ],
                        [
index 3646b27..dedcdff 100644 (file)
@@ -33,11 +33,15 @@ class SearchSqlite extends SearchDatabase {
         * Whether fulltext search is supported by current schema
         * @return bool
         */
-       function fulltextSearchSupported() {
+       private function fulltextSearchSupported() {
+               // Avoid getConnectionRef() in order to get DatabaseSqlite specifically
                /** @var DatabaseSqlite $dbr */
                $dbr = $this->lb->getConnection( DB_REPLICA );
-
-               return $dbr->checkForEnabledSearch();
+               try {
+                       return $dbr->checkForEnabledSearch();
+               } finally {
+                       $this->lb->reuseConnection( $dbr );
+               }
        }
 
        /**
@@ -285,7 +289,7 @@ class SearchSqlite extends SearchDatabase {
         * @param string $title
         * @param string $text
         */
-       function update( $id, $title, $text ) {
+       public function update( $id, $title, $text ) {
                if ( !$this->fulltextSearchSupported() ) {
                        return;
                }
@@ -308,7 +312,7 @@ class SearchSqlite extends SearchDatabase {
         * @param int $id
         * @param string $title
         */
-       function updateTitle( $id, $title ) {
+       public function updateTitle( $id, $title ) {
                if ( !$this->fulltextSearchSupported() ) {
                        return;
                }
index f14e0eb..64c2b84 100644 (file)
@@ -25,6 +25,7 @@ namespace MediaWiki\Session;
 
 use Psr\Log\LoggerInterface;
 use BagOStuff;
+use Psr\Log\NullLogger;
 
 /**
  * Adapter for PHP's session handling
@@ -41,7 +42,7 @@ class PHPSessionHandler implements \SessionHandlerInterface {
        /** @var bool */
        protected $warn = true;
 
-       /** @var SessionManager|null */
+       /** @var SessionManagerInterface|null */
        protected $manager;
 
        /** @var BagOStuff|null */
@@ -53,7 +54,7 @@ class PHPSessionHandler implements \SessionHandlerInterface {
        /** @var array Track original session fields for later modification check */
        protected $sessionFieldCache = [];
 
-       protected function __construct( SessionManager $manager ) {
+       protected function __construct( SessionManagerInterface $manager ) {
                $this->setEnableFlags(
                        \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
                );
@@ -105,9 +106,9 @@ class PHPSessionHandler implements \SessionHandlerInterface {
 
        /**
         * Install a session handler for the current web request
-        * @param SessionManager $manager
+        * @param SessionManagerInterface $manager
         */
-       public static function install( SessionManager $manager ) {
+       public static function install( SessionManagerInterface $manager ) {
                if ( self::$instance ) {
                        $manager->setupPHPSessionHandler( self::$instance );
                        return;
@@ -151,12 +152,12 @@ class PHPSessionHandler implements \SessionHandlerInterface {
        /**
         * Set the manager, store, and logger
         * @private Use self::install().
-        * @param SessionManager $manager
+        * @param SessionManagerInterface $manager
         * @param BagOStuff $store
         * @param LoggerInterface $logger
         */
        public function setManager(
-               SessionManager $manager, BagOStuff $store, LoggerInterface $logger
+               SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger
        ) {
                if ( $this->manager !== $manager ) {
                        // Close any existing session before we change stores
@@ -299,7 +300,7 @@ class PHPSessionHandler implements \SessionHandlerInterface {
                }
                // Anything deleted in $_SESSION and unchanged in Session should be deleted too
                // (but not if $_SESSION can't represent it at all)
-               \Wikimedia\PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() );
+               \Wikimedia\PhpSessionSerializer::setLogger( new NullLogger() );
                foreach ( $cache as $key => $value ) {
                        if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
                                \Wikimedia\PhpSessionSerializer::encode( [ $key => true ] )
index bb6a6b3..6076aba 100644 (file)
@@ -75,7 +75,7 @@ class DBSiteStore implements SiteStore {
        protected function loadSites() {
                $this->sites = new SiteList();
 
-               $dbr = $this->dbLoadBalancer->getConnection( DB_REPLICA );
+               $dbr = $this->dbLoadBalancer->getConnectionRef( DB_REPLICA );
 
                $res = $dbr->select(
                        'sites',
@@ -178,7 +178,7 @@ class DBSiteStore implements SiteStore {
                        return true;
                }
 
-               $dbw = $this->dbLoadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->dbLoadBalancer->getConnectionRef( DB_MASTER );
 
                $dbw->startAtomic( __METHOD__ );
 
@@ -269,7 +269,7 @@ class DBSiteStore implements SiteStore {
         * @return bool Success
         */
        public function clear() {
-               $dbw = $this->dbLoadBalancer->getConnection( DB_MASTER );
+               $dbw = $this->dbLoadBalancer->getConnectionRef( DB_MASTER );
 
                $dbw->startAtomic( __METHOD__ );
                $ok = $dbw->delete( 'sites', '*', __METHOD__ );
index 5d6197e..6ddced4 100644 (file)
@@ -256,7 +256,7 @@ class SkinTemplate extends Skin {
         * @return QuickTemplate The template to be executed by outputPage
         */
        protected function prepareQuickTemplate() {
-               global $wgScript, $wgStylePath, $wgMimeType, $wgJsMimeType,
+               global $wgScript, $wgStylePath, $wgMimeType,
                        $wgSitename, $wgLogo, $wgMaxCredits,
                        $wgShowCreditsIfMax, $wgArticlePath,
                        $wgScriptPath, $wgServer;
@@ -306,7 +306,6 @@ class SkinTemplate extends Skin {
                }
 
                $tpl->set( 'mimetype', $wgMimeType );
-               $tpl->set( 'jsmimetype', $wgJsMimeType );
                $tpl->set( 'charset', 'UTF-8' );
                $tpl->set( 'wgScript', $wgScript );
                $tpl->set( 'skinname', $this->skinname );
index 3e1909b..17f89f9 100644 (file)
@@ -163,6 +163,11 @@ class BrokenRedirectsPage extends QueryPage {
                return $out;
        }
 
+       public function execute( $par ) {
+               $this->addHelpLink( 'Help:Redirects' );
+               parent::execute( $par );
+       }
+
        /**
         * Cache page content model for performance
         *
index f899d76..6841cc5 100644 (file)
@@ -49,27 +49,6 @@ class SpecialChangeCredentials extends AuthManagerSpecialPage {
                return $params;
        }
 
-       public function onAuthChangeFormFields(
-               array $requests, array $fieldInfo, array &$formDescriptor, $action
-       ) {
-               // This method is never called for remove actions.
-
-               $extraFields = [];
-               Hooks::run( 'ChangePasswordForm', [ &$extraFields ], '1.27' );
-               foreach ( $extraFields as $extra ) {
-                       list( $name, $label, $type, $default ) = $extra;
-                       $formDescriptor[$name] = [
-                               'type' => $type,
-                               'name' => $name,
-                               'label-message' => $label,
-                               'default' => $default,
-                       ];
-
-               }
-
-               return parent::onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
-       }
-
        public function execute( $subPage ) {
                $this->setHeaders();
                $this->outputHeader();
index 956ff77..c95aa1b 100644 (file)
@@ -78,10 +78,6 @@ class SpecialChangeEmail extends FormSpecialPage {
                        throw new PermissionsError( 'viewmyprivateinfo' );
                }
 
-               if ( $user->isBlockedFromEmailuser() ) {
-                       throw new UserBlockedError( $user->getBlock() );
-               }
-
                parent::checkExecutePermissions( $user );
        }
 
index 36928ca..6d9dc0f 100644 (file)
@@ -49,6 +49,7 @@ class SpecialComparePages extends SpecialPage {
                $this->setHeaders();
                $this->outputHeader();
                $this->getOutput()->addModuleStyles( 'mediawiki.special' );
+               $this->addHelpLink( 'Help:Diff' );
 
                $form = HTMLForm::factory( 'ooui', [
                        'Page1' => [
index 8817ba3..40d8962 100644 (file)
@@ -46,6 +46,7 @@ class DeletedContributionsPage extends SpecialPage {
                $this->setHeaders();
                $this->outputHeader();
                $this->checkPermissions();
+               $this->addHelpLink( 'Help:User contributions' );
 
                $out = $this->getOutput();
                $out->setPageTitle( $this->msg( 'deletedcontributions-title' ) );
index 77c59f0..fcf1bb2 100644 (file)
@@ -198,6 +198,11 @@ class DoubleRedirectsPage extends QueryPage {
                return ( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
        }
 
+       public function execute( $par ) {
+               $this->addHelpLink( 'Help:Redirects' );
+               parent::execute( $par );
+       }
+
        /**
         * Cache page content model and gender distinction for performance
         *
index ae4b090..7f00311 100644 (file)
@@ -45,6 +45,7 @@ class SpecialListGroupRights extends SpecialPage {
 
                $out = $this->getOutput();
                $out->addModuleStyles( 'mediawiki.special' );
+               $this->addHelpLink( 'Help:User_rights_and_groups' );
 
                $out->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
 
index 48f3640..3284c57 100644 (file)
@@ -145,6 +145,11 @@ class ListredirectsPage extends QueryPage {
                }
        }
 
+       public function execute( $par ) {
+               $this->addHelpLink( 'Help:Redirects' );
+               parent::execute( $par );
+       }
+
        protected function getGroupName() {
                return 'pages';
        }
index e9dca35..38c6b11 100644 (file)
@@ -38,6 +38,7 @@ class SpecialProtectedpages extends SpecialPage {
                $this->setHeaders();
                $this->outputHeader();
                $this->getOutput()->addModuleStyles( 'mediawiki.special' );
+               $this->addHelpLink( 'Help:Protected_pages' );
 
                $request = $this->getRequest();
                $type = $request->getVal( $this->IdType );
index 5dc49ea..4b0997e 100644 (file)
@@ -37,6 +37,7 @@ class SpecialProtectedtitles extends SpecialPage {
        function execute( $par ) {
                $this->setHeaders();
                $this->outputHeader();
+               $this->addHelpLink( 'Help:Protected_pages' );
 
                $request = $this->getRequest();
                $type = $request->getVal( $this->IdType );
index 110fb1f..9a95249 100644 (file)
@@ -50,6 +50,7 @@ class SpecialTags extends SpecialPage {
        function execute( $par ) {
                $this->setHeaders();
                $this->outputHeader();
+               $this->addHelpLink( 'Manual:Tags' );
 
                $request = $this->getRequest();
                switch ( $par ) {
index 1cb27a3..ed2d5cf 100644 (file)
@@ -31,6 +31,7 @@
 class UncategorizedImagesPage extends ImageQueryPage {
        function __construct( $name = 'Uncategorizedimages' ) {
                parent::__construct( $name );
+               $this->addHelpLink( 'Help:Categories' );
        }
 
        function sortDescending() {
index ab83af1..0b7da7b 100644 (file)
@@ -35,6 +35,7 @@ class UncategorizedPagesPage extends PageQueryPage {
 
        function __construct( $name = 'Uncategorizedpages' ) {
                parent::__construct( $name );
+               $this->addHelpLink( 'Help:Categories' );
        }
 
        function sortDescending() {
index 95563d2..31e4836 100644 (file)
@@ -158,6 +158,7 @@ class SpecialUndelete extends SpecialPage {
 
                $this->setHeaders();
                $this->outputHeader();
+               $this->addHelpLink( 'Help:Deletion_and_undeletion' );
 
                $this->loadRequest( $par );
                $this->checkPermissions(); // Needs to be after mTargetObj is set
index ae5b732..41c42ce 100644 (file)
@@ -20,7 +20,6 @@
  * @file
  * @ingroup Upload
  */
-use MediaWiki\MediaWikiServices;
 use MediaWiki\Shell\Shell;
 
 /**
@@ -42,13 +41,36 @@ abstract class UploadBase {
        protected $mTempPath;
        /** @var TempFSFile|null Wrapper to handle deleting the temp file */
        protected $tempFileObj;
-
-       protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
-       protected $mTitle = false, $mTitleError = 0;
-       protected $mFilteredName, $mFinalExtension;
-       protected $mLocalFile, $mStashFile, $mFileSize, $mFileProps;
+       /** @var string|null */
+       protected $mDesiredDestName;
+       /** @var string|null */
+       protected $mDestName;
+       /** @var string|null */
+       protected $mRemoveTempFile;
+       /** @var string|null */
+       protected $mSourceType;
+       /** @var Title|bool */
+       protected $mTitle = false;
+       /** @var int */
+       protected $mTitleError = 0;
+       /** @var string|null */
+       protected $mFilteredName;
+       /** @var string|null */
+       protected $mFinalExtension;
+       /** @var LocalFile */
+       protected $mLocalFile;
+       /** @var UploadStashFile */
+       protected $mStashFile;
+       /** @var int|null */
+       protected $mFileSize;
+       /** @var array|null */
+       protected $mFileProps;
+       /** @var string[] */
        protected $mBlackListedExtensions;
-       protected $mJavaDetected, $mSVGNSError;
+       /** @var bool|null */
+       protected $mJavaDetected;
+       /** @var string|null */
+       protected $mSVGNSError;
 
        protected static $safeXmlEncodings = [
                'UTF-8',
@@ -1508,7 +1530,7 @@ abstract class UploadBase {
         * @todo Replace this with a whitelist filter!
         * @param string $element
         * @param array $attribs
-        * @param array|null $data
+        * @param string|null $data
         * @return bool|array
         */
        public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
@@ -2191,10 +2213,10 @@ abstract class UploadBase {
         * @return Status[]|bool
         */
        public static function getSessionStatus( User $user, $statusKey ) {
-               $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-               $key = $cache->makeKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey );
+               $store = self::getUploadSessionStore();
+               $key = self::getUploadSessionKey( $store, $user, $statusKey );
 
-               return $cache->get( $key );
+               return $store->get( $key );
        }
 
        /**
@@ -2202,19 +2224,42 @@ abstract class UploadBase {
         *
         * The value will be set in cache for 1 day
         *
+        * Avoid triggering this method on HTTP GET/HEAD requests
+        *
         * @param User $user
         * @param string $statusKey
         * @param array|bool $value
         * @return void
         */
        public static function setSessionStatus( User $user, $statusKey, $value ) {
-               $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-               $key = $cache->makeKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey );
+               $store = self::getUploadSessionStore();
+               $key = self::getUploadSessionKey( $store, $user, $statusKey );
 
                if ( $value === false ) {
-                       $cache->delete( $key );
+                       $store->delete( $key );
                } else {
-                       $cache->set( $key, $value, $cache::TTL_DAY );
+                       $store->set( $key, $value, $store::TTL_DAY );
                }
        }
+
+       /**
+        * @param BagOStuff $store
+        * @param User $user
+        * @param string $statusKey
+        * @return string
+        */
+       private static function getUploadSessionKey( BagOStuff $store, User $user, $statusKey ) {
+               return $store->makeKey(
+                       'uploadstatus',
+                       $user->getId() ?: md5( $user->getName() ),
+                       $statusKey
+               );
+       }
+
+       /**
+        * @return BagOStuff
+        */
+       private static function getUploadSessionStore() {
+               return ObjectCache::getInstance( 'db-replicated' );
+       }
 }
index 3e88fcd..1bd99c1 100644 (file)
  * @author Michael Dale
  */
 class UploadFromChunks extends UploadFromFile {
+       /** @var LocalRepo */
+       private $repo;
+       /** @var UploadStash */
+       public $stash;
+       /** @var User */
+       public $user;
+
        protected $mOffset;
        protected $mChunkIndex;
        protected $mFileKey;
        protected $mVirtualTempPath;
-       /** @var LocalRepo */
-       private $repo;
+
+       /** @noinspection PhpMissingParentConstructorInspection */
 
        /**
         * Setup local pointers to stash, repo and user (similar to UploadFromStash)
index 84298e2..7c2f038 100644 (file)
@@ -243,10 +243,19 @@ class User implements IDBAccessObject, UserIdentity {
                return (string)$this->getName();
        }
 
-       public function __get( $name ) {
+       public function &__get( $name ) {
                // A shortcut for $mRights deprecation phase
                if ( $name === 'mRights' ) {
-                       return $this->getRights();
+                       $copy = $this->getRights();
+                       return $copy;
+               } elseif ( !property_exists( $this, $name ) ) {
+                       // T227688 - do not break $u->foo['bar'] = 1
+                       wfLogWarning( 'tried to get non-existent property' );
+                       $this->$name = null;
+                       return $this->$name;
+               } else {
+                       wfLogWarning( 'tried to get non-visible property' );
+                       return null;
                }
        }
 
@@ -258,6 +267,10 @@ class User implements IDBAccessObject, UserIdentity {
                                $this,
                                is_null( $value ) ? [] : $value
                        );
+               } elseif ( !property_exists( $this, $name ) ) {
+                       $this->$name = $value;
+               } else {
+                       wfLogWarning( 'tried to set non-visible property' );
                }
        }
 
@@ -2571,7 +2584,7 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $mode === 'refresh' ) {
                        $cache->delete( $key, 1 ); // low tombstone/"hold-off" TTL
                } else {
-                       $lb->getConnection( DB_MASTER )->onTransactionPreCommitOrIdle(
+                       $lb->getConnectionRef( DB_MASTER )->onTransactionPreCommitOrIdle(
                                function () use ( $cache, $key ) {
                                        $cache->delete( $key );
                                },
index e06df9f..fdac4a2 100644 (file)
@@ -159,7 +159,15 @@ class UserGroupMembership {
                }
 
                // Purge old, expired memberships from the DB
-               JobQueueGroup::singleton()->push( new UserGroupExpiryJob() );
+               $hasExpiredRow = $dbw->selectField(
+                       'user_groups',
+                       '1',
+                       [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
+                       __METHOD__
+               );
+               if ( $hasExpiredRow ) {
+                       JobQueueGroup::singleton()->lazyPush( new UserGroupExpiryJob() );
+               }
 
                // Check that the values make sense
                if ( $this->group === null ) {
@@ -248,9 +256,9 @@ class UserGroupMembership {
 
                $lbFactory = $services->getDBLoadBalancerFactory();
                $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
-               $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
+               $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER );
 
-               $lockKey = $dbw->getDomainID() . ':usergroups-prune'; // specific to this wiki
+               $lockKey = "{$dbw->getDomainID()}:UserGroupMembership:purge"; // per-wiki
                $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
                if ( !$scopedLock ) {
                        return false; // already running
index 1a39945..d33b6ae 100644 (file)
@@ -1120,6 +1120,11 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                }
 
                $oldRev = $this->revisionLookup->getRevisionById( $oldid );
+               if ( !$oldRev ) {
+                       // Oldid given but does not exist (probably deleted)
+                       return false;
+               }
+
                $nextRev = $this->revisionLookup->getNextRevision( $oldRev );
                if ( !$nextRev ) {
                        // Oldid given and is the latest revision for this title; clear the timestamp.
index e05b7a7..0a15530 100644 (file)
@@ -3226,6 +3226,7 @@ public static $zh2Hant = [
 '〇周后' => '〇周後',
 '〇只' => '〇隻',
 '〇余' => '〇餘',
+'》里' => '》裡',
 '“' => '「',
 '”' => '」',
 '‘' => '『',
@@ -4321,6 +4322,7 @@ public static $zh2Hant = [
 '包准' => '包準',
 '包谷' => '包穀',
 '包扎' => '包紮',
+'包制' => '包製',
 '匏系' => '匏繫',
 '北山索面' => '北山索麵',
 '北仑河' => '北崙河',
@@ -4515,7 +4517,6 @@ public static $zh2Hant = [
 '后安路' => '后安路',
 '后平路' => '后平路',
 '后庄' => '后庄',
-'后座' => '后座',
 '后母戊' => '后母戊',
 '后海湾' => '后海灣',
 '后海灣' => '后海灣',
@@ -5989,6 +5990,7 @@ public static $zh2Hant = [
 '方便面' => '方便麵',
 '方向' => '方向',
 '方法里' => '方法裡',
+'于吉林' => '於吉林',
 '于震中' => '於震中',
 '于震前' => '於震前',
 '于震后' => '於震後',
@@ -6160,6 +6162,7 @@ public static $zh2Hant = [
 '松口镇' => '松口鎮',
 '松山庄' => '松山庄',
 '松溪县' => '松谿縣',
+'松开始' => '松開始',
 '板荡' => '板蕩',
 '林宏岳' => '林宏嶽',
 '林杰樑' => '林杰樑',
@@ -6693,6 +6696,7 @@ public static $zh2Hant = [
 '片里' => '片裡',
 '片言只语' => '片言隻語',
 '版图里' => '版圖裡',
+'版本里' => '版本裡',
 '牙签' => '牙籤',
 '牛只' => '牛隻',
 '物欲' => '物慾',
@@ -6741,6 +6745,7 @@ public static $zh2Hant = [
 '理次发' => '理次髮',
 '理发动' => '理發動',
 '理发展' => '理發展',
+'理发放' => '理發放',
 '理发现' => '理發現',
 '理发生' => '理發生',
 '理发表' => '理發表',
@@ -7064,6 +7069,7 @@ public static $zh2Hant = [
 '空蒙' => '空濛',
 '空荡' => '空蕩',
 '空荡荡' => '空蕩蕩',
+'空里' => '空裡',
 '空钟' => '空鐘',
 '空余' => '空餘',
 '窒欲' => '窒慾',
@@ -7792,6 +7798,7 @@ public static $zh2Hant = [
 '制成' => '製成',
 '制毒' => '製毒',
 '制法' => '製法',
+'制汉字' => '製漢字',
 '制浆' => '製漿',
 '制片' => '製片',
 '制版' => '製版',
@@ -7904,7 +7911,6 @@ public static $zh2Hant = [
 '托交' => '託交',
 '托付' => '託付',
 '托克逊' => '託克遜',
-'托儿' => '託兒',
 '托古讽今' => '託古諷今',
 '托名' => '託名',
 '托命' => '託命',
@@ -8245,6 +8251,7 @@ public static $zh2Hant = [
 '这只比' => '這只比',
 '这只用' => '這只用',
 '这只能' => '這只能',
+'这只要' => '這只要',
 '这只限' => '這只限',
 '这只需' => '這只需',
 '这只须' => '這只須',
@@ -8356,6 +8363,7 @@ public static $zh2Hant = [
 '那只比' => '那只比',
 '那只用' => '那只用',
 '那只能' => '那只能',
+'那只要' => '那只要',
 '那只限' => '那只限',
 '那只需' => '那只需',
 '那只须' => '那只須',
@@ -8471,6 +8479,7 @@ public static $zh2Hant = [
 '厘革' => '釐革',
 '金仆姑' => '金僕姑',
 '金城里' => '金城里',
+'金发放' => '金發放',
 '金范' => '金範',
 '金圣叹' => '金聖歎',
 '金表情' => '金表情',
@@ -8647,6 +8656,8 @@ public static $zh2Hant = [
 '闯荡' => '闖蕩',
 '闯炼' => '闖鍊',
 '关系' => '關係',
+'关注:' => '關注:',
+'關注:' => '關注:',
 '关系列' => '關系列',
 '关系所' => '關系所',
 '关系科' => '關系科',
@@ -10039,7 +10050,6 @@ public static $zh2Hans = [
 '島' => '岛',
 '峽' => '峡',
 '崍' => '崃',
-'崑' => '昆',
 '崗' => '岗',
 '崙' => '仑',
 '崢' => '峥',
@@ -13641,6 +13651,7 @@ public static $zh2Hans = [
 '昇汞' => '升汞',
 '陞用' => '升用',
 '陞補' => '升补',
+'昇起' => '升起',
 '陞遷' => '升迁',
 '昇降' => '升降',
 '卓著' => '卓著',
@@ -13818,6 +13829,16 @@ public static $zh2Hans = [
 '旋乾转坤' => '旋乾转坤',
 '無言不讎' => '无言不雠',
 '曠若發矇' => '旷若发矇',
+'崑崙' => '昆仑',
+'崑岡' => '昆冈',
+'崑劇' => '昆剧',
+'崑山' => '昆山',
+'崑嵛' => '昆嵛',
+'崑承湖' => '昆承湖',
+'崑曲' => '昆曲',
+'崑腔' => '昆腔',
+'崑蘇' => '昆苏',
+'崑調' => '昆调',
 '易·乾' => '易·乾',
 '易經·乾' => '易经·乾',
 '易经·乾' => '易经·乾',
@@ -13869,6 +13890,7 @@ public static $zh2Hans = [
 '瀋液' => '渖液',
 '满拚自尽' => '满拚自尽',
 '滿拚自盡' => '满拚自尽',
+'靈崑' => '灵昆',
 '薰習' => '熏习',
 '薰心' => '熏心',
 '薰沐' => '熏沐',
@@ -13924,6 +13946,7 @@ public static $zh2Hans = [
 '老么' => '老幺',
 '肉乾乾' => '肉干干',
 '肘手鍊足' => '肘手链足',
+'蘇崑' => '苏昆',
 '甦醒' => '苏醒',
 '苧烯' => '苧烯',
 '薴烯' => '苧烯',
@@ -13948,6 +13971,7 @@ public static $zh2Hans = [
 '蔡孝乾' => '蔡孝乾',
 '蔡絛' => '蔡絛',
 '行餘' => '行馀',
+'西崑' => '西昆',
 '覆蓋' => '覆盖',
 '見微知著' => '见微知著',
 '見著' => '见著',
@@ -14151,8 +14175,8 @@ public static $zh2TW = [
 '局域网' => '區域網',
 '局域网络' => '區域網路',
 '十杆' => '十桿',
-'特立尼达和托巴哥' => '千里達托貝哥',
-'特立尼達和多巴哥' => '千里達托貝哥',
+'特立尼达和托巴哥' => '千里達及托巴哥',
+'特立尼達和多巴哥' => '千里達及托巴哥',
 '不列颠哥伦比亚省' => '卑詩省',
 '南朝鲜' => '南韓',
 '卡斯特罗' => '卡斯楚',
@@ -14235,6 +14259,7 @@ public static $zh2TW = [
 '塞维利亚' => '塞維亞',
 '西維爾' => '塞維亞',
 '塞黑' => '塞蒙',
+'塔希提' => '大溪地',
 '共和联邦' => '大英國協',
 '英联邦' => '大英國協',
 '英聯邦' => '大英國協',
@@ -14282,7 +14307,7 @@ public static $zh2TW = [
 '尼日尔' => '尼日',
 '尼日爾' => '尼日',
 '雅马哈' => '山葉',
-'巴厘岛' => '峇里島',
+'巴厘' => '峇里',
 '特朗普' => '川普',
 '机床' => '工具機',
 '機床' => '工具機',
@@ -14403,6 +14428,7 @@ public static $zh2TW = [
 '萌島' => '曼島',
 '马恩岛' => '曼島',
 '木杆' => '木桿',
+'尾班車' => '末班車',
 '列奥纳多' => '李奧納多',
 '杜塞尔多夫' => '杜塞道夫',
 '杜塞爾多夫' => '杜塞道夫',
@@ -14586,6 +14612,8 @@ public static $zh2TW = [
 '圣基茨和尼维斯' => '聖克里斯多福及尼維斯',
 '聖吉斯納域斯' => '聖克里斯多福及尼維斯',
 '聖佐治' => '聖喬治',
+'圣多美和普林西比' => '聖多美普林西比',
+'聖多美和普林西比' => '聖多美普林西比',
 '圣文森特和格林纳丁斯' => '聖文森及格瑞那丁',
 '聖文森特和格林納丁斯' => '聖文森及格瑞那丁',
 '圣赫勒拿' => '聖赫倫那',
@@ -14694,6 +14722,7 @@ public static $zh2TW = [
 '扎伊爾' => '薩伊',
 '素檀' => '蘇丹',
 '苏里南' => '蘇利南',
+'蘇里南' => '蘇利南',
 '浮罗交怡' => '蘭卡威',
 '浮羅交怡' => '蘭卡威',
 '劳拉' => '蘿拉',
@@ -14843,6 +14872,7 @@ public static $zh2TW = [
 '香煙' => '香菸',
 '馬里共和國' => '馬利共和國',
 '马里共和国' => '馬利共和國',
+'馬拉維' => '馬拉威',
 '马拉维' => '馬拉威',
 '馬勒當拿' => '馬拉度納',
 '马拉多纳' => '馬拉度納',
@@ -14882,6 +14912,7 @@ public static $zh2HK = [
 'IP地址' => 'IP位址',
 '·威尔士' => '·威爾士',
 '·威爾士' => '·威爾士',
+'》里' => '》裏',
 '一地里' => '一地裏',
 '三十六著' => '三十六着',
 '三極體' => '三極管',
@@ -15632,6 +15663,7 @@ public static $zh2HK = [
 '夢著述' => '夢著述',
 '夢著錄' => '夢著錄',
 '梦里' => '夢裏',
+'塔希提' => '大溪地',
 '天里' => '天裏',
 '宇航员' => '太空人',
 '夾著' => '夾着',
@@ -15748,7 +15780,7 @@ public static $zh2HK = [
 '山里的' => '山裏的',
 '甘比亞' => '岡比亞',
 '岸裡' => '岸裡',
-'巴厘岛' => '峇里島',
+'巴厘' => '峇里',
 '工作台' => '工作枱',
 '已占' => '已佔',
 '巴塞罗那' => '巴塞隆拿',
@@ -16623,8 +16655,9 @@ public static $zh2HK = [
 '爭著錄' => '爭著錄',
 '墙里' => '牆裏',
 '版图里' => '版圖裏',
+'版本里' => '版本裏',
 '版权信息' => '版權資訊',
-'千里達托貝哥' => '特立尼達和多巴哥',
+'千里達及托巴哥' => '特立尼達和多巴哥',
 '牽著' => '牽着',
 '牽著作' => '牽著作',
 '牽著名' => '牽著名',
@@ -16908,6 +16941,7 @@ public static $zh2HK = [
 '空著者' => '空著者',
 '空著述' => '空著述',
 '空著錄' => '空著錄',
+'空里' => '空裏',
 '太空梭' => '穿梭機',
 '航天飞机' => '穿梭機',
 '穿著' => '穿着',
@@ -17087,6 +17121,7 @@ public static $zh2HK = [
 '聖喬治' => '聖佐治',
 '圣基茨和尼维斯' => '聖吉斯納域斯',
 '聖克里斯多福及尼維斯' => '聖吉斯納域斯',
+'聖多美普林西比' => '聖多美和普林西比',
 '聖文森及格瑞那丁' => '聖文森特和格林納丁斯',
 '聖露西亞' => '聖盧西亞',
 '聖馬利諾' => '聖馬力諾',
@@ -17218,6 +17253,7 @@ public static $zh2HK = [
 '藏著者' => '藏著者',
 '藏著述' => '藏著述',
 '藏著錄' => '藏著錄',
+'蘇利南' => '蘇里南',
 '蘊涵著' => '蘊涵着',
 '蘸著' => '蘸着',
 '蘸著作' => '蘸著作',
@@ -17853,6 +17889,7 @@ public static $zh2HK = [
 '馬拉度納' => '馬勒當拿',
 '马拉多纳' => '馬勒當拿',
 '马拉特·萨芬' => '馬拉特·沙芬',
+'馬拉威' => '馬拉維',
 '馬斯垂克' => '馬斯特里赫特',
 '馬爾地夫' => '馬爾代夫',
 '馬利共和國' => '馬里共和國',
@@ -17864,6 +17901,7 @@ public static $zh2HK = [
 '駕著者' => '駕著者',
 '駕著述' => '駕著述',
 '駕著錄' => '駕著錄',
+'駛著' => '駛着',
 '騎著' => '騎着',
 '騎著作' => '騎著作',
 '騎著名' => '騎著名',
@@ -17880,7 +17918,6 @@ public static $zh2HK = [
 '騙著者' => '騙著者',
 '騙著述' => '騙著述',
 '騙著錄' => '騙著錄',
-'驶著' => '驶着',
 '体里' => '體裏',
 '高畫質' => '高清',
 '高著' => '高着',
@@ -17965,6 +18002,7 @@ public static $zh2CN = [
 '全球資訊網' => '万维网',
 '三十六著' => '三十六着',
 '三極體' => '三极管',
+'上落客' => '上下客',
 '下著' => '下着',
 '下著作' => '下著作',
 '下著名' => '下著名',
@@ -17975,6 +18013,7 @@ public static $zh2CN = [
 '下著稱' => '下著称',
 '下著者' => '下著者',
 '下著述' => '下著述',
+'落車' => '下车',
 '卑詩省' => '不列颠哥伦比亚省',
 '不著' => '不着',
 '不著書' => '不著书',
@@ -18595,6 +18634,7 @@ public static $zh2CN = [
 '聖露西亞' => '圣卢西亚',
 '聖克里斯多福及尼維斯' => '圣基茨和尼维斯',
 '聖吉斯納域斯' => '圣基茨和尼维斯',
+'聖多美普林西比' => '圣多美和普林西比',
 '聖文森及格瑞那丁' => '圣文森特和格林纳丁斯',
 '聖馬利諾' => '圣马力诺',
 '蓋亞那' => '圭亚那',
@@ -19312,6 +19352,7 @@ public static $zh2CN = [
 '朝著稱' => '朝著称',
 '朝著者' => '朝著者',
 '朝著述' => '朝著述',
+'尾班車' => '末班车',
 '賓·拉登' => '本·拉登',
 '本份' => '本分',
 '賓拉登' => '本拉登',
@@ -19570,7 +19611,6 @@ public static $zh2CN = [
 '牽著述' => '牵著述',
 '千里達' => '特立尼达',
 '千里達及托巴哥' => '特立尼达和多巴哥',
-'千里達托貝哥' => '特立尼达和托巴哥',
 '德蕾莎·梅伊' => '特蕾莎·梅',
 '文翠珊' => '特蕾莎·梅',
 '狗隻' => '犬只',
index 776008e..7cdfa42 100644 (file)
        "nstab-category": "Kategori",
        "mainpage-nstab": "Kaca utama",
        "nosuchspecialpage": "Nénten wénten kaca kusus sakadi punika",
+       "nospecialpagetext": "<strong>Ida nagih kaca pinih luwih sane nenten patut.</strong>\n\nWacakan kaca pinih luwih dados kacingak ring [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Kaiwangan",
        "databaseerror": "Database kaluputan",
        "missing-article": "data utama nenten prasida nemu tulisan saking lembar sane sepatutne wenten, inggih punika  $1, $2\n\nindike puniki biasane keranayang olih pranala kaon nuju pabenahan sane dumun lembar sane sampun kaicalang\n\nyening nenten puniki sane ngranayang, ida dane minab sampun manggihin kaiwangang ring sajeroning piranti lunak.\nDurus sadokang indik puniki rin silih sinunggil anak \n\n[[Special:ListUsers/sysop|Pengurus]], antuk ngetik alamat URL sane katuju",
        "userlogin-noaccount": "Durung madué akun?",
        "userlogin-joinproject": "Nyarengin {{SITENAME}}",
        "createaccount": "Karyanin akun",
+       "userlogin-resetpassword-link": "Engsap ring kruna kunci?",
        "userlogin-helplink2": "Wantuan indik manjing log",
        "createacct-emailoptional": "Alamat email (becikang kadagingin)",
        "createacct-email-ph": "Dagingin alamat email jero",
        "mergehistory-from": "Kaca wit:",
        "revertmerge": "tansida nyarengin",
        "history-title": "Babad uahan saking \"$1\"",
+       "difference-title": "$1: sane malianan ring revisi",
        "lineno": "Carik $1:",
        "compareselectedversions": "bandingang penguwahan sane kapilih",
        "editundo": "nguliang",
        "diff-empty": "(Nénten wénten sané malianan)",
+       "diff-multi-sameuser": "({{PLURAL:$1|$1 revisi pantaraning}} olih pangawi sane pateh nenten kacumawisang)",
        "searchresults": "asil pangrereh",
        "searchresults-title": "asil pangrereh anggen \"$1\"",
        "prevn": "{{PLURAL:$1|$1}} sadurungnyané",
        "recentchanges-label-minor": "Punika uahan alit",
        "recentchanges-label-bot": "Uahan puniki kalaksanayang antuk bot",
        "recentchanges-label-unpatrolled": "Uahan puniki durung kapatroli",
+       "recentchanges-label-plusminus": "Pagentos akeh kaca manut ring bita",
        "recentchanges-legend-heading": "<strong>Legenda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (taler cingak [[Special:NewPages|bacakan kaca anyar]])",
        "recentchanges-submit": "Sinahang",
        "actionfailed": "pelaksana luput",
        "dellogpage": "log pangapus",
        "rollbacklink": "mabalik",
+       "rollbacklinkcount": "balikang $1 {{PLURAL:$1|suratan}}",
        "changecontentmodel-title-label": "Murda kaca",
        "protectlogpage": "Log saiban",
        "protectedarticle": "nyaib \"[[$1]]\"",
index 47645a3..21c661f 100644 (file)
        "enotif_lastvisited": "Дзеля ўсіх зьменаў з вашага апошняга наведваньня, глядзіце $1",
        "enotif_lastdiff": "Глядзіце $1, каб пабачыць гэтую зьмену.",
        "enotif_anon_editor": "ананімны ўдзельнік $1",
-       "enotif_body": "Вітаем, $WATCHINGUSERNAME.\n\n$PAGEINTRO $NEWPAGE\n\nАпісаньне зьменаў: $PAGESUMMARY $PAGEMINOREDIT\n\nЗьвязацца з рэдактарам:\nпраз электронную пошту: $PAGEEDITOR_EMAIL\nпразь вікі-старонку: $PAGEEDITOR_WIKI\n\nПаведамленьні ня будуць дасылацца ў выпадку новых дзеяньняў, пакуль Вы не наведаеце гэтую старонку па ўваходзе ў сыстэму. Вы таксама можаце адключыць паведамленьні пра зьмены для ўсіх старонак з Вашага сьпісу назіраньня.\n\n             Сыстэма паведамленьняў {{GRAMMAR:родны|{{SITENAME}}}}\n\n--\nКаб зьмяніць налады абвяшчэньня праз электронную пошту, наведайце:\n{{canonicalurl:{{#special:Preferences}}}}\n\nКаб зьмяніць налады сьпісу назіраньня, наведайце:\n{{canonicalurl:{{#special:Preferences}}}}\n\nКаб выдаліць старонку з Вашага сьпісу назіраньня, наведайце:\n$UNWATCHURL\n\nЗваротная сувязь і дапамога:\n$HELPPAGE",
+       "enotif_body": "Вітаем, $WATCHINGUSERNAME.\n\n$PAGEINTRO $NEWPAGE\n\nАпісаньне зьменаў: $PAGESUMMARY $PAGEMINOREDIT\n\nЗьвязацца з рэдактарам:\nпраз электронную пошту: $PAGEEDITOR_EMAIL\nпразь вікістаронку: $PAGEEDITOR_WIKI\n\nПаведамленьні ня будуць дасылацца ў выпадку новых дзеяньняў, пакуль вы не наведаеце гэтую старонку па ўваходзе ў сыстэму. Вы таксама можаце адключыць паведамленьні пра зьмены для ўсіх старонак з вашага сьпісу назіраньня.\n\n             Сыстэма паведамленьняў {{GRAMMAR:родны|{{SITENAME}}}}\n\n--\nКаб зьмяніць налады абвяшчэньня праз электронную пошту, наведайце:\n{{canonicalurl:{{#special:Preferences}}}}\n\nКаб зьмяніць налады сьпісу назіраньня, наведайце:\n{{canonicalurl:{{#special:Preferences}}}}\n\nКаб выдаліць старонку з вашага сьпісу назіраньня, наведайце:\n$UNWATCHURL\n\nЗваротная сувязь і дапамога:\n$HELPPAGE",
        "enotif_minoredit": "Гэта дробная праўка",
        "created": "створаная",
        "changed": "зьмененая",
        "delete-legend": "Выдаліць",
        "historywarning": "<strong>Папярэджаньне</strong>: старонка, якую Вы зьбіраецеся выдаліць, мае гісторыю з $1 {{PLURAL:$1|вэрсіі|вэрсіяў|вэрсіяў}}:",
        "historyaction-submit": "Паказаць вэрсіі",
-       "confirmdeletetext": "Ð\97аÑ\80аз Ð\92Ñ\8b Ð²Ñ\8bдалÑ\96Ñ\86е Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам Ð· Ñ\83Ñ\81Ñ\91й Ð³Ñ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй Ð·Ñ\8cменаÑ\9e.\nÐ\9aалÑ\96 Ð»Ð°Ñ\81ка, Ð¿Ð°Ñ\86Ñ\8cвеÑ\80дзÑ\96Ñ\86е, Ñ\88Ñ\82о Ð\92Ñ\8b Ð·Ñ\8cбÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f Ð³Ñ\8dÑ\82а Ð·Ñ\80абÑ\96Ñ\86Ñ\8c Ñ\96 Ñ\88Ñ\82о Ð\92ы разумееце ўсе наступствы, а таксама робіце гэта ў адпаведнасьці з [[{{MediaWiki:Policy-url}}|правіламі]].",
+       "confirmdeletetext": "Ð\97аÑ\80аз Ð²Ñ\8b Ð²Ñ\8bдалÑ\96Ñ\86е Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам Ð· Ñ\83Ñ\81Ñ\91й Ð³Ñ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй Ð·Ñ\8cменаÑ\9e.\nÐ\9aалÑ\96 Ð»Ð°Ñ\81ка, Ð¿Ð°Ñ\86Ñ\8cвеÑ\80дзÑ\96Ñ\86е, Ñ\88Ñ\82о Ð²Ñ\8b Ð·Ñ\8cбÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f Ð³Ñ\8dÑ\82а Ð·Ñ\80абÑ\96Ñ\86Ñ\8c Ñ\96 Ñ\88Ñ\82о Ð²ы разумееце ўсе наступствы, а таксама робіце гэта ў адпаведнасьці з [[{{MediaWiki:Policy-url}}|правіламі]].",
        "actioncomplete": "Дзеяньне выкананае",
        "actionfailed": "Дзеяньне ня выкананае",
        "deletedtext": "«$1» была выдаленая.\nЗапісы пра выдаленыя старонкі зьмяшчаюцца ў $2.",
        "dellogpage": "Журнал выдаленьняў",
-       "dellogpagetext": "Сьпіс апошніх выдаленьняў.",
+       "dellogpagetext": "Ð\9dÑ\96жÑ\8dй Ð·Ð½Ð°Ñ\85одзÑ\96Ñ\86Ñ\86а Ñ\81ьпіс апошніх выдаленьняў.",
        "deletionlog": "журнал выдаленьняў",
        "log-name-create": "Журнал стварэньня старонак",
        "log-description-create": "Ніжэй знаходзіцца сьпіс апошніх стварэньняў старонак.",
index f1bbf9b..832c1ca 100644 (file)
        "history": "История",
        "history_short": "История",
        "history_small": "история",
-       "updatedmarker": "пÑ\80оменено Ð¾Ñ\82 Ð¿Ð¾Ñ\81ледноÑ\82о Ð¼и посещение",
+       "updatedmarker": "пÑ\80оменено Ð¾Ñ\82 Ð¿Ð¾Ñ\81ледноÑ\82о Ð\92и посещение",
        "printableversion": "Версия за печат",
        "permalink": "Постоянна препратка",
        "print": "Печат",
        "deleteprotected": "Не можете да изтриете страницата, защото е защитена.",
        "deleting-backlinks-warning": "<strong>Внимание:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|Други страници]] сочат към или включват като шаблон страницата, която се опитвате да изтриете.",
        "rollback": "Отмяна на промените",
+       "rollback-confirmation-confirm": "Моля потвърдете:",
+       "rollback-confirmation-yes": "Отмяна",
        "rollback-confirmation-no": "Отказ",
        "rollbacklink": "отмяна",
        "rollbacklinkcount": "отмяна на $1 {{PLURAL:$1|редакция|редакции}}",
        "blocklist-userblocks": "Скриване блокирането на потребителски сметки",
        "blocklist-tempblocks": "Скриване на временни блокирания",
        "blocklist-addressblocks": "Скриване на отделни блокирания на IP адреси",
+       "blocklist-type-opt-sitewide": "За всички уикита",
+       "blocklist-type-opt-partial": "Частично",
        "blocklist-rangeblocks": "Скриване на блокиранията по IP диапазон",
        "blocklist-timestamp": "Дата и час",
        "blocklist-target": "Цел",
        "watchlistedit-clear-titles": "Заглавия:",
        "watchlistedit-clear-submit": "Изчистване на списъка за наблюдение (Необратимо!)",
        "watchlistedit-clear-done": "Списъкът за наблюдение беше изчистен.",
+       "watchlistedit-clear-jobqueue": "Вашият списък за наблюдение се изчиства. Това може да отнеме известно време!",
        "watchlistedit-clear-removed": "{{PLURAL:$1|1 заглавие беше премахнато|$1 заглавия бяха премахнати}}:",
        "watchlistedit-too-many": "Има твърде много страници за показване.",
        "watchlisttools-clear": "Изчистване на списъка за наблюдение",
        "redirect-file": "Име на файл",
        "redirect-logid": "Номер на записа",
        "redirect-not-exists": "Стойността не е намерена",
+       "redirect-not-numeric": "Стойността не е числова",
        "fileduplicatesearch": "Търсене на повтарящи се файлове",
        "fileduplicatesearch-summary": "Търсене на повтарящи се файлове на база хеш стойности.",
        "fileduplicatesearch-filename": "Име на файл:",
        "tag-mw-contentmodelchange": "промяна на модела на съдържание",
        "tag-mw-new-redirect": "Ново пренасочване",
        "tag-mw-removed-redirect": "Премахнато пренасочване",
+       "tag-mw-changed-redirect-target": "Промяна целта на пренасочване",
+       "tag-mw-changed-redirect-target-description": "Редакции, променящи целта на пренасочване",
        "tag-mw-blank": "Изтриване на съдържанието",
        "tag-mw-replace": "Заменено",
+       "tag-mw-replace-description": "Редакции, премахващи над 90% от съдържанието на страница",
        "tag-mw-rollback": "Отмяна",
        "tag-mw-undo": "Отмяна",
        "tags-title": "Етикети",
index b87811a..88e5bc1 100644 (file)
        "mycontris": "Сан къинхьегам",
        "anoncontribs": "Къинхьегам",
        "contribsub2": "Къинхьегам $1 ($2)",
+       "contributions-subtitle": "{{GENDER:$3|$1}} кхуьна",
        "contributions-userdoesnotexist": "«$1» иштта декъашхочун дӀаяздар дац.",
        "nocontribs": "Дехарца хийцамаш цакарий.",
        "uctop": "карара",
index 72b1763..b721395 100644 (file)
        "powersearch-togglelabel": "تاوتوێ بکە:",
        "powersearch-toggleall": "ھەموویان",
        "powersearch-togglenone": "ھیچیان",
-       "powersearch-remember": "ھەڵبژاردەکانت بۆ گەڕانەکانی تر لە بیر بێت",
+       "powersearch-remember": "بەبیرھێناوەی ھەڵبژاردەکان بۆ گەڕانەکانی داھاتوو",
        "search-external": "گەڕانی دەرەکی",
        "searchdisabled": "گەڕانی {{SITENAME}} ئێستە کار ناکات.\nدەتوانی بۆ ئێستا لە گەڕانی گووگڵ کەڵک وەرگری.\nلەیادت بێت لەوانەیە پێرستەکانیان بۆ گەڕانی ناو {{SITENAME}}، کات‌بەسەرچوو بێت.",
        "preferences": "ھەڵبژاردەکان",
index 8c1b440..d5beb37 100644 (file)
@@ -99,7 +99,9 @@
        "navigation": "Navigazione",
        "and": "&#32;è",
        "actions": "Azzione",
+       "namespaces": "Spazii",
        "variants": "Variante",
+       "navigation-heading": "Navigazione",
        "errorpagetitle": "Errore",
        "returnto": "Vultà à $1.",
        "tagline": "À prupositu di {{SITENAME}}",
        "specialpage": "Pagina speciale",
        "personaltools": "Strumenti persunali",
        "talk": "Discussione",
+       "views": "Viste sfarenti",
        "toolbox": "Stuvigli",
        "mediawikipage": "Vede i missaghji",
        "templatepage": "Vede a pagina di mudellu",
        "categorypage": "Vede a pagina di categuria",
        "viewtalkpage": "Vede a discussione",
        "otherlanguages": "In altre lingue",
+       "redirectedfrom": "(Reindirizzamentu da $1)",
        "redirectpagesub": "Pagina di reindirizzamentu",
        "redirectto": "Reindirizzamentu à:",
-       "lastmodifiedat": "Ultima mudifica di sta pagina u $1 à e $2.",
+       "lastmodifiedat": "Ùltima mudìfica di sta pàgina u $1 à e $2.",
        "protectedpage": "Pagina prutetta",
        "jumpto": "Andà à:",
        "jumptonavigation": "navigazione",
        "loginlanguagelabel": "Lingua: $1",
        "pt-login": "Cunnessione",
        "pt-login-button": "Cunnessione",
+       "pt-createaccount": "Registramentu",
        "pt-userlogout": "Scunnessione",
        "retypenew": "Scrive torna a nova parulla secreta:",
        "resetpass-submit-cancel": "Cancillà",
        "currentrev": "Ultima revisione",
        "currentrev-asof": "Versione attuale di e $1",
        "revisionasof": "Versione di e $1",
-       "revision-info": "Versione di e $4 à e $5 di $2",
+       "revision-info": "Versione di e $4 à e $5 da {{GENDER:$6|$2}}$7",
        "previousrevision": "← Versione menu ricente",
        "nextrevision": "Versione più nova →",
        "currentrevisionlink": "Ultima revisione",
        "searchprofile-everything": "Tuttu",
        "searchprofile-advanced": "Avanzatu",
        "searchprofile-articles-tooltip": "Circà in $1",
+       "searchprofile-images-tooltip": "Circà schedarii",
        "searchprofile-everything-tooltip": "Circà dapertuttu (incluse e pagine di discussione)",
        "search-result-size": "$1 ({{PLURAL:$2|1 parolla|$2 parolle}})",
        "search-redirect": "(Reindirizzamentu da $1)",
        "hide": "piattà",
        "show": "mustrà",
        "minoreditletter": "m",
+       "newpageletter": "N",
        "boteditletter": "b",
        "rc-enhanced-hide": "Nasconde i dittagli",
        "recentchangeslinked": "Mudifiche assuciate",
        "file-anchor-link": "Schedariu",
        "filehist": "Cronolugia di l'imagine",
        "filehist-deleteone": "supprimà",
+       "filehist-current": "attuale",
+       "filehist-datetime": "Data/Óra",
+       "filehist-thumb": "Previsualizzazione",
+       "filehist-thumbtext": "Previsualizzazione di a versione di",
        "filehist-user": "Cuntributore",
        "filehist-dimensions": "Dimensione",
        "filehist-comment": "Cummentu",
        "watchlistfor2": "Per $1 ($2)",
        "watch": "Suvità",
        "unwatch": "Ùn suvità micca",
-       "wlshowlast": "Mustrà l'ultime $1 ore $2 ghjorni",
+       "wlshowlast": "Mustrà l'ùltime $1 ore $2 ghjorni",
        "enotif_reset": "Marcà tutte e pagine visitate",
        "created": "creatu",
        "changed": "cambiatu",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|revisione|revisione}}",
        "tooltip-pt-userpage": "{{GENDER:|A to}} pàgina di cuntributore",
        "tooltip-pt-mytalk": "{{GENDER:|A to}} pàgina di discussione",
-       "tooltip-pt-preferences": "{{GENDER:|E to}}} preferenze",
+       "tooltip-pt-preferences": "{{GENDER:|E to}} preferenze",
        "tooltip-pt-watchlist": "Lista di e pagine ch'è tù suviti",
        "tooltip-pt-mycontris": "Lista di {{GENDER:|e to}} cuntribuzioni",
        "tooltip-pt-login": "U registramentu hè suggeritu, micca ubligatoriu",
        "tooltip-pt-logout": "Esce da a sessione",
        "tooltip-ca-talk": "Vede e discussione relative à sta pagina",
-       "tooltip-ca-edit": "Pò mudificà 'ssa pagina. Per piacè improda l'ozzione di previsualisazzione prima di salvà",
+       "tooltip-ca-edit": "Mudificà 'ssa pagina",
        "tooltip-ca-addsection": "Cumincià una nova sezzione",
        "tooltip-ca-viewsource": "Sta pagina hè prutetta, ma si pò vede u so codice surghjente",
        "tooltip-ca-history": "Versione precedente di sta pagina",
        "tooltip-t-whatlinkshere": "Listinu di tutte e pagine chì sò ligate à quessa",
        "tooltip-t-recentchangeslinked": "Versione di l'ultime mudifiche à e pagine legate à quessa",
        "tooltip-t-contributions": "Listinu di e mudifiche {{GENDER:$1|di 'ssu cuntributore}}",
+       "tooltip-t-upload": "Incaricà un schedariu",
        "tooltip-t-specialpages": "Listinu di tutte e pagine spiciale",
        "tooltip-t-print": "Versione stampevule di 'ssa pagina",
        "tooltip-t-permalink": "Ligame permanente à e revisione di sta pagina",
        "tooltip-ca-nstab-user": "Vede a pagina di cuntributore",
        "tooltip-ca-nstab-special": "Questa hè una pàgina particulare chi ùn si pó micca esse mudificata",
        "tooltip-ca-nstab-project": "Vede a pagina di u prugettu",
+       "tooltip-ca-nstab-image": "Vede pàgina di schedariu",
        "tooltip-ca-nstab-template": "Vede u mudellu",
        "tooltip-ca-nstab-category": "Vede a pagina di categuria",
        "tooltip-minoredit": "Signalà com'è mudifica minore",
        "noimages": "Nulla da vede.",
        "ilsubmit": "Ricerca",
        "bydate": "per data",
+       "namespacesall": "tutti",
        "monthsall": "tutti",
        "confirm_purge_button": "D'accordu",
        "table_pager_next": "Pagina seguente",
        "logentry-move-move": "$1 {{GENDER:$2|hà spustatu}} a pagina $3 à $4",
        "logentry-newusers-create": "U participante $3 hè statu creatu da $1",
        "rightsnone": "(nessunu)",
-       "searchsuggest-search": "Ricerca",
+       "searchsuggest-search": "Circà in {{SITENAME}}",
        "expand_templates_output": "Risultatu"
 }
index e7ae281..0d88510 100644 (file)
@@ -74,7 +74,8 @@
                        "Weblars",
                        "Kranix",
                        "Psl85",
-                       "Dipsacus fullonum"
+                       "Dipsacus fullonum",
+                       "Fugithora"
                ]
        },
        "tog-underline": "Understreg link:",
        "history": "Sidehistorik",
        "history_short": "Historik",
        "history_small": "historik",
-       "updatedmarker": "opdateret siden seneste besøg",
+       "updatedmarker": "opdateret siden dit seneste besøg",
        "printableversion": "Udskriftsvenlig udgave",
        "permalink": "Permanent link",
        "print": "Udskriv",
        "virus-scanfailed": "scan fejlede (fejlkode $1)",
        "virus-unknownscanner": "ukendt antivirus:",
        "logouttext": "<strong>Du er nu logget af.</strong>\n\nBemærk, at nogle sider stadigvæk kan vises som om du var logget på, indtil du tømmer din browsers cache.",
+       "logging-out-notify": "Du bliver logget ud, vent venligst.",
+       "logout-failed": "Kan ikke logge ud nu: $1",
        "cannotlogoutnow-title": "Kan ikke logge af på nuværende tidspunkt",
        "cannotlogoutnow-text": "Det er ikke muligt at logge af når du bruger $1.",
        "welcomeuser": "Velkommen, $1!",
index ee787a3..1c510f3 100644 (file)
        "autoblockedtext": "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem anderen Benutzer genutzt wurde, der von $1 gesperrt wurde.\nAls Grund wurde angegeben:\n\n:''$2''\n\n* Beginn der Sperre: $8\n* Ende der Sperre: $6\n* Sperre betrifft: $7\n\nDu kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.\n\nDu kannst die „{{int:emailuser}}“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.\n\nDeine aktuelle IP-Adresse ist $3, und die Sperr-ID ist $5.\nBitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
        "systemblockedtext": "Dein Benutzername oder deine IP-Adresse wurde von MediaWiki automatisch gesperrt.\nDer angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der Sperre: $6\n* Sperre betrifft: $7\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
        "blockednoreason": "keine Begründung angegeben",
+       "blockedtext-composite": "<strong>Dein Benutzername oder deine IP-Adresse wurde gesperrt.</strong>\n\nDer Angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der längsten Sperre: $6\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
+       "blockedtext-composite-reason": "Es gibt mehrere Sperren gegen dein Benutzerkonto und/oder deine IP-Adresse",
        "whitelistedittext": "Du musst dich $1, um Seiten bearbeiten zu können.",
        "confirmedittext": "Du musst deine E-Mail-Adresse erst bestätigen, bevor du Bearbeitungen durchführen kannst. Bitte ergänze und bestätige deine E-Mail in den [[Special:Preferences|Einstellungen]].",
        "nosuchsectiontitle": "Abschnitt nicht gefunden",
        "mw-widgets-abandonedit-title": "Bist du sicher?",
        "mw-widgets-copytextlayout-copy": "Kopieren",
        "mw-widgets-copytextlayout-copy-fail": "Der Text konnte nicht in die Zwischenablage kopiert werden.",
+       "mw-widgets-copytextlayout-copy-success": "Text in die Zwischenablage kopiert.",
        "mw-widgets-dateinput-no-date": "Kein Datum ausgewählt",
        "mw-widgets-dateinput-placeholder-day": "JJJJ-MM-TT",
        "mw-widgets-dateinput-placeholder-month": "JJJJ-MM",
        "restrictionsfield-help": "Eine IP-Adresse oder ein CIDR-Bereich pro Zeile. Um alles zu aktivieren, verwende:\n<pre>\n0.0.0.0/0\n::/0\n</pre>",
        "edit-error-short": "Fehler: $1",
        "edit-error-long": "Fehler:\n\n$1",
+       "specialmute": "Stumm",
+       "specialmute-success": "Deine Stummschaltungseinstellungen wurden aktualisiert. Schau dir alle stummgeschalteten Benutzer in [[Special:Preferences|deinen Einstellungen]] an.",
+       "specialmute-submit": "Bestätigen",
+       "specialmute-label-mute-email": "E-Mails von diesem Benutzer stummschalten",
+       "specialmute-header": "Bitte wähle deine Stummschaltungseinstellungen für {{BIDI:[[User:$1]]}}.",
+       "specialmute-error-invalid-user": "Der gesuchte Benutzername konnte nicht gefunden werden.",
+       "specialmute-error-email-blacklist-disabled": "Das Stummschalten von E-Mails von Benutzern ist nicht aktiviert.",
+       "specialmute-error-email-preferences": "Du musst deine E-Mail Adresse bestätigen bevor du einen Benutzer bestätigen kannst. Du kannst dies [[Special:Preferences|in deinen Einstellungen]] tun.",
+       "specialmute-email-footer": "Um deine E-Mail Einstellungen für {{BIDI:$2}} zu verwalten besuche bitte $1.",
+       "specialmute-login-required": "Bitte melde dich an um deine Stummschaltungseinstellungen zu ändern.",
        "revid": "Version $1",
        "pageid": "Seitenkennung $1",
        "interfaceadmin-info": "$1\n\nBerechtigungen für das Bearbeiten von wikiweiten CSS/JS/JSON-Dateien wurden kürzlich vom Recht <code>editinterface</code> getrennt. Falls du nicht verstehst, warum du diesen Fehler erhältst, siehe [[mw:MediaWiki_1.32/interface-admin]].",
index ccd9218..88bf510 100644 (file)
@@ -21,7 +21,8 @@
                        "Macofe",
                        "Matma Rex",
                        "Fitoschido",
-                       "Vlad5250"
+                       "Vlad5250",
+                       "J budissin"
                ]
        },
        "tog-underline": "Wótkaze pódšmarnuś:",
        "nstab-template": "Pśedłoga",
        "nstab-help": "Pomoc",
        "nstab-category": "Kategorija",
+       "mainpage-nstab": "Głowny bok",
        "nosuchaction": "Toś tu akciju njedajo",
        "nosuchactiontext": "Akcija, kótaruž URL pódawa, jo njepłaśiwa.\nSy se snaź zapisał pśi zapódaśu URL abo sy slědował wopacnemu wótkazoju.\nTo by mógło teke programěrowańska zmólka w {{GRAMMAR:lokatiw|{{SITENAME}}}} byś.",
        "nosuchspecialpage": "Toś ten specialny bok njeeksistěrujo",
index 232e0f0..55773f5 100644 (file)
                        "Cuatro Remos",
                        "Ryo567",
                        "Agusbou2015",
-                       "Waldyrious"
+                       "Waldyrious",
+                       "Johny Weissmuller Jr"
                ]
        },
        "tog-underline": "Enlaces que se van a subrayar:",
        "autoblockedtext": "Tu dirección IP ha sido bloqueada automáticamente porque fue utilizada por otro usuario, que resultó bloqueado por $1.\nEl motivo dado es el siguiente:\n\n:<em>$2</em>\n\n* Inicio del bloqueo: $8\n* Caducidad del bloqueo: $6\n* Bloqueo destinado a: $7\n\nPuedes contactar con $1 o con otro de los [[{{MediaWiki:Grouppage-sysop}}|administradores]] para discutir el bloqueo.\n\nObserva que no puedes utilizar la función «{{int:emailuser}}» a menos que hayas registrado una dirección de correo electrónico válida en tus [[Special:Preferences|preferencias de usuario]] y la función no haya sido también bloqueada.\n\nTu dirección IP actual es $3, y el identificador del bloqueo es n.º $5.\nIncluye todos los datos aquí mostrados en cualquier consulta que hagas.",
        "systemblockedtext": "Tu nombre de usuario o dirección IP ha sido bloqueado automáticamente por el software MediaWiki.\nLa razón dada es:\n\n:<em>$2</em>\n\n* Inicio del bloqueo: $8\n* Caducidad de bloqueo: $6\n* Destinatario del bloqueo: $7\n\nTu dirección IP actual es $3.\nPor favor, incluye todos los datos aquí mostrados en cualquier consulta que hagas.",
        "blockednoreason": "no se ha especificado el motivo",
+       "blockedtext-composite": "<strong>Tu nombre de usuario o dirección IP han sido bloqueados.</strong>\n\nLa razón es:\n\n:<em>$2</em>.\n\n* Inicio del bloqueo: $8\n* Vencimiento del bloqueo más largo: $6\n\nTu dirección IP actual es $3.\nPor favor, incluye todos los detalles anteriores en cualquier consulta que realices.",
+       "blockedtext-composite-reason": "Hay múltiples bloques contra tu cuenta y/o dirección IP.",
        "whitelistedittext": "Tienes que $1 para editar páginas.",
        "confirmedittext": "Debes confirmar tu dirección de correo electrónico antes de poder editar páginas. Por favor, configura y confirma tu dirección de correo a través de tus [[Special:Preferences|preferencias de usuario]].",
        "nosuchsectiontitle": "Sección no encontrada",
        "edit-error-short": "Error: $1",
        "edit-error-long": "Errores:\n\n$1",
        "specialmute": "Silenciar",
+       "specialmute-success": "\nTus preferencias de silencio han sido actualizadas. Mira todos los usuarios silenciados en [[Especial: Preferencias|tus preferencias]].",
        "specialmute-submit": "Confirmar",
        "specialmute-label-mute-email": "Silenciar los correos electrónicos de este usuario",
        "specialmute-error-invalid-user": "No se encontró el nombre de usuario solicitado.",
index 044703b..44e3c94 100644 (file)
        "exif-photometricinterpretation-2": "RGB",
        "exif-photometricinterpretation-3": "Палета",
        "exif-photometricinterpretation-4": "Маска транспарентности",
-       "exif-photometricinterpretation-5": "Ð\9eдвојено (вероватно CMYK)",
+       "exif-photometricinterpretation-5": "Раздвојено (вероватно CMYK)",
        "exif-photometricinterpretation-6": "YCbCr",
        "exif-photometricinterpretation-8": "CIE L*a*b*",
        "exif-photometricinterpretation-9": "CIE L*a*b* (ICC кодирање)",
index 491c771..5b522e2 100644 (file)
        "currentevents": "אקטואליה",
        "currentevents-url": "Project:אקטואליה",
        "disclaimers": "הבהרות משפטיות",
-       "disclaimerpage": "Project:×\94×\91×\94ר×\94 ×\9eשפ×\98×\99ת",
+       "disclaimerpage": "Project:×\94×\91×\94ר×\95ת ×\9eשפ×\98×\99×\95ת",
        "edithelp": "עזרה בעריכה",
        "helppage-top-gethelp": "עזרה",
        "mainpage": "עמוד ראשי",
        "pt-createaccount": "יצירת חשבון",
        "pt-userlogout": "יציאה מהחשבון",
        "php-mail-error-unknown": "שגיאה לא ידועה בפונקציה mail()‎ של PHP.",
-       "user-mail-no-addy": "×\94ת×\91צע × ×\99ס×\99×\95×\9f ×\9cש×\9c×\99×\97ת ×\94×\95×\93×¢×\94 ×\9c×\9c×\90 ×\9bת×\95×\91ת ×\93×\95×\90×´ל.",
-       "user-mail-no-body": "× ×\99ס×\99×\95×\9f ×\9cש×\9c×\95×\97 דוא\"ל עם תוכן ריק או קצר מאוד.",
+       "user-mail-no-addy": "×\94ת×\91צע × ×\99ס×\99×\95×\9f ×\9cש×\9c×\99×\97ת ×\94×\95×\93עת ×\93×\95×\90\"×\9c ×\9c×\9c×\90 ×\9bת×\95×\91ת ×\93×\95×\90\"ל.",
+       "user-mail-no-body": "×\94ת×\91צע × ×\99ס×\99×\95×\9f ×\9cש×\9c×\99×\97ת ×\94×\95×\93עת דוא\"ל עם תוכן ריק או קצר מאוד.",
        "changepassword": "שינוי סיסמה",
        "resetpass_announce": "כדי לסיים את הכניסה לחשבון, יש להגדיר סיסמה חדשה.",
        "resetpass_text": "<!-- יש להוסיף טקסט כאן -->",
        "autoblockedtext": "כתובת ה־IP שלך נחסמה באופן אוטומטי כיוון שמשתמש אחר, שנחסם על־ידי $1, השתמש בה.\nהסיבה שניתנה לחסימה היא:\n\n:<em>$2</em>\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nבאפשרותך ליצור קשר עם $1 או עם כל אחד מ[[{{MediaWiki:Grouppage-sysop}}|מפעילי המערכת]] האחרים כדי לדון בחסימה.\n\nכמו־כן, באפשרותך להשתמש בתכונת \"{{int:emailuser}}\", אלא אם לא ציינת כתובת דוא\"ל תקפה ב[[Special:Preferences|העדפות המשתמש שלך]] או אם נחסמת משליחת דוא\"ל.\n\nכתובת ה־IP הנוכחית שלך היא $3, ומספר החסימה שלך הוא #$5.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
        "systemblockedtext": "שם המשתמש או כתובת ה־IP שלך נחסמו באופן אוטומטי על־ידי תוכנת מדיה־ויקי.\nהסיבה שניתנה לחסימה היא:\n\n:<em>$2</em>\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nכתובת ה־IP הנוכחית שלך היא $3.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
        "blockednoreason": "לא ניתנה סיבה",
-       "blockedtext-composite": "<strong>ש×\9d ×\94×\9eשת×\9eש ×\90×\95 ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9b×\9d × ×\97ס×\9e×\95 ×\9eער×\99×\9b×\94.</strong>\n\n×\94ס×\99×\91×\94 ×©× ×\99תנ×\94 ×\94×\99×\90:\n\n:<em>$2</em>.\n\n* ×ª×\97×\99×\9cת ×\94×\97ס×\99×\9e×\94: $8\n* ×¤×§×\99עת ×\94×\97ס×\99×\9e×\94 ×\94×\90ר×\95×\9b×\94 ×\91×\99×\95תר: $6\n\n×\9bת×\95×\91ת ×\94Ö¾IP ×\94× ×\95×\9b×\97×\99ת ×©×\9c×\9a ×\94×\99×\90 $3.\n×\99ש ×\9cספק ×\90ת ×\9b×\9c ×\94×\9e×\99×\93×¢ ×\94× \"×\9c ×¢×\91×\95ר ×\9b×\9c ×\94ש×\90×\99×\9cת×\95ת ×©×\90ת×\9d ×\9e×\91צע×\99×\9d.",
-       "blockedtext-composite-reason": "×\99שנ×\9f ×\9eספר ×\97ס×\99×\9e×\95ת ×¢×\9c ×\94×\97ש×\91×\95×\9f ×©×\9c×\9a ×\95\90×\95 ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a",
+       "blockedtext-composite": "<strong>ש×\9d ×\94×\9eשת×\9eש ×\90×\95 ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a × ×\97ס×\9e×\95.</strong>\n\n×\94ס×\99×\91×\94 ×©× ×\99תנ×\94 ×\9c×\9b×\9a ×\94×\99×\90:\n\n:<em>$2</em>.\n\n* ×ª×\97×\99×\9cת ×\94×\97ס×\99×\9e×\94: $8\n* ×¤×§×\99עת ×\94×\97ס×\99×\9e×\94 ×\94×\90ר×\95×\9b×\94 ×\91×\99×\95תר: $6\n\n×\9bת×\95×\91ת ×\94Ö¾IP ×\94× ×\95×\9b×\97×\99ת ×©×\9c×\9a ×\94×\99×\90 $3.\n×\99ש ×\9cצ×\99×\99×\9f ×\90ת ×\9b×\9c ×\94פר×\98×\99×\9d ×\94×\9c×\9c×\95 ×\91×\9b×\9c ×¤× ×\99×\99×\94 ×\9c×\91×\99ר×\95ר ×\94×\97ס×\99×\9e×\94.",
+       "blockedtext-composite-reason": "×\94×\95פע×\9c×\95 ×\9eספר ×\97ס×\99×\9e×\95ת ×¢×\9c ×\97ש×\91×\95×\9f ×\94×\9eשת×\9eש ×©×\9c×\9a ×\90×\95 ×¢×\9c ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a (×\90×\95 ×¢×\9c ×©× ×\99×\94×\9d)",
        "whitelistedittext": "נדרשת $1 כדי לערוך דפים.",
        "confirmedittext": "יש לאמת את כתובת הדוא\"ל לפני עריכת דפים.\nנא להגדיר ולאמת את כתובת הדוא\"ל שלך באמצעות [[Special:Preferences|העדפות המשתמש]] שלך.",
        "nosuchsectiontitle": "הפסקה לא נמצאה",
        "gender-male": "הוא עורך דפים בוויקי",
        "gender-female": "היא עורכת דפים בוויקי",
        "prefs-help-gender": "לא חובה למלא העדפה זו.\nהמערכת משתמשת במידע הזה כדי לפנות אליך/אלייך ולציין את שם המשתמש שלך במין הדקדוקי הנכון.\nהמידע יהיה ציבורי.",
-       "email": "דוא״ל",
+       "email": "דוא\"ל",
        "prefs-help-realname": "לא חובה למלא את השם האמיתי.\nאם סופק, הוא עשוי לשמש כדי לייחס לך את עבודתך.",
        "prefs-help-email": "כתובת דואר אלקטרוני היא אופציונלית, אבל היא חיונית לאיפוס הסיסמה במקרה ש{{GENDER:|תשכח|תשכחי}} אותה.",
        "prefs-help-email-others": "באפשרותך גם לאפשר למשתמשים ליצור איתך קשר באמצעות דוא\"ל דרך קישור בדף המשתמש או בדף השיחה שלך.\nכתובת הדוא\"ל שלך לא תיחשף כשמשתמשים יצרו איתך קשר.",
        "mw-widgets-abandonedit-discard": "ביטול העריכות",
        "mw-widgets-abandonedit-keep": "המשך עריכה",
        "mw-widgets-abandonedit-title": "בטוח?",
-       "mw-widgets-copytextlayout-copy": "העתק",
+       "mw-widgets-copytextlayout-copy": "העתקה",
        "mw-widgets-copytextlayout-copy-fail": "ההעתקה ללוח נכשלה.",
        "mw-widgets-copytextlayout-copy-success": "הועתק ללוח.",
        "mw-widgets-dateinput-no-date": "לא נבחר תאריך",
        "edit-error-short": "שגיאה: $1",
        "edit-error-long": "שגיאות:\n\n$1",
        "specialmute": "השתקה",
-       "specialmute-success": "העדפות ההשתקה שלך עודכנו. ר' את כל המשתמשים המושתקים ב[[Special:Preferences|העדפות שלך]].",
+       "specialmute-success": "העדפות ההשתקה שלך עודכנו. רשימת כל המשתמשים המושתקים זמינה ב[[Special:Preferences|העדפות שלך]].",
        "specialmute-submit": "אישור",
-       "specialmute-label-mute-email": "×\9c×\94שת×\99ק דואר אלקטרוני מהמשתמש הזה",
-       "specialmute-header": "× ×\90 ×\9c×\91×\97×\95ר ×\90ת העדפות ההשתקה שלך עבור {{BIDI:[[User:$1]]}}.",
+       "specialmute-label-mute-email": "×\94שתקת ×\94×\95×\93×¢×\95ת דואר אלקטרוני מהמשתמש הזה",
+       "specialmute-header": "×\91×\97×\99ר×\95ת העדפות ההשתקה שלך עבור {{BIDI:[[User:$1]]}}.",
        "specialmute-error-invalid-user": "שם המשתמש המבוקש לא נמצא.",
-       "specialmute-error-email-blacklist-disabled": "השתקת משתמשים משליחת דואר אלקטרוני אליך אינה מופעלת.",
+       "specialmute-error-email-blacklist-disabled": "×\94×\90פשר×\95ת ×\9c×\94שתקת ×\9eשת×\9eש×\99×\9d ×\9eש×\9c×\99×\97ת ×\93×\95×\90ר ×\90×\9cק×\98ר×\95× ×\99 ×\90×\9c×\99×\9a ×\90×\99× ×\94 ×\9e×\95פע×\9cת.",
        "specialmute-error-email-preferences": "יש לאמת את כתובת הדואר האלקטרוני שלך לפני שתהיה לך אפשרות להשתיק משתמש. אפשר לעשות זאת מהדף [[Special:Preferences]].",
-       "specialmute-email-footer": "×\9b×\93×\99 ×\9c× ×\94×\9c ×\90ת ×\94×\94×¢×\93פ×\95ת ×¢×\91×\95ר {{BIDI:$2}} × ×\90 לבקר בדף <$1>.",
-       "specialmute-login-required": "× ×\90 ×\9c×\94×\99×\9bנס ×\9c×\97ש×\91×\95×\9f ×\9b×\93×\99 ×\9cש×\91ות את העדפות ההשתקה שלך.",
+       "specialmute-email-footer": "×\9b×\93×\99 ×\9c× ×\94×\9c ×\90ת ×\94×¢×\93פ×\95ת ×§×\91×\9cת ×\94×\93×\95×\90ר ×\94×\90×\9cק×\98ר×\95× ×\99 ×©× ×©×\9c×\97 ×¢×\9cÖ¾×\99×\93×\99 {{BIDI:$2}}, ×\91×\90פשר×\95ת×\9a לבקר בדף <$1>.",
+       "specialmute-login-required": "× ×\93רשת ×\9b× ×\99ס×\94 ×\9c×\97ש×\91×\95×\9f ×\9b×\93×\99 ×\9cשנות את העדפות ההשתקה שלך.",
        "revid": "גרסה $1",
        "pageid": "מזהה דף $1",
        "interfaceadmin-info": "$1\n\nההרשאות לעריכת קובצי CSS/JS/JSON של האתר כולו הופרדו לאחרונה מההרשאה <code>editinterface</code>. אם לא ברור לך מדוע קיבלת את הודעת השגיאה הזאת, ר' [[mw:MediaWiki_1.32/interface-admin]].",
index 3a4ed91..1bffe93 100644 (file)
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2|promijenio|promijenila}} je jezik stranice $3 iz $4 u $5.",
        "mediastatistics": "Statistika datoteka",
        "mediastatistics-summary": "Slijede statistike postavljenih datoteka koje pokazuju zadnju inačicu datoteke. Starije ili izbrisane inačice nisu prikazane.",
-       "mediastatistics-nfiles": "$1 ($2%)",
+       "mediastatistics-nfiles": "$1 ($2 %)",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3 %)",
        "mediastatistics-bytespertype": "Ukupna veličina datoteka za ovaj odlomak: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3%).",
        "mediastatistics-allbytes": "Ukupna veličina svih datoteka: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2).",
        "specialmute-success": "Vaše postavke utišavanja su uspješno ažurirane. Vidite sve utišane korisnike ovdje: [[Special:Preferences]].",
        "specialmute-submit": "Potvrdi",
        "specialmute-error-invalid-user": "Korisničko ime koje ste tražili nije moguće pronaći.",
-       "specialmute-error-email-preferences": "Morate potvrditi svoju email adresu prije nego što možete utišati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].",
-       "specialmute-login-required": "Molimo Vas prijavite se da biste promijenili postavke.",
+       "specialmute-error-email-preferences": "Morate potvrditi svoju adresu e-pošte prije nego što možete utišati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].",
+       "specialmute-login-required": "Molimo Vas, prijavite se da biste promijenili postavke.",
        "gotointerwiki": "Napuštate projekt {{SITENAME}}",
        "gotointerwiki-invalid": "Navedeni naslov nije valjan.",
        "gotointerwiki-external": "Napuštate projekt {{SITENAME}} da biste posjetili zasebno mrežno mjesto [[$2]].\n\n<strong>[$1 Nastavljate na $1]</strong>",
index 0ed8f56..1c23bb8 100644 (file)
        "ipusubmit": "Հանել արգելափակումը",
        "unblocked": "[[User:$1|$1]] մասնակիցը անարգելված է։",
        "unblocked-id": "$1 արգելափակումը հանված է",
-       "blocklist": "Արգելափակված մասնակիցներ։",
+       "blocklist": "Արգելափակված մասնակիցներ",
        "autoblocklist-submit": "Որոնել",
        "ipblocklist": "Արգելափակված IP-հասցեները և մասնակիցները",
        "ipblocklist-legend": "Արգելափակված մասնակցի որոնում",
index 27d9569..107c22f 100644 (file)
        "history": "Riwayat halaman",
        "history_short": "Versi terdahulu",
        "history_small": "riwayat",
-       "updatedmarker": "diubah sejak kunjungan terakhir saya",
+       "updatedmarker": "berubah sejak kunjungan terakhir saya",
        "printableversion": "Versi cetak",
        "permalink": "Pranala permanen",
        "print": "Cetak",
        "autoblockedtext": "Alamat IP Anda telah terblokir secara otomatis karena digunakan oleh pengguna lain, yang diblokir oleh $1. Pemblokiran dilakukan dengan alasan:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAnda dapat menghubungi $1 atau [[{{MediaWiki:Grouppage-sysop}}|pengurus]] lainnya untuk membicarakan pemblokiran ini.\n\nAnda tidak dapat menggunakan fitur \"{{int:emailuser}}\" kecuali Anda telah memasukkan alamat surel yang sah di [[Special:Preferences|preferensi akun]] Anda dan Anda tidak diblokir untuk menggunakannya.\n\nAlamat IP Anda saat ini adalah $3, dan ID pemblokiran adalah #$5.\nTolong sertakan informasi-informasi ini dalam setiap pertanyaan Anda.",
        "systemblockedtext": "Nama pengguna atau alamat IP Anda telah diblokir secara otomatis oleh MediaWiki.\nAlasan yang diberikan adalah:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAlamat IP Anda saat ini adalah $3\nMohon sertakan semua perincian di atas dalam setiap pertanyaan yang Anda ajukan.",
        "blockednoreason": "tidak ada alasan yang diberikan",
+       "blockedtext-composite-reason": "Ada pemblokiran berganda terhadap akun Anda dan/atau alamat IP Anda.",
        "whitelistedittext": "Anda harus $1 untuk dapat menyunting halaman.",
        "confirmedittext": "Anda harus mengkonfirmasikan dulu alamat surel Anda sebelum menyunting halaman.\nHarap masukkan dan validasikan alamat surel Anda melalui [[Special:Preferences|halaman preferensi pengguna]] Anda.",
        "nosuchsectiontitle": "Bagian tidak ditemukan",
        "mw-widgets-abandonedit-discard": "Buang suntingan",
        "mw-widgets-abandonedit-keep": "Lanjutkan penyuntingan",
        "mw-widgets-abandonedit-title": "Apakah Anda yakin?",
+       "mw-widgets-copytextlayout-copy": "Salin",
+       "mw-widgets-copytextlayout-copy-fail": "Gagal menyalin ke papan klip.",
+       "mw-widgets-copytextlayout-copy-success": "Salin ke papan klip.",
        "mw-widgets-dateinput-no-date": "Tanggal tidak ada yang terpilih",
        "mw-widgets-dateinput-placeholder-day": "TTTT-BB-HH",
        "mw-widgets-dateinput-placeholder-month": "TTTT-BB",
        "restrictionsfield-help": "Satu alamat IP atau rentang CIDR per baris. Untuk mengaktifkan semuanya, gunakan:\n<pre>0.0.0.0/0\n::/0</pre>",
        "edit-error-short": "Galat: $1",
        "edit-error-long": "Galat:\n\n$1",
+       "specialmute": "Diam",
+       "specialmute-submit": "Konfirmasi",
        "revid": "revisi $1",
        "pageid": "ID halaman $1",
        "rawhtml-notallowed": "Tag &lt;html&gt; tidak dapat digunakan di luar halaman normal.",
index 7879fce..7764a21 100644 (file)
        "logentry-partialblock-block-ns": "{{PLURAL:$1|名前空間}} $2",
        "logentry-partialblock-block": "$1 が {{GENDER:$4|$3}} に対して $7 からの編集を $5 {{GENDER:$2||ブロックしました}} $6",
        "logentry-partialblock-reblock": "$1 が {{GENDER:$4|$3}} に対する $7 のブロックの期限を $5 に{{GENDER:$2|変更しました}} $6",
+       "logentry-non-editing-block-block": "$1 が {{GENDER:$4|$3}} に対して編集以外の処理を $5 $6 で{{GENDER:$2||ブロックしました}}",
+       "logentry-non-editing-block-reblock": "$1 が {{GENDER:$4|$3}} に対する特定の編集以外の処理のブロックの期限を $5 $6 に{{GENDER:$2|変更しました}}",
        "logentry-suppress-block": "$1 が {{GENDER:$4|$3}} を$5で{{GENDER:$2|ブロックしました}} $6",
        "logentry-suppress-reblock": "$1 が {{GENDER:$4|$3}} のブロックの期限を$5に{{GENDER:$2|変更しました}} $6",
        "logentry-import-upload": "$1 がファイルをアップロードして $3 を{{GENDER:$2|インポートしました}}",
        "edit-error-short": "エラー: $1",
        "edit-error-long": "エラー:\n\n\n\n$1",
        "specialmute": "ミュート",
+       "specialmute-success": "ミュートの個人設定が更新されました。[[Special:Preferences|ご自分の個人設定ページ]]でミューとした利用者の一覧を確認できます。",
+       "specialmute-submit": "確定",
        "specialmute-label-mute-email": "この利用者からのウィキメールをミュートする",
+       "specialmute-header": "{{BIDI:[[User:$1]]}}さんに対するミュートを個人設定で選択してください。",
        "specialmute-error-invalid-user": "あなたが要求した利用者名は見つかりませんでした。",
+       "specialmute-error-email-blacklist-disabled": "利用者からメールを受け取らないようにするミュートは設定されていません。",
+       "specialmute-error-email-preferences": "発信者をミューとする準備として、ご自分のeメールアドレスの認証が必要です。手続きは[[Special:Preferences|個人設定]]のページで行います。",
+       "specialmute-email-footer": "{{BIDI:$2}}のeメール発信者の個人設定を変更するには<$1>を開いてください。",
+       "specialmute-login-required": "ミュートの個人設定を変更するにはログインしてください。",
        "revid": "版 $1",
        "pageid": "ページID $1",
        "interfaceadmin-info": "$1\n\nサイト全体のCSS/JavaScriptの編集権限は、最近<code>editinterface</code> 権限から分離されました。なぜこのエラーが表示されたのかわからない場合は、[[mw:MediaWiki_1.32/interface-admin]]をご覧ください。",
index da07dd6..3a4ba28 100644 (file)
@@ -76,7 +76,8 @@
                        "Delim",
                        "Comjun04",
                        "Son77391",
-                       "Jango"
+                       "Jango",
+                       "D6283"
                ]
        },
        "tog-underline": "링크에 밑줄 긋기:",
        "logentry-block-block": "$1님이 {{GENDER:$4|$3}}님을 $5 {{GENDER:$2|차단했습니다}} $6",
        "logentry-block-unblock": "$1님이 {{GENDER:$4|$3}}님의 {{GENDER:$2|차단을 해제했습니다}}",
        "logentry-block-reblock": "$1 님이 {{GENDER:$4|$3}} 님의 차단 기간을 $5(으)로 {{GENDER:$2|바꾸었습니다}} $6",
-       "logentry-partialblock-block": "$1님이 {{GENDER:$4|$3}}님을 $7 {{PLURAL:$8|문서를|문서들을}} 편집하지 못하도록 $5 {{GENDER:$2|차단}}했습니다. $6",
+       "logentry-partialblock-block": "$1님이 {{GENDER:$4|$3}}님을 $7 편집하지 못하도록 $5 {{GENDER:$2|차단}}했습니다. $6",
        "logentry-suppress-block": "$1님이 {{GENDER:$4|$3}} 사용자를 $5 {{GENDER:$2|차단했습니다}} $6",
        "logentry-suppress-reblock": "$1 님이 {{GENDER:$4|$3}} 님의 차단 기간을 $5(으)로 {{GENDER:$2|바꾸었습니다}} $6",
        "logentry-import-upload": "$1님이 $3 문서를 파일 올리기로 {{GENDER:$2|가져왔습니다}}",
        "edit-error-short": "오류: $1",
        "edit-error-long": "오류:\n\n$1",
        "specialmute": "알림 미표시",
-       "specialmute-success": "ì\95\8c림 ë¯¸í\91\9cì\8b\9c í\99\98ê²½ ì\84¤ì \95ì\9d´ ì\84±ê³µì \81ì\9c¼ë¡\9c ì\97\85ë\8d°ì\9d´í\8a¸ë\90\98ì\97\88ì\8aµë\8b\88ë\8b¤. [[Special:Preferences]]에서 알림이 표시되지 않는 모든 사용자를 확인하십시오.",
+       "specialmute-success": "ì\95\8c림 ë¯¸í\91\9cì\8b\9c í\99\98ê²½ ì\84¤ì \95ì\9d´ ì\97\85ë\8d°ì\9d´í\8a¸ë\90\98ì\97\88ì\8aµë\8b\88ë\8b¤. [[Special:Preferences|í\99\98ê²½ ì\84¤ì \95]]에서 알림이 표시되지 않는 모든 사용자를 확인하십시오.",
        "specialmute-submit": "확인",
        "specialmute-label-mute-email": "이 사용자의 이메일 알림을 표시하지 않습니다",
        "specialmute-header": "{{BIDI:[[User:$1]]}}의 알림 미표시 환경 설정을 선택해 주십시오.",
index de47550..c29f5d6 100644 (file)
        "versionrequired": "Dibutuahan MediaWiki versi $1",
        "versionrequiredtext": "MediaWiki versi $1 dibutuahan untuak manggunoan laman ko. Caliak [[Special:Version|versi laman]]",
        "ok": "OK",
-       "pagetitle": "$1 - {{SITENAME}} bahaso Minang",
+       "pagetitle": "$1 - {{SITENAME}} Minangkabau",
        "pagetitle-view-mainpage": "{{SITENAME}} bahaso Minang",
        "backlinksubtitle": "← $1",
        "retrievedfrom": "Didapek dari \"$1\"",
index 75726e2..0ac7b3d 100644 (file)
        "minoredit": "Chisto è nu cagnamiénto piccerillo",
        "watchthis": "Tiene d'uocchio sta paggena",
        "savearticle": "Sarva 'a paggena",
-       "savechanges": "Sarva 'e cagnamiénte",
+       "savechanges": "Sarva",
        "publishpage": "Pubbreca paggena",
        "publishchanges": "Pubbreca 'e cagnamiente",
        "savearticle-start": "Sarva 'a paggena...",
index dfd9e1e..a011006 100644 (file)
@@ -67,8 +67,8 @@
        "sun": "ߞߊ߯ߙߌߟߏ߲",
        "mon": "ߞߐ߬ߓߊ߬ߟߏ߲",
        "tue": "ߞߐ߬ߟߏ߲",
-       "wed": "ß\93ß\9fß\90ß\9fß\90",
-       "thu": "ß\9eß\8e߬ߣß\8e߲߬ߟߏ߲",
+       "wed": "ß\9eß\8e߬ߣß\8e߲߬ß\9fß\8fß²",
+       "thu": "ß\93ß\8cߟߏ߲",
        "fri": "ߛߌ߬ߣߌ߲߬ߟߏ߲",
        "sat": "ߞߍ߲ߘߍߟߏ߲",
        "january": "ߓߌ߲ߠߊߥߎߟߋ߲",
        "moredotdotdot": "ߡߊߞߊ߬ߝߏ߬...",
        "morenotlisted": "ߛߙߍߘߍ ߣߌ߲߬ ߘߝߊߓߊߟߌ߫ ߓߍ߫ ߞߍ߫.",
        "mypage": "ߞߐߜߍ",
-       "mytalk": "ߞߎߡߊ",
+       "mytalk": "ߞߎߡߊ߫",
        "anontalk": "ߢߊߝߐߞߣߍ",
        "navigation": "ߛߏ߲߯ߓߊߟߌ",
        "and": "&#32;ߊ߬ ߣߌ߫",
        "search-filter-title-prefix": "ߞߐߜߍ ߡߍ߲ ߠߎ߬ ߞߎ߲߬ߕߐ߮ ߦߋ߫ ߘߊߡߌ߬ߣߊ߬ ߟߊ߫  \"$1\" ߡߊ߬ ߏ߬ ߟߎ߫ ߟߋ߬ ߘߐߙߐ߲߫ ߢߌߣߌ߲ ߦߴߌ ߘߐ߫.",
        "search-filter-title-prefix-reset": "ߞߐߜߍ ߓߍ߯ ߢߌߣߌ߲߫",
        "searchresults-title": "ߣߌ߲߬ \"$1\" ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ",
+       "titlematches": "ߞߐߜߍ ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߢߐ߲߰ߡߊ߬ߣߍ߲߫",
        "prevn": "ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬ {{PLURAL:$1|$1}}",
        "nextn": "ߟߊߕߎ߲߰ߠߊ {{PLURAL:$1|$1}}",
        "prev-page": "ߞߐߜߍ ߢߍߕߊ",
        "right-editmyuserjs": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
        "right-viewmywatchlist": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߦߋ߫",
        "right-editmyoptions": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߟߊߝߌߛߦߊߟߌ ߡߊߦߟߍ߬ߡߊ߲߫",
+       "right-import": "ߞߐߜߍ ߟߎ߬ ߟߊߛߣߍ߫ ߞߊ߬ ߓߐ߫ ߥߞߌ ߕߐ߭ ߟߎ߬ ߘߐ߫",
+       "right-importupload": "ߞߐߜߍ ߟߎ߬ ߟߊߛߣߍ߫ ߞߊ߬ ߓߐ߫ ߞߐߕߐ߯ ߟߊߦߟߍ߬ߣߍ߲ ߠߎ߬ ߘߐ߫",
+       "right-patrol": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ ߓߍ߬ߙߍ߲߬ߓߍ߬ߙߍ߲߬ߣߍ߲ ߠߎ߬ ߣߐ߬ߣߐ߬.",
+       "right-autopatrol": "ߒ ߖߍ߬ߘߍ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߣߍ߲ ߠߎ߬ ߞߍ߫ ߓߍ߬ߙߍ߲߬ߓߍ߬ߙߍ߲߬ߣߍ߲ ߘߌ߫ ߞߍ߲ߒߖߘߍߦߋ߫ ߓߟߏߡߊ߬",
+       "right-patrolmarks": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߬ ߦߋ߫ ߓߍ߬ߙߍ߲߬ߓߍ߬ߙߍ߲߬ߠߌ߲߫ ߣߐ߬ߣߐ߬ߣߍ߲ ߘߌ߫",
        "right-unwatchedpages": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߓߊߟߌ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫",
        "right-mergehistory": "ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬",
        "right-userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߤߊߞߍ ߓߍ߯ ߡߊߦߟߍ߬ߡߊ߲߫",
        "action-unblockself": "ߌ ߖߍ߬ߘߍ ߓߊ߬ߟߌ߬ߣߍ߲ ߓߐ߫",
        "nchanges": "$1 {{PLURAL:$1|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ߞߊ߬ߦߌ߯ ߓߐߒߡߊߟߌ ߟߊߓߊ߲}}",
-       "enhancedrc-history": "ß\95ß\8a߬ߡß\8c߲߬ߣß\8dß²",
+       "enhancedrc-history": "ß\98ß\90߬ß\9dß\90",
        "recentchanges": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߬",
        "recentchanges-legend": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߟߊ߬ߓߍ߲߬ߢߐ߰ߡߦߊ߬ߘߊ",
        "recentchanges-summary": "ߥߞߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎ߲ߓߊ ߡߍ߲ ߠߎ߬ ߞߍߣߍ߲߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߏ߬ ߟߎ߫ ߣߐ߬ߣߐ߬.",
        "rcfilters-filterlist-feedbacklink": "ߌ ߤߊߞߟߌߣߊ߲ ߝߐ߫ ߊ߲ ߧߋ߫ ߞߊ߬ ߓߍ߲߬ ߛߍ߲ߛߍ߲ߟߊ߲ ߖߐ߯ߙߊ߲ ߠߊ߫ ߞߏ ߡߊ߬.",
        "rcfilters-highlightbutton-title": "ߞߐߝߟߌ߫ ߡߊߦߋߙߋ߲ߣߍ߲ ߠߎ߬",
        "rcfilters-highlightmenu-title": "ߞߐ߬ߟߐ ߘߏ߫ ߓߊߓߌ߬ߟߊ߬",
+       "rcfilters-filterlist-noresults": "ߛߍ߲ߛߍ߲ߟߊ߲߫ ߡߊ߫ ߛߐ߬ߘߐ߲߬",
        "rcfilters-filter-editsbyself-label": "ߡߍ߲ ߠߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬ߣߍ߲߬ ߌ ߓߟߏ߫",
        "rcfilters-filter-editsbyself-description": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲.",
        "rcfilters-filter-editsbyother-label": "ߘߏ ߟߎ߬ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬",
        "filedelete-reason-dropdown": "* ߖߏ߰ߛߌ߬ߟߌ ߟߎ߬ ߝߊ߲߬ߓߊ ߞߎ߲߭\n** ߓߊߦߟߍߡߊ߲ ߤߊߞߍ ߕߌߢߍߟߌ\n** ߞߐߕߐ߯ ߓߊߟߌߣߍ߲ ߠߎ߬",
        "filedelete-edit-reasonlist": "ߖߏ߰ߛߌ߬ߟߌ ߞߎ߲߭ ߡߊߦߟߍ߬ߡߊ߲߫",
        "filedelete-maintenance-title": "ߞߐߕߐ߮ ߕߍ߫ ߛߐ߲߬ ߖߏ߰ߛߌ߬ ߟߊ߫",
+       "mimetype": "MIME ߛߎ߮ߦߊ:",
+       "download": "ߟߊ߬ߖߌ߰ߒ߬ߞߎ߲߬ߠߌ߲",
+       "unwatchedpages": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߓߊߟߌ ߟߎ߬",
+       "listredirects": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߛߙߍߘߍ ߟߎ߬",
+       "listduplicatedfiles": "ߞߐߕߐ߯ ߓߊߟߌߣߍ߲ ߠߎ߬ ߛߙߍߘߍ",
+       "listduplicatedfiles-entry": "[[:File:$1|$1]] ߓߘߊ߫ [[$3|{{PLURAL:$2|ߓߊߟߌ߫|ߟߎ߬ ߓߊߟߌߣߍ߲߫}}]]",
+       "unusedtemplates": "ߞߙߊߞߏ߫ ߟߊߓߊ߯ߙߊߓߊߟߌ ߟߎ߬",
        "unusedtemplateswlh": "ߛߘߌ߬ߜߋ߲ ߜߘߍ ߟߎ߬",
        "randompage": "ߞߎ߲߬ߝߍ߬ ߞߐߜߍ",
+       "randompage-nopages": "ߞߐߕߐ߯ ߛߌ߫ ߕߍ߫ ߢߌ߲߬ ߠߎ߬ ߘߐ߫ \n{{PLURAL:$2|ߕߐ߯ߛߓߍ ߞߣߍ|ߕߐ߯ߛߓߍ߫ ߞߣߍ ߟߎ߬}}: $1.",
        "randomincategory": "ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ ߦߌߟߡߊ ߘߐ߫",
+       "randomincategory-invalidcategory": "$1 ߕߍ߫ ߦߌߟߡߊ߫ ߕߐ߯ ߓߍ߲߬ߣߍ߲߬ ߘߌ߫.",
        "randomincategory-nopages": "ߞߐߜߍ߫ ߛߌ߫ ߕߍ߫  [[:Category:$1|$1]] ߘߌ߫ ߦߌߟߡߊ",
        "randomincategory-category": "ߦߌߟߡߊ",
        "randomincategory-legend": "ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ ߦߌߟߡߊ ߘߐ߫",
        "statistics-pages": "ߞߐߜߍ ߟߎ߬",
        "statistics-pages-desc": "ߞߐߜߍ ߡߍ߲ ߓߍ߯ ߦߋ߫ ߥߞߌ ߞߊ߲߬߸ ߦߏ߫ ߞߎߡߊߢߐ߲߯ߦߊ߫ ߞߐߜߍ߸ ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲߸ ߊ߬ ߣߌ߫.",
        "statistics-files": "ߞߐߕߐ߮ ߟߊߦߟߍ߬ߣߍ߲ ߠߎ߬",
+       "statistics-edits-average": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߎ߰ߡߍ ߡߍ߲ ߞߍߣߍ߲߫ ߞߐߜߍ ߡߊ߬",
+       "statistics-users": "ߟߊߓߊ߯ߙߊߓߊ߯ ߛߙߍߘߍߦߊߣߍ߲ ߠߎ߬",
+       "statistics-users-active-desc": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߡߍ߲ ߠߎ߬ ߝߊߘߌ߲ߧߊ߫ ߘߊ߫ ߞߏ߫ ߘߏ߫ ߞߍ {{PLURAL:$1|ߕߟߋ߬|$1 ߕߋ߬ߟߋ}} ߟߎ߬ ߞߘߐ߫.",
        "pageswithprop-submit": "ߕߊ߯",
        "double-redirect-fixer": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߘߐߓߍ߲߬ߟߊ߲",
        "brokenredirects-edit": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߬",
        "apisandbox-dynamic-parameters-add-label": "ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊߘߏ߲߬",
        "apisandbox-dynamic-parameters-add-placeholder": "ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߕߐ߮",
        "apisandbox-dynamic-error-exists": "ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߕߐ߮  \"$1\" ߦߋ߫ ߦߋ߲߬ ߞߘߐ߬ߡߊ߲߬.",
+       "apisandbox-fetch-token": "ߖߐߟߐ߲ߞߐ ߞߍߒߖߘߍߦߋ߫ ߟߝߊߟߌ",
        "apisandbox-add-multi": "ߟߊ߬ߘߏ߲߬ߠߌ߲",
        "apisandbox-results": "ߞߐߖߋߓߌ ߟߎ߬",
        "apisandbox-sending-request": "API ߡߊ߬ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߗߋߟߌ ߦߴߌ ߘߐ߫...",
        "allpagessubmit": "ߥߊ߫",
        "allpages-hide-redirects": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߢߡߊߘߏ߲߰",
        "categories": "ߦߌߟߡߊ ߟߎ߬",
+       "categoriesfrom": "ߦߌߟߡߊ ߟߎ߬ ߦߌ߬ߘߊ߬ߟߌ ߟߊߝߟߐ߫ ߣߌ߲߬ ߡߊ߬:",
+       "deletedcontributions": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߊ߫ ߓߟߏߡߊߜߍ߲ ߠߎ߬ ߓߘߊ߫ ߖߏ߬ߛߌ߬",
+       "deletedcontributions-title": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߊ߫ ߓߟߏߡߊߜߍ߲ ߓߘߊ߫ ߓߊ߲߫ ߖߏ߬ߛߌ߬ ߟߊ߫",
+       "sp-deletedcontributions-contribs": "ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߠߎ߬",
        "linksearch": "ߞߐߞߊ߲ߠߊ ߛߘߌ߬ߜߋ߲ ߢߌߣߌ߲ߠߌ߲",
+       "linksearch-ns": "ߕߐ߯ߛߓߍ ߞߣߍ:",
+       "linksearch-ok": "ߢߌߣߌ߲ߠߌ߲",
+       "linksearch-line": "$1 ߦߋ߫ ߛߘߌ߬ߜߋ߲ ߠߋ߬ ߘߌ߫ ߞߊ߬ ߓߐ߫ $2",
+       "listusersfrom": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߎ߬ ߦߌ߬ߘߊ߬ߟߌ ߟߊߝߟߐ߫ ߣߌ߲߬ ߡߊ߬:",
+       "listusers-submit": "ߦߌ߬ߘߊ߬ߟߌ",
+       "listusers-noresult": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߕߴߦߋ߲߬",
        "activeusers-noresult": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߕߴߦߋ߲߬",
        "listgrouprights-members": "(ߛߌ߲߬ߝߏ߲ ߠߎ߫ ߛߙߍߘߍ)",
        "emailuser": "ߗߋߛߓߍ ߗߋ߫ ߣߌ߲߬ ߕߌ߭ ߡߊ߬",
        "emailuserfooter": "ߢߎߡߍߙߋ߲ ߣߌ߲߬ ߦߋ߫ {{GENDER:$1|ߗߋߟߌߣߐ ߟߋ߬ ߘߌ߫}} ߞߊ߬ ߝߘߊ߫ $1 ߟߊ߫ ߞߊ߬ ߥߊ߫{{GENDER:$2|$2}} \"{{int:emailuser}}\" ߓߟߏ߫߸ ߦߋ߫ ߓߊ߯ߙߊ߫ ߟߊ߫ {{SITENAME}}. ߣߌ߫ {{GENDER:$2|ߌ߫}} ߞߵߊ߬ ߖߋ߬ߓߌ߬ ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߣߌ߲߬ ߠߊ߫߸ {{GENDER:$2|ߌ ߟߊ߫}} ߢߎߡߍߙߋ߲ ߘߌ߫ ߗߋ߫ {{GENDER:$1|ߗߋߟߌߟߊ ߛߎ߲}} ߠߊ߫߸ ߊ߬ ߘߌ߫ ߖߊ߬ߕߋ߫ {{GENDER:$2|ߌ ߟߊ߫}} ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߓߊ߬ߕߐ߮ ߘߌ߫ ߞߊ߬ ߕߊ߯ {{GENDER:$2|ߞߎߡߘߊ}} ߟߊ߫.",
        "usermessage-editor": "ߞߊ߲ߞߋ߫ ߗߋߛߓߍ ߡߊߦߟߍ߬ߡߊ߲߬ߓߊ߮",
        "watchlist": "ߣߐ߬ߝߍ߬ߜߍ߲߬ߛߙߍߘߍ",
-       "mywatchlist": "ß\98ß\90ß\9cß\8dß« ß\98ß²ß\9cß\8dß\95ß\8a",
+       "mywatchlist": "ß\9cß\8b߬ß\9fß\8e߲߬ߠß\8c߲߬ ß\9bß\99ß\8dß\98ß\8d",
        "watchlistfor2": "ߞߏߛߐ߲߬ $1 $2",
        "watch": "ߊ߬ ߘߐߜߍ߫",
        "unwatch": "ߊ߬ ߞߍ߫ ߦߋߓߊߟߌ ߘߌ߫",
        "tooltip-pt-watchlist": "ߌ ߟߊ߫ ߞߐߜߍ߫ ߡߊߦߟߍ߬ߡߊ߲߬ߕߊ ߜߋ߬ߟߎ߲߬ߣߍ߲ ߠߎ߬ ߛߙߍߘߍ",
        "tooltip-pt-mycontris": "{{GENDER:|ߌ ߟߊ߫}} ߓߟߏߡߊߜߍ߲߫ ߛߙߍߘߍ ߟߎ߬",
        "tooltip-pt-login": "ߌ ߘߐߛߎߣߍ߲߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߡߊ߬߸ ߞߏ߬ߣߌ߲߬ ߘߌߦߊߜߏߦߊ߫ ߕߍ߫",
-       "tooltip-pt-logout": "ߌ ߜߊ߲߬ߞߎ߲߬ ߓߐ߫",
+       "tooltip-pt-logout": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߓߐ߫",
        "tooltip-pt-createaccount": "ߊ߲ ߧߴߌ ߘߐߛߎ߫ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ߬ߞߏ ߡߊ߬ ߊ߬ ߣߌ߫ ߘߏ߲߬ߕߐ߰ߟߊ߬ߘߏ߲߸ ߞߏ߬ߣߌ߲߬ ߢߊ߬ߒ߬ߞߐ߬ߓߊߟߌ߫ ߕߍ߫ ߢߊ߫ ߛߌ߫ ߞߊ߲߬",
        "tooltip-ca-talk": "ߞߣߐߘߐ ߞߐߜߍ ߞߏߢߊ ߘߐߢߌߡߌ߲ߠߌ߲",
        "tooltip-ca-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬",
        "tooltip-n-recentchanges": "ߥߞߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߬ ߛߙߍߘߍ",
        "tooltip-n-randompage": "ߞߐߜߍ ߘߏ߫ ߡߊߦߟߍ߬ߡߊ߲߬ ߞߎ߲߬ߝߍ߬ߞߏ ߘߐ߫",
        "tooltip-n-help": "ߘߍ߬ߡߍ߲߬ ߦߙߐ",
-       "tooltip-t-whatlinkshere": "ߞߐߜߍ ߟߎ߫ ߛߘߌ߬ߜߋ߲ ߛߙߍߘߍ߸ ߡߍ߲ ߠߎ߫ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߦߊ߲߬ ߡߊ߬",
+       "tooltip-t-whatlinkshere": "ߞߐߜߍ ߟߎ߫ ߛߘߌ߬ߜߋ߲ ߛߙߍߘߍ ߡߍ߲ ߠߎ߫ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߦߊ߲߬ ߡߊ߬",
        "tooltip-t-recentchangeslinked": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߠߊ߬ߓߊ߲ ߡߍ߲ ߠߎ߬ ߦߋ߫ ߞߐߜߍ ߣߌ߲߬ ߛߘߌ߬ߜߋ߲ ߠߎ߬ ߘߐ߫",
        "tooltip-feed-atom": "ߞߐߜߍ ߣߌ߲߬ ߝߕߌ߫ ߓߊߟߏ",
        "tooltip-t-contributions": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ ߛߙߍߘߍ",
        "tooltip-t-emailuser": "ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߟߊߕߊ߯ ߟߊߓߊ߯ߙߟߊ ߣߌ߲߬ ߡߊ߬{{GENDER:$1|ߟߊߓߊ߯ߙߟߊ}}",
        "tooltip-t-upload": "ߞߐߕߐ߮ ߟߎ߬ ߟߊߦߟߍ߬",
        "tooltip-t-specialpages": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߐߜߍ ߞߋ߬ߟߋ߲߬ߞߋ߬ߟߋ߲߬ߠߊ ߟߎ߬ ߛߙߍߘߍ",
-       "tooltip-t-print": "\nß\9eß\90ß\9cß\8d ß£ß\8c߲߬ ß\93ß\90ß\9eß\8fߣß\8a߲߫ ß\9cß\8cß\99ß\8cß²ß\98ß\8cߕߊ",
+       "tooltip-t-print": "\nß\9eß\90ß\9cß\8d ß£ß\8c߲߬ ß¦ß\8cß\9fß¡ß\8aß« ß\9cß\8c߬ß\99ß\8c߲߬ß\98ß\8c߬ߕߊ",
        "tooltip-t-permalink": "ߞߐߜߍ ߣߌ߲߬ ߡߛߊ߬ߦߌ߲߬ߠߌ߲߬ ߛߘߌ߬ߜߋ߲߬ ߞߎߘߊߦߌ",
        "tooltip-ca-nstab-main": "ߞߣߐߘߐ ߞߣߐߘߐ߫ ߘߐߜߍ߫",
        "tooltip-ca-nstab-user": "ߞߐߜߍ߫ ߟߊߓߊ߯ߙߕߊ ߘߐߜߍ߫",
index 7a5671a..3783e26 100644 (file)
        "log-action-filter-suppress-block": "Сокрытие пользователя через блокировки",
        "log-action-filter-suppress-reblock": "Сокрытие пользователя через повторное блокирование",
        "log-action-filter-upload-upload": "Новая загрузка",
-       "log-action-filter-upload-overwrite": "Ð\9fовÑ\82оÑ\80но Ð·Ð°Ð³Ñ\80Ñ\83зиÑ\82Ñ\8c",
-       "log-action-filter-upload-revert": "Ð\9eÑ\82каÑ\82иÑ\82Ñ\8c",
+       "log-action-filter-upload-overwrite": "Ð\9fеÑ\80езапиÑ\81Ñ\8c Ñ\84айла",
+       "log-action-filter-upload-revert": "Ð\92озвÑ\80аÑ\82 Ñ\81Ñ\82аÑ\80ой Ð²ÐµÑ\80Ñ\81ии Ñ\84айла",
        "authmanager-authn-not-in-progress": "Проверка подлинности не выполняется или данные сессии были утеряны. Пожалуйста, начните снова с самого начала.",
        "authmanager-authn-no-primary": "Предоставленные учётные данные не могут быть проверены на подлинность.",
        "authmanager-authn-no-local-user": "Предоставленные учётные данные не связаны ни с одним участником этой вики.",
index 33ff4f3..db88999 100644 (file)
        "revertmerge": "растави",
        "mergelogpagetext": "Испод се налази списак најновијих обједињавања историја једне странице у другу.",
        "history-title": "Историја измена странице „$1”",
-       "difference-title": "Разлика између измена на страници „$1”",
+       "difference-title": "$1 — разлика између измена",
        "difference-title-multipage": "Разлика између страница „$1“ и „$2“",
        "difference-multipage": "(разлике између страница)",
        "lineno": "Ред $1:",
        "svg-long-desc": "SVG датотека, номинално $1 × $2 пиксела, величина: $3",
        "svg-long-desc-animated": "Анимирана SVG датотека, номинално: $1 × $2 пиксела, величина: $3",
        "svg-long-error": "Неважећа SVG датотека: $1",
-       "show-big-image": "Ð\9fÑ\80вобиÑ\82на датотека",
+       "show-big-image": "Ð\9eÑ\80игинална датотека",
        "show-big-image-preview": "Величина овог приказа: $1.",
        "show-big-image-preview-differ": "Величина $3 прегледа за ову $2 датотеку је $1.",
        "show-big-image-other": "$2 {{PLURAL:$2|друга резолуција|друге резолуције|других резолуција}}: $1.",
index aa43bb3..6db295c 100644 (file)
@@ -31,7 +31,8 @@
                        "Fitoschido",
                        "TrisT7",
                        "Patsagorn Y.",
-                       "Geonuch"
+                       "Geonuch",
+                       "กิ๊ฟ เลิกล่ะ สายแข็ง"
                ]
        },
        "tog-underline": "การขีดเส้นใต้ลิงก์:",
        "autoblockedtext": "เลขที่อยู่ไอพีของคุณถูกบล็อกอัตโนมัติ เพราะเคยมีผู้ใช้อื่นใช้ ซึ่งถูกบล็อกโดย $1\nโดยให้เหตุผลว่า\n\n:<em>$2</em>\n\n* เริ่มการบล็อก: $8\n* สิ้นสุดการบล็อก: $6\n* ผู้ถูกบล็อกที่เจตนา: $7\n\nคุณสามารถติดต่อ $1 หรือ[[{{MediaWiki:Grouppage-sysop}}|ผู้ดูแลระบบ]]คนอื่นเพื่ออภิปรายการบล็อกนี้ \nคุณไม่สามารถใช้คุณลักษณะ \"{{int:emailuser}}\" จนกว่าจะระบุที่อยู่อีเมลที่ถูกต้องใน[[Special:Preferences|การตั้งค่าบัญชี]]ของคุณ และคุณมิได้ถูกห้ามใช้\nเลขที่อยู่ไอพีปัจจุบันของคุณคือ $3 และหมายเลขการบล็อกคือ #$5 \nโปรดรวมรายละเอียดข้างต้นทั้งหมดในการสอบถามใด ๆ",
        "systemblockedtext": "ชื่อผู้ใช้หรือที่อยู่ไอพีของคุณถูกบล็อกอัตโนมัติโดยมีเดียวิกิ\nเหตุผลสำหรับการบล็อกคือ:\n\n:<em>$2</em>\n\n* เริ่มการบล็อก: $8\n* สิ้นสุดการบล็อก: $6\n* ผู้ดำเนินการบล็อก: $7\n\nไอพีแอดเดรสปัจจุบันของคุณคือ $3\nโปรดแจ้งรายละเอียดทั้งหมดข้างต้น ถ้าคุณมีข้อสงสัยใด ๆ",
        "blockednoreason": "ไม่ได้ให้เหตุผล",
+       "blockedtext-composite": "<strong>",
        "whitelistedittext": "คุณต้อง$1เพื่อแก้ไขหน้า",
        "confirmedittext": "คุณต้องยืนยันที่อยู่อีเมลของคุณก่อนแก้ไขหน้า \nโปรดตั้งและตรวจสอบความสมเหตุสมผลของที่อยู่อีเมลของคุผ่าน[[Special:Preferences|การตั้งค่าผู้ใช้]]",
        "nosuchsectiontitle": "ไม่พบส่วน",
index 4da0901..e1d7fca 100644 (file)
@@ -56,6 +56,8 @@ class AttachLatest extends Maintenance {
                        __METHOD__ );
 
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $dbDomain = $lbFactory->getLocalDomainID();
+
                $n = 0;
                foreach ( $result as $row ) {
                        $pageId = intval( $row->page_id );
@@ -66,18 +68,19 @@ class AttachLatest extends Maintenance {
                                [ 'rev_page' => $pageId ],
                                __METHOD__ );
                        if ( !$latestTime ) {
-                               $this->output( wfWikiID() . " $pageId [[$name]] can't find latest rev time?!\n" );
+                               $this->output( "$dbDomain $pageId [[$name]] can't find latest rev time?!\n" );
                                continue;
                        }
 
                        $revision = Revision::loadFromTimestamp( $dbw, $title, $latestTime );
                        if ( is_null( $revision ) ) {
-                               $this->output( wfWikiID()
-                                       . " $pageId [[$name]] latest time $latestTime, can't find revision id\n" );
+                               $this->output(
+                                       "$dbDomain $pageId [[$name]] latest time $latestTime, can't find revision id\n"
+                               );
                                continue;
                        }
                        $id = $revision->getId();
-                       $this->output( wfWikiID() . " $pageId [[$name]] latest time $latestTime, rev id $id\n" );
+                       $this->output( "$dbDomain $pageId [[$name]] latest time $latestTime, rev id $id\n" );
                        if ( $this->hasOption( 'fix' ) ) {
                                $page = WikiPage::factory( $title );
                                $page->updateRevisionOn( $dbw, $revision );
index eb45cfc..536e6db 100644 (file)
@@ -87,7 +87,8 @@ TEXT
 
                $this->outputStatus( 'Done!' );
                if ( $this->hasOption( 'fix' ) ) {
-                       $this->outputStatus( ' Cleaned up invalid DB keys on ' . wfWikiID() . "!\n" );
+                       $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
+                       $this->outputStatus( " Cleaned up invalid DB keys on $dbDomain!\n" );
                }
        }
 
index 9ba5bf5..1142325 100644 (file)
@@ -121,7 +121,6 @@ class CopyFileBackend extends Maintenance {
                        }
                        if ( count( $batchPaths ) ) { // left-overs
                                $this->copyFileBatch( array_keys( $batchPaths ), $backendRel, $src, $dst );
-                               $batchPaths = []; // done
                        }
                        $this->output( "\tCopied $count file(s).\n" );
 
@@ -148,7 +147,6 @@ class CopyFileBackend extends Maintenance {
                                }
                                if ( count( $batchPaths ) ) { // left-overs
                                        $this->delFileBatch( array_keys( $batchPaths ), $backendRel, $dst );
-                                       $batchPaths = []; // done
                                }
 
                                $this->output( "\tDeleted $count file(s).\n" );
@@ -212,7 +210,7 @@ class CopyFileBackend extends Maintenance {
                $ops = [];
                $fsFiles = [];
                $copiedRel = []; // for output message
-               $wikiId = $src->getWikiId();
+               $domainId = $src->getDomainId();
 
                // Download the batch of source files into backend cache...
                if ( $this->hasOption( 'missingonly' ) ) {
@@ -232,7 +230,7 @@ class CopyFileBackend extends Maintenance {
                        $srcPath = $src->getRootStoragePath() . "/$backendRel/$srcPathRel";
                        $dstPath = $dst->getRootStoragePath() . "/$backendRel/$srcPathRel";
                        if ( $this->hasOption( 'utf8only' ) && !mb_check_encoding( $srcPath, 'UTF-8' ) ) {
-                               $this->error( "$wikiId: Detected illegal (non-UTF8) path for $srcPath." );
+                               $this->error( "$domainId: Detected illegal (non-UTF8) path for $srcPath." );
                                continue;
                        } elseif ( !$this->hasOption( 'missingonly' )
                                && $this->filesAreSame( $src, $dst, $srcPath, $dstPath )
@@ -246,24 +244,24 @@ class CopyFileBackend extends Maintenance {
                        if ( !$fsFile ) {
                                $src->clearCache( [ $srcPath ] );
                                if ( $src->fileExists( [ 'src' => $srcPath, 'latest' => 1 ] ) === false ) {
-                                       $this->error( "$wikiId: File '$srcPath' was listed but does not exist." );
+                                       $this->error( "$domainId: File '$srcPath' was listed but does not exist." );
                                } else {
-                                       $this->error( "$wikiId: Could not get local copy of $srcPath." );
+                                       $this->error( "$domainId: Could not get local copy of $srcPath." );
                                }
                                continue;
                        } elseif ( !$fsFile->exists() ) {
                                // FSFileBackends just return the path for getLocalReference() and paths with
                                // illegal slashes may get normalized to a different path. This can cause the
                                // local reference to not exist...skip these broken files.
-                               $this->error( "$wikiId: Detected possible illegal path for $srcPath." );
+                               $this->error( "$domainId: Detected possible illegal path for $srcPath." );
                                continue;
                        }
                        $fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed
                        // Note: prepare() is usually fast for key/value backends
                        $status = $dst->prepare( [ 'dir' => dirname( $dstPath ), 'bypassReadOnly' => 1 ] );
                        if ( !$status->isOK() ) {
-                               $this->error( print_r( $status->getErrorsArray(), true ) );
-                               $this->fatalError( "$wikiId: Could not copy $srcPath to $dstPath." );
+                               $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+                               $this->fatalError( "$domainId: Could not copy $srcPath to $dstPath." );
                        }
                        $ops[] = [ 'op' => 'store',
                                'src' => $fsFile->getPath(), 'dst' => $dstPath, 'overwrite' => 1 ];
@@ -279,8 +277,8 @@ class CopyFileBackend extends Maintenance {
                }
                $elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( !$status->isOK() ) {
-                       $this->error( print_r( $status->getErrorsArray(), true ) );
-                       $this->fatalError( "$wikiId: Could not copy file batch." );
+                       $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+                       $this->fatalError( "$domainId: Could not copy file batch." );
                } elseif ( count( $copiedRel ) ) {
                        $this->output( "\n\tCopied these file(s) [{$elapsed_ms}ms]:\n\t" .
                                implode( "\n\t", $copiedRel ) . "\n\n" );
@@ -298,7 +296,7 @@ class CopyFileBackend extends Maintenance {
        ) {
                $ops = [];
                $deletedRel = []; // for output message
-               $wikiId = $dst->getWikiId();
+               $domainId = $dst->getDomainId();
 
                // Determine what files need to be copied over...
                foreach ( $dstPathsRel as $dstPathRel ) {
@@ -316,8 +314,8 @@ class CopyFileBackend extends Maintenance {
                }
                $elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( !$status->isOK() ) {
-                       $this->error( print_r( $status->getErrorsArray(), true ) );
-                       $this->fatalError( "$wikiId: Could not delete file batch." );
+                       $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+                       $this->fatalError( "$domainId: Could not delete file batch." );
                } elseif ( count( $deletedRel ) ) {
                        $this->output( "\n\tDeleted these file(s) [{$elapsed_ms}ms]:\n\t" .
                                implode( "\n\t", $deletedRel ) . "\n\n" );
index 93614e0..da9b4d6 100644 (file)
@@ -103,11 +103,12 @@ class CreateAndPromote extends Maintenance {
 
                        return;
                } elseif ( count( $promotions ) !== 0 ) {
+                       $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
                        $promoText = "User:{$username} into " . implode( ', ', $promotions ) . "...\n";
                        if ( $exists ) {
-                               $this->output( wfWikiID() . ": Promoting $promoText" );
+                               $this->output( "$dbDomain: Promoting $promoText" );
                        } else {
-                               $this->output( wfWikiID() . ": Creating and promoting $promoText" );
+                               $this->output( "$dbDomain: Creating and promoting $promoText" );
                        }
                }
 
@@ -149,7 +150,7 @@ class CreateAndPromote extends Maintenance {
 
                if ( !$exists ) {
                        # Increment site_stats.ss_users
-                       $ssu = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+                       $ssu = SiteStatsUpdate::factory( [ 'users' => 1 ] );
                        $ssu->doUpdate();
                }
 
index 45457f5..aa9cb2e 100644 (file)
@@ -4635,15 +4635,11 @@ yourwiki
 yuml
 yyyymmddhhiiss
 zcmd
-zerobanner
 zerobar
 zerobutton
-zeroconfig
 zerodontask
-zerodot
 zeroinfo
 zeronet
-zeroportal
 zfile
 zhdaemon
 zhengzhu
index 0118c94..df3b4a1 100644 (file)
@@ -412,10 +412,12 @@ abstract class BackupDumper extends Maintenance {
                                $pageRatePart = '-';
                                $revRatePart = '-';
                        }
+
+                       $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
                        $this->progress( sprintf(
                                "%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), "
                                        . "%d revs (%0.1f|%0.1f/sec all|curr), ETA %s [max %d]",
-                               $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate,
+                               $now, $dbDomain, $this->ID, $this->pageCount, $pageRate,
                                $pageRatePart, $this->revCount, $revRate, $revRatePart, $etats,
                                $this->maxCount
                        ) );
index b37fec1..c6738bc 100644 (file)
@@ -373,11 +373,13 @@ TEXT
                                $pageRatePart = '-';
                                $revRatePart = '-';
                        }
+
+                       $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
                        $this->progress( sprintf(
                                "%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), "
                                        . "%d revs (%0.1f|%0.1f/sec all|curr), %0.1f%%|%0.1f%% "
                                        . "prefetched (all|curr), ETA %s [max %d]",
-                               $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate,
+                               $now, $dbDomain, $this->ID, $this->pageCount, $pageRate,
                                $pageRatePart, $this->revCount, $revRate, $revRatePart,
                                $fetchRate, $fetchRatePart, $etats, $this->maxCount
                        ) );
index 938503c..f4cd2d3 100644 (file)
@@ -87,7 +87,7 @@ in the load balancer, usually indicating a replication environment.' );
                                $delta = microtime( true ) - $start;
                                $rate = ( $delta == 0.0 ) ? 0.0 : $migrated / $delta;
                                $this->output( sprintf( "%s %d (%0.1f%%) done in %0.1f secs (%0.3f accounts/sec).\n",
-                                       wfWikiID(),
+                                       WikiMap::getCurrentWikiDbDomain()->getId(),
                                        $migrated,
                                        min( $max, $lastUser ) / $lastUser * 100.0,
                                        $delta,
index 1613b83..e1016dc 100644 (file)
 索羅門群島        所罗门群岛
 汶萊 文莱
 史瓦濟蘭   斯威士兰
+史瓦帝尼   斯威士兰
 斯洛維尼亞        斯洛文尼亚
 紐西蘭      新西兰
 格瑞那達   格林纳达
 波士尼亞與赫塞哥維納 波斯尼亚和黑塞哥维那
 辛巴威      津巴布韦
 宏都拉斯   洪都拉斯
-千里達托貝哥     特立尼达和托巴哥
 萬那杜      瓦努阿图
 溫納圖      瓦努阿图
 葛摩 科摩罗
@@ -2554,6 +2554,9 @@ IP位址  IP地址
 電視影集   电视系列剧
 原子筆      圆珠笔
 智慧卡      智能卡
+尾班車      末班车
+落車 下车
+上落客      上下客
 鐵達尼號   泰坦尼克号
 轉殖 克隆
 空中巴士   空中客车
@@ -2713,7 +2716,6 @@ A型肝炎        甲型肝炎
 卑詩省      不列颠哥伦比亚省
 丹帕沙      登巴萨
 峇里 巴厘
-史瓦帝尼   斯威士兰
 皮特肯      皮特凯恩
 安地卡      安提瓜
 撒拉威阿拉伯     阿拉伯撒哈拉
@@ -2725,3 +2727,4 @@ A型肝炎        甲型肝炎
 格瑞那丁   格林纳丁斯
 普立茲獎   普利策奖
 富比士      福布斯
+聖多美普林西比  圣多美和普林西比
index 4bc445b..915050b 100644 (file)
 公寓里      公寓裏
 窝里斗      窩裏鬥
 镇里 鎮裏
+》里 》裏
+空里 空裏
+版本里      版本裏
 苑裡 苑裡
 霄裡 霄裡
 岸裡 岸裡
 寫著 寫着
 遇著 遇着
 殺著 殺着
©¶è\91\97 é©¶
§\9bè\91\97 é§\9b
 著筆 着筆
 著鞭 着鞭
 著法 着法
 厄瓜多尔   厄瓜多爾
 厄瓜多爾   厄瓜多爾
 厄瓜多      厄瓜多爾
+馬拉威      馬拉維
 百慕大      百慕達
 厄利垂亞   厄立特里亞
 吉布地      吉布堤
 索羅門群島        所羅門群島
 文莱 汶萊
 史瓦濟蘭   斯威士蘭
+史瓦帝尼   斯威士蘭
 斯洛維尼亞        斯洛文尼亞
 紐西蘭      新西蘭
 格瑞那達   格林納達
 沙烏地阿拉伯     沙特阿拉伯
 辛巴威      津巴布韋
 宏都拉斯   洪都拉斯
-千里達托貝哥     特立尼達和多巴哥
+千里達及托巴哥  特立尼達和多巴哥
 萬那杜      瓦努阿圖
 葛摩 科摩羅
 寮國 老撾
 北朝鲜      北韓
 寮語 老撾語
 寮人民民主共和國       老撾人民民主共和國
+蘇利南      蘇里南
 莱特湾      雷伊泰灣
 萊特灣      雷伊泰灣
 蘭卡威      浮羅交怡
@@ -3069,8 +3075,7 @@ IP地址  IP位址
 帕拉林匹克        殘疾人奧林匹克
 不列颠哥伦比亚省       卑詩省
 丹帕沙      登巴薩
-巴厘岛      峇里島
-史瓦帝尼   斯威士蘭
+巴厘 峇里
 皮特凯恩   皮特肯
 安地卡      安提瓜
 撒拉威阿拉伯     阿拉伯撒哈拉
@@ -3085,3 +3090,5 @@ IP地址  IP位址
 疯牛病      瘋牛症
 狂牛症      瘋牛症
 普利策奖   普立茲獎
+聖多美普林西比  聖多美和普林西比
+塔希提      大溪地
index 4adbbcf..6329133 100644 (file)
 剖釐 剖厘
 一釐 一厘
 昇平 升平
+昇起 升起
 飛昇 飞升
 提昇 提升
 高昇 高升
 滿拚自盡   满拚自尽
 拚生尽死   拚生尽死
 拚生盡死   拚生尽死
+崑劇 昆剧
+崑山 昆山
+崑岡 昆冈
+崑崙 昆仑
+崑嵛 昆嵛
+崑曲 昆曲
+崑腔 昆腔
+崑蘇 昆苏
+崑調 昆调
+蘇崑 苏昆
+西崑 西昆
+靈崑 灵昆
+崑承湖      昆承湖
index bf24176..6b5ecdd 100644 (file)
 所罗门群岛        索羅門群島
 所羅門群島        索羅門群島
 文莱 汶萊
-斯威士兰   史瓦濟蘭
-斯威士蘭   史瓦濟蘭
+斯威士兰   史瓦帝尼
+斯威士蘭   史瓦帝尼
 斯洛文尼亚        斯洛維尼亞
 斯洛文尼亞        斯洛維尼亞
 新西兰      紐西蘭
 津巴布韦   辛巴威
 津巴布韋   辛巴威
 洪都拉斯   宏都拉斯
-特立尼达和托巴哥       千里達托貝
-特立尼達和多巴哥       千里達托貝
+特立尼达和托巴哥       千里達及托巴
+特立尼達和多巴哥       千里達及托巴
 瑙鲁 諾魯
 瑙魯 諾魯
 瓦努阿图   萬那杜
 内罗毕      奈洛比
 內羅畢      奈洛比
 苏里南      蘇利南
+蘇里南      蘇利南
 莫桑比克   莫三比克
 莱索托      賴索托
 萊索托      賴索托
 金沙薩      金夏沙
 达累斯萨拉姆     三蘭港
 马拉维      馬拉威
+馬拉維      馬拉威
 留尼汪      留尼旺
 布隆方丹   布隆泉
 厄瓜多      厄瓜多
@@ -663,6 +665,7 @@ IP地址    IP位址
 东南亚国家联盟  東南亞國家協會
 東南亞國家聯盟  東南亞國家協會
 哥特式      哥德式
+尾班車      末班車
 落車 下車
 上落客      上下客
 集装箱      貨櫃
@@ -808,9 +811,7 @@ IP地址    IP位址
 不列颠哥伦比亚省       卑詩省
 登巴萨      丹帕沙
 登巴薩      丹帕沙
-巴厘岛      峇里島
-斯威士兰   史瓦帝尼
-斯威士蘭   史瓦帝尼
+巴厘 峇里
 皮特凯恩   皮特肯
 安提瓜      安地卡
 阿拉伯撒哈拉     撒拉威阿拉伯
@@ -823,3 +824,6 @@ IP地址    IP位址
 格林納丁斯        格瑞那丁
 空中客车   空中巴士
 普利策奖   普立茲獎
+圣多美和普林西比       聖多美普林西比
+聖多美和普林西比       聖多美普林西比
+塔希提      大溪地
index 871f1ef..78b5a73 100644 (file)
 黄岩县      黃巖縣
 黄岩区      黃巖區
 北仑河      北崙河
+昆剧 崑劇
+昆山 崑山
+昆冈 崑岡
+昆仑 崑崙
 昆嵛 崑嵛
-昆承湖      崑承湖
+昆曲 崑曲
+昆腔 崑腔
+昆苏 崑蘇
+昆调 崑調
+苏昆 蘇崑
+西昆 西崑
 灵昆 靈崑
+昆承湖      崑承湖
 龙岩 龍巖
 扑冬 撲鼕
 冬冬鼓      鼕鼕鼓
index 5ff1d63..74064bb 100644 (file)
@@ -197,7 +197,6 @@ U+05C5B屛|U+05C4F屏|
 U+05C6D屭|U+05C43屃|
 U+05C85岅|U+05742坂|
 U+05CDD峝|U+05CD2峒|
-U+05D11崑|U+06606昆|
 U+05D19崙|U+04ED1仑|
 U+05D57嵗|U+05C81岁|
 U+05D7D嵽|U+2BD87𫶇|
index 4a480ab..2cf35ba 100644 (file)
@@ -59,6 +59,7 @@
 這只比
 這只限
 這只應
+這只要
 這只不過
 這只包括
 那只能
@@ -73,6 +74,7 @@
 那只比
 那只限
 那只應
+那只要
 那只不過
 那只包括
 多只能
 黑奴籲天錄
 林郁方
 讚歌
-崑山
-崑曲
-崑腔
-崑調
-崑劇
-崑蘇
-蘇崑
 一干家中
 星期後
 依依不捨
 于禁
 于敏中
 註:# 不作“注:”
+關注:
 劃為# 不作“划為”
 一個# 避免“個裡”的錯誤
 兩個
 殿裡
 隊裡
 詞裡
+》裡
+空裡
+版本裡
 裏白 #植物常用名
 烏蘇里 #分詞用
 夸脫
 于丹
 于冕
 于吉
+於吉林
 于堅
 于姓
 于氏
 米瀋
 拾瀋
 姦污
-託兒
 同人誌
 文學誌
 衝着
 燉製
 煮製
 熬製
+包製
+製漢字 #和製漢字等
 遏制 #以下分詞用
 管制
 抑制
 胎發生
 結發育
 結發表
+金發放
+理發放
 古人有云
 昔人有云
 云敞
 哈囉喂
 松口鎮
 岩松了
+松開始
 沙瑯
 琺瑯
 菜餚
 關系統
 關系所
 關系科
-崑崙
-崑山
-崑劇
-崑曲
-崑腔
-崑蘇
-崑調
-崑岡
-西崑
-蘇崑
 銹病
 嚐糞
index 98f0eb9..513edf3 100644 (file)
@@ -43,6 +43,8 @@ class McTest extends Maintenance {
        public function execute() {
                global $wgMainCacheType, $wgMemCachedTimeout, $wgObjectCaches;
 
+               $memcachedTypes = [ CACHE_MEMCACHED, 'memcached-php', 'memcached-pecl' ];
+
                $cache = $this->getOption( 'cache' );
                $iterations = $this->getOption( 'i', 100 );
                if ( $cache ) {
@@ -52,7 +54,7 @@ class McTest extends Maintenance {
                        $servers = $wgObjectCaches[$cache]['servers'];
                } elseif ( $this->hasArg( 0 ) ) {
                        $servers = [ $this->getArg( 0 ) ];
-               } elseif ( $wgMainCacheType === CACHE_MEMCACHED ) {
+               } elseif ( in_array( $wgMainCacheType, $memcachedTypes, true ) ) {
                        global $wgMemCachedServers;
                        $servers = $wgMemCachedServers;
                } elseif ( isset( $wgObjectCaches[$wgMainCacheType]['servers'] ) ) {
index ba0fe5d..9e0971a 100644 (file)
@@ -92,7 +92,11 @@ class NukePage extends Maintenance {
                        if ( $delete ) {
                                $this->output( "Updating site stats..." );
                                $ga = $isGoodArticle ? -1 : 0; // if it was good, decrement that too
-                               $stats = new SiteStatsUpdate( 0, -$count, $ga, -1 );
+                               $stats = SiteStatsUpdate::factory( [
+                                       'edits' => -$count,
+                                       'articles' => $ga,
+                                       'pages' => -1
+                               ] );
                                $stats->doUpdate();
                                $this->output( "done.\n" );
                        }
index 4e92653..35af15c 100644 (file)
@@ -80,6 +80,8 @@ class RebuildRecentchanges extends Maintenance {
 
        /**
         * Rebuild pass 1: Insert `recentchanges` entries for page revisions.
+        *
+        * @param ILBFactory $lbFactory
         */
        private function rebuildRecentChangesTablePass1( ILBFactory $lbFactory ) {
                $dbw = $this->getDB( DB_MASTER );
@@ -177,6 +179,8 @@ class RebuildRecentchanges extends Maintenance {
        /**
         * Rebuild pass 2: Enhance entries for page revisions with references to the previous revision
         * (rc_last_oldid, rc_new etc.) and size differences (rc_old_len, rc_new_len).
+        *
+        * @param ILBFactory $lbFactory
         */
        private function rebuildRecentChangesTablePass2( ILBFactory $lbFactory ) {
                $dbw = $this->getDB( DB_MASTER );
@@ -199,25 +203,25 @@ class RebuildRecentchanges extends Maintenance {
                $lastOldId = 0;
                $lastSize = null;
                $updated = 0;
-               foreach ( $res as $obj ) {
+               foreach ( $res as $row ) {
                        $new = 0;
 
-                       if ( $obj->rc_cur_id != $lastCurId ) {
+                       if ( $row->rc_cur_id != $lastCurId ) {
                                # Switch! Look up the previous last edit, if any
-                               $lastCurId = intval( $obj->rc_cur_id );
-                               $emit = $obj->rc_timestamp;
+                               $lastCurId = intval( $row->rc_cur_id );
+                               $emit = $row->rc_timestamp;
 
-                               $row = $dbw->selectRow(
+                               $revRow = $dbw->selectRow(
                                        'revision',
                                        [ 'rev_id', 'rev_len' ],
                                        [ 'rev_page' => $lastCurId, "rev_timestamp < " . $dbw->addQuotes( $emit ) ],
                                        __METHOD__,
                                        [ 'ORDER BY' => 'rev_timestamp DESC' ]
                                );
-                               if ( $row ) {
-                                       $lastOldId = intval( $row->rev_id );
+                               if ( $revRow ) {
+                                       $lastOldId = intval( $revRow->rev_id );
                                        # Grab the last text size if available
-                                       $lastSize = !is_null( $row->rev_len ) ? intval( $row->rev_len ) : null;
+                                       $lastSize = !is_null( $revRow->rev_len ) ? intval( $revRow->rev_len ) : null;
                                } else {
                                        # No previous edit
                                        $lastOldId = 0;
@@ -233,7 +237,7 @@ class RebuildRecentchanges extends Maintenance {
                                $size = (int)$dbw->selectField(
                                        'revision',
                                        'rev_len',
-                                       [ 'rev_id' => $obj->rc_this_oldid ],
+                                       [ 'rev_id' => $row->rc_this_oldid ],
                                        __METHOD__
                                );
 
@@ -249,13 +253,13 @@ class RebuildRecentchanges extends Maintenance {
                                        ],
                                        [
                                                'rc_cur_id' => $lastCurId,
-                                               'rc_this_oldid' => $obj->rc_this_oldid,
-                                               'rc_timestamp' => $obj->rc_timestamp // index usage
+                                               'rc_this_oldid' => $row->rc_this_oldid,
+                                               'rc_timestamp' => $row->rc_timestamp // index usage
                                        ],
                                        __METHOD__
                                );
 
-                               $lastOldId = intval( $obj->rc_this_oldid );
+                               $lastOldId = intval( $row->rc_this_oldid );
                                $lastSize = $size;
 
                                if ( ( ++$updated % $this->getBatchSize() ) == 0 ) {
@@ -267,6 +271,8 @@ class RebuildRecentchanges extends Maintenance {
 
        /**
         * Rebuild pass 3: Insert `recentchanges` entries for action logs.
+        *
+        * @param ILBFactory $lbFactory
         */
        private function rebuildRecentChangesTablePass3( ILBFactory $lbFactory ) {
                global $wgLogRestrictions, $wgFilterLogTypes;
@@ -347,6 +353,8 @@ class RebuildRecentchanges extends Maintenance {
 
        /**
         * Rebuild pass 4: Mark bot and autopatrolled entries.
+        *
+        * @param ILBFactory $lbFactory
         */
        private function rebuildRecentChangesTablePass4( ILBFactory $lbFactory ) {
                global $wgUseRCPatrol, $wgMiserMode;
@@ -376,8 +384,8 @@ class RebuildRecentchanges extends Maintenance {
                        );
 
                        $botusers = [];
-                       foreach ( $res as $obj ) {
-                               $botusers[] = User::newFromRow( $obj );
+                       foreach ( $res as $row ) {
+                               $botusers[] = User::newFromRow( $row );
                        }
 
                        # Fill in the rc_bot field
@@ -428,8 +436,8 @@ class RebuildRecentchanges extends Maintenance {
                                [ 'user_groups' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
                        );
 
-                       foreach ( $res as $obj ) {
-                               $patrolusers[] = User::newFromRow( $obj );
+                       foreach ( $res as $row ) {
+                               $patrolusers[] = User::newFromRow( $row );
                        }
 
                        # Fill in the rc_patrolled field
@@ -453,8 +461,10 @@ class RebuildRecentchanges extends Maintenance {
        }
 
        /**
-        * Rebuild pass 5: Delete duplicate entries where we generate both a page revision and a log entry
-        * for a single action (upload only, at the moment, but potentially also move, protect, ...).
+        * Rebuild pass 5: Delete duplicate entries where we generate both a page revision and a log
+        * entry for a single action (upload only, at the moment, but potentially move, protect, ...).
+        *
+        * @param ILBFactory $lbFactory
         */
        private function rebuildRecentChangesTablePass5( ILBFactory $lbFactory ) {
                $dbw = wfGetDB( DB_MASTER );
@@ -475,9 +485,9 @@ class RebuildRecentchanges extends Maintenance {
                );
 
                $updates = 0;
-               foreach ( $res as $obj ) {
-                       $rev_id = $obj->ls_value;
-                       $log_id = $obj->ls_log_id;
+               foreach ( $res as $row ) {
+                       $rev_id = $row->ls_value;
+                       $log_id = $row->ls_log_id;
 
                        // Mark the logging row as having an associated rev id
                        $dbw->update(
index 219b47c..3866be7 100644 (file)
@@ -57,12 +57,12 @@ class OrphanStats extends Maintenance {
                $hashes = [];
                $maxSize = 0;
 
-               foreach ( $res as $boRow ) {
-                       $extDB = $this->getDB( $boRow->bo_cluster );
+               foreach ( $res as $row ) {
+                       $extDB = $this->getDB( $row->bo_cluster );
                        $blobRow = $extDB->selectRow(
                                'blobs',
                                '*',
-                               [ 'blob_id' => $boRow->bo_blob_id ],
+                               [ 'blob_id' => $row->bo_blob_id ],
                                __METHOD__
                        );
 
index e6733a1..8a8f4d8 100644 (file)
@@ -148,7 +148,7 @@ class RecompressTracked {
                if ( $this->replicaId !== false ) {
                        $header .= "({$this->replicaId})";
                }
-               $header .= ' ' . wfWikiID();
+               $header .= ' ' . WikiMap::getCurrentWikiDbDomain()->getId();
                LegacyLogger::emit( sprintf( "%-50s %s\n", $header, $msg ), $file );
        }
 
index 76a5721..7d343b2 100644 (file)
@@ -49,7 +49,11 @@ class SyncFileBackend extends Maintenance {
                $src = FileBackendGroup::singleton()->get( $this->getOption( 'src' ) );
 
                $posDir = $this->getOption( 'posdir' );
-               $posFile = $posDir ? $posDir . '/' . wfWikiID() : false;
+               if ( $posDir != '' ) {
+                       $posFile = "$posDir/" . rawurlencode( $src->getDomainId() );
+               } else {
+                       $posFile = false;
+               }
 
                if ( $this->hasOption( 'posdump' ) ) {
                        // Just dump the current position into the specified position dir
index fe40536..d84ec5c 100755 (executable)
@@ -28,6 +28,7 @@
 require_once __DIR__ . '/Maintenance.php';
 
 use Wikimedia\Rdbms\IMaintainableDatabase;
+use Wikimedia\Rdbms\DatabaseSqlite;
 
 /**
  * Maintenance script to run database schema updates.
@@ -160,7 +161,8 @@ class UpdateMediaWiki extends Maintenance {
                        $this->fatalError( $text );
                }
 
-               $this->output( "Going to run database updates for " . wfWikiID() . "\n" );
+               $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
+               $this->output( "Going to run database updates for $dbDomain\n" );
                if ( $db->getType() === 'sqlite' ) {
                        /** @var IMaintainableDatabase|DatabaseSqlite $db */
                        $this->output( "Using SQLite file: '{$db->getDbFilePath()}'\n" );
@@ -247,18 +249,27 @@ class UpdateMediaWiki extends Maintenance {
                ];
        }
 
+       /**
+        * @throws FatalError
+        * @throws MWException
+        * @suppress PhanPluginDuplicateConditionalNullCoalescing
+        */
        public function validateParamsAndArgs() {
                // Allow extensions to add additional params.
                $params = [];
                Hooks::run( 'MaintenanceUpdateAddParams', [ &$params ] );
+
+               // This executes before the PHP version check, so don't use null coalesce (??).
+               // Keeping this compatible with older PHP versions lets us reach the code that
+               // displays a more helpful error.
                foreach ( $params as $name => $param ) {
                        $this->addOption(
                                $name,
                                $param['desc'],
-                               $param['require'] ?? false,
-                               $param['withArg'] ?? false,
-                               $param['shortName'] ?? false,
-                               $param['multiOccurrence'] ?? false
+                               isset( $param['require'] ) ? $param['require'] : false,
+                               isset( $param['withArg'] ) ? $param['withArg'] : false,
+                               isset( $param['shortName'] ) ? $param['shortName'] : false,
+                               isset( $param['multiOccurrence'] ) ? $param['multiOccurrence'] : false
                        );
                }
 
index af2d828..0216b28 100644 (file)
@@ -61,7 +61,8 @@ class UpdateSearchIndex extends Maintenance {
        }
 
        public function execute() {
-               $posFile = $this->getOption( 'p', 'searchUpdate.' . wfWikiID() . '.pos' );
+               $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
+               $posFile = $this->getOption( 'p', 'searchUpdate.' . rawurlencode( $dbDomain ) . '.pos' );
                $end = $this->getOption( 'e', wfTimestampNow() );
                if ( $this->hasOption( 's' ) ) {
                        $start = $this->getOption( 's' );
index 2d182a6..b4258d0 100644 (file)
                </exclude>
        </groups>
        <filter>
-               <whitelist addUncoveredFilesFromWhitelist="true">
+               <whitelist addUncoveredFilesFromWhitelist="false">
                        <directory suffix=".php">includes</directory>
                        <directory suffix=".php">languages</directory>
                        <directory suffix=".php">maintenance</directory>
+                       <directory suffix=".php">extensions</directory>
+                       <directory suffix=".php">skins</directory>
                        <exclude>
                                <directory suffix=".php">languages/messages</directory>
                                <file>languages/data/normalize-ar.php</file>
index bebc172..3b99696 100644 (file)
@@ -354,7 +354,7 @@ a.new {
        font-weight: bold;
 }
 
-/* success and error messages */
+/* Error, warning and success messages */
 .error,
 .warning,
 .success {
@@ -366,11 +366,11 @@ a.new {
 }
 
 .warning {
-       color: #705000;
+       color: #ac6600;
 }
 
 .success {
-       color: #009000;
+       color: #14866d;
 }
 
 .errorbox,
@@ -380,15 +380,13 @@ a.new {
        padding: 0.5em 1em;
        margin-bottom: 1em;
        display: inline-block;
-       zoom: 1;
-       *display: inline; /* stylelint-disable-line declaration-block-no-duplicate-properties */
 }
 
 .errorbox h2,
 .warningbox h2,
 .successbox h2 {
-       font-size: 1em;
        color: inherit;
+       font-size: 1em;
        font-weight: bold;
        display: inline;
        margin: 0 0.5em 0 0;
@@ -396,26 +394,26 @@ a.new {
 }
 
 .errorbox {
-       color: #d33;
-       border-color: #fac5c5;
-       background-color: #fae3e3;
+       background-color: #fee7e6;
+       color: #000;
+       border-color: #d33;
 }
 
 .warningbox {
-       color: #705000;
-       border-color: #fde29b;
-       background-color: #fdf1d1;
+       background-color: #fef6e7;
+       color: #000;
+       border-color: #fc3;
 }
 
 .successbox {
-       color: #008000;
-       border-color: #b7fdb5;
-       background-color: #e1fddf;
+       background-color: #d5fdf4;
+       color: #000;
+       border-color: #14866d;
 }
 
 /* general info/warning box for SP */
 .mw-infobox {
-       border: 2px solid #ff7f00;
+       border: 2px solid #fc3;
        margin: 0.5em;
        clear: left;
        overflow: hidden;
index 6a75db0..c0e53a7 100644 (file)
 
 // Text colors
 @colorText: @colorGray2;
+@colorTextEmphasized: @colorGray1;
 @colorTextLight: @colorGray5;
 @colorButtonText: @colorGray2;
 @colorButtonTextHighlight: @colorGray4;
 @colorButtonTextActive: @colorGray1;
 @colorDisabledText: @colorGray7;
-@colorErrorText: #d33;
-@colorWarningText: #705000;
+
+// Messages
+// Messages: Error
+@backgroundColorError: #fee7e6;
+// Use only for inlined messages, boxed messages require `@colorTextEmphasized` for
+// minimum contrast ratio.
+@colorError: #d33;
+@borderColorError: @colorError;
+// Messages: Warning
+@backgroundColorWarning: #fef6e7;
+@colorWarning: @colorTextEmphasized;
+@borderColorWarning: #fc3;
+// Messages: Success
+@backgroundColorSuccess: #d5fdf4;
+@colorSuccess: #14866d;
+@borderColorSuccess: @colorSuccess;
+
+// FIXME: Remove after a few weeks, when extensions got updated
+@colorErrorText: @colorError;
+@colorWarningText: @colorWarning;
 
 // UI colors
 @backgroundColorInputBinaryChecked: @colorProgressive;
index 20621d2..fd65e8a 100644 (file)
 
        /**
         * @inheritdoc
+        * @param {string} subject Section title.
+        * @param {string} body Message body, as wikitext. Signature code will automatically be added unless the message already contains the string ~~~.
+        * @param {Object} [options] Message options:
+        * @param {string} [options.tags] [Change tags](https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tags) to add to the message's revision, pipe-separated.
         */
-       WikitextMessagePoster.prototype.post = function ( subject, body ) {
-               mw.messagePoster.WikitextMessagePoster.parent.prototype.post.call( this, subject, body );
+       WikitextMessagePoster.prototype.post = function ( subject, body, options ) {
+               var additionalParams;
+               mw.messagePoster.WikitextMessagePoster.parent.prototype.post.call( this, subject, body, options );
 
                // Add signature if needed
                if ( body.indexOf( '~~~' ) === -1 ) {
                        body += '\n\n~~~~';
                }
 
+               additionalParams = { redirect: true };
+               if ( options.tags !== undefined ) {
+                       additionalParams.tags = options.tags;
+               }
                return this.api.newSection(
                        this.title,
                        subject,
                        body,
-                       { redirect: true }
+                       additionalParams
                ).then( function ( resp, jqXHR ) {
                        if ( resp.edit.result === 'Success' ) {
                                return $.Deferred().resolve( resp, jqXHR );
index 7f217e9..14c971d 100644 (file)
@@ -22,6 +22,7 @@
         * @param {string} body Body, as wikitext.  Signature code will automatically be added
         *   by MessagePosters that require one, unless the message already contains the string
         *   ~~~.
+        * @param {Object} [options] Message options. See MessagePoster implementations for details.
         * @return {jQuery.Promise} Promise completing when the post succeeds or fails.
         *   For failure, will be rejected with three arguments:
         *
index d08fff5..01318c9 100644 (file)
                .box-sizing( border-box );
                font-size: 0.9em;
                margin: 0 0 1em 0;
-               padding: 0.5em;
+               padding: 0.5em 1em;
                word-wrap: break-word;
        }
 
        // Colours taken from those for .errorbox in shared.css
        .error {
-               color: @colorErrorText;
-               border: 1px solid #fac5c5;
-               background-color: #fae3e3;
+               background-color: @backgroundColorError;
+               color: @colorTextEmphasized;
+               border: 1px solid @borderColorError;
        }
 
        // Colours taken from those for .warningbox in shared.css
        .warning {
-               color: @colorWarningText;
-               border: 1px solid #fde29b;
-               background-color: #fdf1d1;
+               background-color: @backgroundColorWarning;
+               color: @colorTextEmphasized;
+               border: 1px solid @borderColorWarning;
        }
 
        // This specifies styling for individual field validation error messages.
index a42f573..d4df8ae 100644 (file)
@@ -11,6 +11,7 @@ class TestSetup {
        public static function applyInitialConfig() {
                global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgMainWANCache;
                global $wgMainStash;
+               global $wgObjectCaches;
                global $wgLanguageConverterCacheType, $wgUseDatabaseMessages;
                global $wgLocaltimezone, $wgLocalisationCacheConf;
                global $wgSearchType;
@@ -40,6 +41,8 @@ class TestSetup {
                $wgLanguageConverterCacheType = 'hash';
                // Uses db-replicated in DefaultSettings
                $wgMainStash = 'hash';
+               // Use hash instead of db
+               $wgObjectCaches['db-replicated'] = $wgObjectCaches['hash'];
                // Use memory job queue
                $wgJobTypeConf = [
                        'default' => [ 'class' => JobQueueMemory::class, 'order' => 'fifo' ],
index 7d46e83..f29b0d7 100644 (file)
@@ -279,7 +279,6 @@ class ParserTestRunner {
                $setup['wgNoFollowDomainExceptions'] = [ 'no-nofollow.org' ];
                $setup['wgExternalLinkTarget'] = false;
                $setup['wgLocaltimezone'] = 'UTC';
-               $setup['wgHtml5'] = true;
                $setup['wgDisableLangConversion'] = false;
                $setup['wgDisableTitleConversion'] = false;
 
@@ -1656,7 +1655,7 @@ class ParserTestRunner {
 
                // Wipe WANObjectCache process cache, which is invalidated by article insertion
                // due to T144706
-               ObjectCache::getMainWANInstance()->clearProcessCache();
+               MediaWikiServices::getInstance()->getMainWANObjectCache()->clearProcessCache();
 
                $this->executeSetupSnippets( $teardown );
        }
index bba9d5a..07d135d 100644 (file)
@@ -540,6 +540,11 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
 
        protected function setUp() {
                parent::setUp();
+               $reflection = new ReflectionClass( $this );
+               // TODO: Eventually we should assert for test presence in /integration/
+               if ( strpos( $reflection->getFilename(), '/unit/' ) !== false ) {
+                       $this->fail( 'This integration test should not be in "tests/phpunit/unit" !' );
+               }
                $this->called['setUp'] = true;
 
                $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
index 3f0fc7a..43a333c 100644 (file)
@@ -44,8 +44,8 @@ abstract class MediaWikiUnitTestCase extends TestCase {
                $GLOBALS = [];
                // Add back the minimal set of globals needed for unit tests to run for core +
                // extensions/skins.
-               foreach ( [ 'wgAutoloadClasses', 'wgAutoloadLocalClasses', 'IP' ] as $requiredGlobal ) {
-                       $GLOBALS[$requiredGlobal] = $this->unitGlobals[ $requiredGlobal ];
+               foreach ( $this->unitGlobals['wgPhpUnitBootstrapGlobals'] ?? [] as $key => $value ) {
+                       $GLOBALS[ $key ] = $this->unitGlobals[ $key ];
                }
        }
 
index 64693b0..2a21351 100644 (file)
@@ -101,6 +101,7 @@ class ResourceLoaderTestModule extends ResourceLoaderModule {
        protected $type = ResourceLoaderModule::LOAD_GENERAL;
        protected $targets = [ 'phpunit' ];
        protected $shouldEmbed = null;
+       protected $mayValidateScript = false;
 
        public function __construct( $options = [] ) {
                foreach ( $options as $key => $value ) {
@@ -109,7 +110,14 @@ class ResourceLoaderTestModule extends ResourceLoaderModule {
        }
 
        public function getScript( ResourceLoaderContext $context ) {
-               return $this->validateScriptFile( 'input', $this->script );
+               if ( $this->mayValidateScript ) {
+                       // This enables the validation check that replaces invalid
+                       // scripts with a warning message.
+                       // Based on $wgResourceLoaderValidateJS
+                       return $this->validateScriptFile( 'input', $this->script );
+               } else {
+                       return $this->script;
+               }
        }
 
        public function getStyles( ResourceLoaderContext $context ) {
index 10348d4..4400475 100644 (file)
@@ -56,6 +56,12 @@ $IP = realpath( __DIR__ . '/../../' );
 
 // these variables must be defined before setup runs
 $GLOBALS['IP'] = $IP;
+// Set bootstrap globals to reuse in MediaWikiUnitTestCase
+$bootstrapGlobals = [];
+foreach ( $GLOBALS as $key => $value ) {
+       $bootstrapGlobals[ $key ] = $value;
+}
+$GLOBALS['wgPhpUnitBootstrapGlobals'] = $bootstrapGlobals;
 // Faking for Setup.php
 $GLOBALS['wgScopeTest'] = 'MediaWiki Setup.php scope test';
 $GLOBALS['wgCommandLineMode'] = true;
@@ -67,6 +73,7 @@ wfRequireOnceInGlobalScope( "$IP/includes/AutoLoader.php" );
 wfRequireOnceInGlobalScope( "$IP/tests/common/TestsAutoLoader.php" );
 wfRequireOnceInGlobalScope( "$IP/includes/Defines.php" );
 wfRequireOnceInGlobalScope( "$IP/includes/DefaultSettings.php" );
+wfRequireOnceInGlobalScope( "$IP/includes/GlobalFunctions.php" );
 
 // Load extensions/skins present in filesystem so that classes can be discovered.
 $directoryToJsonMap = [
diff --git a/tests/phpunit/includes/GlobalFunctions/wfAppendQueryTest.php b/tests/phpunit/includes/GlobalFunctions/wfAppendQueryTest.php
deleted file mode 100644 (file)
index bb71610..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfAppendQuery
- */
-class WfAppendQueryTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideAppendQuery
-        */
-       public function testAppendQuery( $url, $query, $expected, $message = null ) {
-               $this->assertEquals( $expected, wfAppendQuery( $url, $query ), $message );
-       }
-
-       public static function provideAppendQuery() {
-               return [
-                       [
-                               'http://www.example.org/index.php',
-                               '',
-                               'http://www.example.org/index.php',
-                               'No query'
-                       ],
-                       [
-                               'http://www.example.org/index.php',
-                               [ 'foo' => 'bar' ],
-                               'http://www.example.org/index.php?foo=bar',
-                               'Set query array'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foz=baz',
-                               'foo=bar',
-                               'http://www.example.org/index.php?foz=baz&foo=bar',
-                               'Set query string'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               '',
-                               'http://www.example.org/index.php?foo=bar',
-                               'Empty string with query'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               [ 'baz' => 'quux' ],
-                               'http://www.example.org/index.php?foo=bar&baz=quux',
-                               'Add query array'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               'baz=quux',
-                               'http://www.example.org/index.php?foo=bar&baz=quux',
-                               'Add query string'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               [ 'baz' => 'quux', 'foo' => 'baz' ],
-                               'http://www.example.org/index.php?foo=bar&baz=quux&foo=baz',
-                               'Modify query array'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               'baz=quux&foo=baz',
-                               'http://www.example.org/index.php?foo=bar&baz=quux&foo=baz',
-                               'Modify query string'
-                       ],
-                       [
-                               'http://www.example.org/index.php#baz',
-                               'foo=bar',
-                               'http://www.example.org/index.php?foo=bar#baz',
-                               'URL with fragment'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar#baz',
-                               'quux=blah',
-                               'http://www.example.org/index.php?foo=bar&quux=blah#baz',
-                               'URL with query string and fragment'
-                       ]
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfArrayPlus2dTest.php b/tests/phpunit/includes/GlobalFunctions/wfArrayPlus2dTest.php
deleted file mode 100644 (file)
index 65b56ef..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-/**
- * @group GlobalFunctions
- * @covers ::wfArrayPlus2d
- */
-class WfArrayPlus2dTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideArrays
-        */
-       public function testWfArrayPlus2d( $baseArray, $newValues, $expected, $testName ) {
-               $this->assertEquals(
-                       $expected,
-                       wfArrayPlus2d( $baseArray, $newValues ),
-                       $testName
-               );
-       }
-
-       /**
-        * Provider for testing wfArrayPlus2d
-        *
-        * @return array
-        */
-       public static function provideArrays() {
-               return [
-                       // target array, new values array, expected result
-                       [
-                               [ 0 => '1dArray' ],
-                               [ 1 => '1dArray' ],
-                               [ 0 => '1dArray', 1 => '1dArray' ],
-                               "Test simple union of two arrays with different keys",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => '2dArray' ],
-                               ],
-                               [
-                                       0 => [ 1 => '2dArray' ],
-                               ],
-                               [
-                                       0 => [ 0 => '2dArray', 1 => '2dArray' ],
-                               ],
-                               "Test union of 2d arrays with different keys in the value array",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => '2dArray' ],
-                               ],
-                               [
-                                       0 => [ 0 => '1dArray' ],
-                               ],
-                               [
-                                       0 => [ 0 => '2dArray' ],
-                               ],
-                               "Test union of 2d arrays with same keys in the value array",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 1 => '2dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               "Test union of 3d array with different keys",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 1 => [ 0 => '2dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ], 1 => [ 0 => '2dArray' ] ],
-                               ],
-                               "Test union of 3d array with different keys in the value array",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 0 => '2dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               "Test union of 3d array with same keys in the value array",
-                       ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php
deleted file mode 100644 (file)
index 7ddad36..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-/**
- * @group GlobalFunctions
- * @covers ::wfAssembleUrl
- */
-class WfAssembleUrlTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideURLParts
-        */
-       public function testWfAssembleUrl( $parts, $output ) {
-               $partsDump = print_r( $parts, true );
-               $this->assertEquals(
-                       $output,
-                       wfAssembleUrl( $parts ),
-                       "Testing $partsDump assembles to $output"
-               );
-       }
-
-       /**
-        * Provider of URL parts for testing wfAssembleUrl()
-        *
-        * @return array
-        */
-       public static function provideURLParts() {
-               $schemes = [
-                       '' => [],
-                       '//' => [
-                               'delimiter' => '//',
-                       ],
-                       'http://' => [
-                               'scheme' => 'http',
-                               'delimiter' => '://',
-                       ],
-               ];
-
-               $hosts = [
-                       '' => [],
-                       'example.com' => [
-                               'host' => 'example.com',
-                       ],
-                       'example.com:123' => [
-                               'host' => 'example.com',
-                               'port' => 123,
-                       ],
-                       'id@example.com' => [
-                               'user' => 'id',
-                               'host' => 'example.com',
-                       ],
-                       'id@example.com:123' => [
-                               'user' => 'id',
-                               'host' => 'example.com',
-                               'port' => 123,
-                       ],
-                       'id:key@example.com' => [
-                               'user' => 'id',
-                               'pass' => 'key',
-                               'host' => 'example.com',
-                       ],
-                       'id:key@example.com:123' => [
-                               'user' => 'id',
-                               'pass' => 'key',
-                               'host' => 'example.com',
-                               'port' => 123,
-                       ],
-               ];
-
-               $cases = [];
-               foreach ( $schemes as $scheme => $schemeParts ) {
-                       foreach ( $hosts as $host => $hostParts ) {
-                               foreach ( [ '', '/path' ] as $path ) {
-                                       foreach ( [ '', 'query' ] as $query ) {
-                                               foreach ( [ '', 'fragment' ] as $fragment ) {
-                                                       $parts = array_merge(
-                                                               $schemeParts,
-                                                               $hostParts
-                                                       );
-                                                       $url = $scheme .
-                                                               $host .
-                                                               $path;
-
-                                                       if ( $path ) {
-                                                               $parts['path'] = $path;
-                                                       }
-                                                       if ( $query ) {
-                                                               $parts['query'] = $query;
-                                                               $url .= '?' . $query;
-                                                       }
-                                                       if ( $fragment ) {
-                                                               $parts['fragment'] = $fragment;
-                                                               $url .= '#' . $fragment;
-                                                       }
-
-                                                       $cases[] = [
-                                                               $parts,
-                                                               $url,
-                                                       ];
-                                               }
-                                       }
-                               }
-                       }
-               }
-
-               $complexURL = 'http://id:key@example.org:321' .
-                       '/over/there?name=ferret&foo=bar#nose';
-               $cases[] = [
-                       wfParseUrl( $complexURL ),
-                       $complexURL,
-               ];
-
-               return $cases;
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php b/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php
deleted file mode 100644 (file)
index 78e09e6..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-/**
- * @group GlobalFunctions
- * @covers ::wfBaseName
- */
-class WfBaseNameTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider providePaths
-        */
-       public function testBaseName( $fullpath, $basename ) {
-               $this->assertEquals( $basename, wfBaseName( $fullpath ),
-                       "wfBaseName('$fullpath') => '$basename'" );
-       }
-
-       public static function providePaths() {
-               return [
-                       [ '', '' ],
-                       [ '/', '' ],
-                       [ '\\', '' ],
-                       [ '//', '' ],
-                       [ '\\\\', '' ],
-                       [ 'a', 'a' ],
-                       [ 'aaaa', 'aaaa' ],
-                       [ '/a', 'a' ],
-                       [ '\\a', 'a' ],
-                       [ '/aaaa', 'aaaa' ],
-                       [ '\\aaaa', 'aaaa' ],
-                       [ '/aaaa/', 'aaaa' ],
-                       [ '\\aaaa\\', 'aaaa' ],
-                       [ '\\aaaa\\', 'aaaa' ],
-                       [
-                               '/mnt/upload3/wikipedia/en/thumb/8/8b/'
-                                       . 'Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg',
-                               '93px-Zork_Grand_Inquisitor_box_cover.jpg'
-                       ],
-                       [ 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE', 'VIEWER.EXE' ],
-                       [ 'Östergötland_coat_of_arms.png', 'Östergötland_coat_of_arms.png' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfEscapeShellArgTest.php b/tests/phpunit/includes/GlobalFunctions/wfEscapeShellArgTest.php
deleted file mode 100644 (file)
index 7402054..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfEscapeShellArg
- */
-class WfEscapeShellArgTest extends MediaWikiTestCase {
-       public function testSingleInput() {
-               if ( wfIsWindows() ) {
-                       $expected = '"blah"';
-               } else {
-                       $expected = "'blah'";
-               }
-
-               $actual = wfEscapeShellArg( 'blah' );
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-       public function testMultipleArgs() {
-               if ( wfIsWindows() ) {
-                       $expected = '"foo" "bar" "baz"';
-               } else {
-                       $expected = "'foo' 'bar' 'baz'";
-               }
-
-               $actual = wfEscapeShellArg( 'foo', 'bar', 'baz' );
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-       public function testMultipleArgsAsArray() {
-               if ( wfIsWindows() ) {
-                       $expected = '"foo" "bar" "baz"';
-               } else {
-                       $expected = "'foo' 'bar' 'baz'";
-               }
-
-               $actual = wfEscapeShellArg( [ 'foo', 'bar', 'baz' ] );
-
-               $this->assertEquals( $expected, $actual );
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php
deleted file mode 100644 (file)
index 8a7bfa5..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfGetCaller
- */
-class WfGetCallerTest extends MediaWikiTestCase {
-       public function testZero() {
-               $this->assertEquals( 'WfGetCallerTest->testZero', wfGetCaller( 1 ) );
-       }
-
-       function callerOne() {
-               return wfGetCaller();
-       }
-
-       public function testOne() {
-               $this->assertEquals( 'WfGetCallerTest->testOne', self::callerOne() );
-       }
-
-       static function intermediateFunction( $level = 2, $n = 0 ) {
-               if ( $n > 0 ) {
-                       return self::intermediateFunction( $level, $n - 1 );
-               }
-
-               return wfGetCaller( $level );
-       }
-
-       public function testTwo() {
-               $this->assertEquals( 'WfGetCallerTest->testTwo', self::intermediateFunction() );
-       }
-
-       public function testN() {
-               $this->assertEquals( 'WfGetCallerTest->testN', self::intermediateFunction( 2, 0 ) );
-               $this->assertEquals(
-                       'WfGetCallerTest::intermediateFunction',
-                       self::intermediateFunction( 1, 0 )
-               );
-
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $this->assertEquals(
-                               'WfGetCallerTest::intermediateFunction',
-                               self::intermediateFunction( $i + 1, $i )
-                       );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php b/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php
deleted file mode 100644 (file)
index eae5588..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfRemoveDotSegments
- */
-class WfRemoveDotSegmentsTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider providePaths
-        */
-       public function testWfRemoveDotSegments( $inputPath, $outputPath ) {
-               $this->assertEquals(
-                       $outputPath,
-                       wfRemoveDotSegments( $inputPath ),
-                       "Testing $inputPath expands to $outputPath"
-               );
-       }
-
-       /**
-        * Provider of URL paths for testing wfRemoveDotSegments()
-        *
-        * @return array
-        */
-       public static function providePaths() {
-               return [
-                       [ '/a/b/c/./../../g', '/a/g' ],
-                       [ 'mid/content=5/../6', 'mid/6' ],
-                       [ '/a//../b', '/a/b' ],
-                       [ '/.../a', '/.../a' ],
-                       [ '.../a', '.../a' ],
-                       [ '', '' ],
-                       [ '/', '/' ],
-                       [ '//', '//' ],
-                       [ '.', '' ],
-                       [ '..', '' ],
-                       [ '...', '...' ],
-                       [ '/.', '/' ],
-                       [ '/..', '/' ],
-                       [ './', '' ],
-                       [ '../', '' ],
-                       [ './a', 'a' ],
-                       [ '../a', 'a' ],
-                       [ '../../a', 'a' ],
-                       [ '.././a', 'a' ],
-                       [ './../a', 'a' ],
-                       [ '././a', 'a' ],
-                       [ '../../', '' ],
-                       [ '.././', '' ],
-                       [ './../', '' ],
-                       [ '././', '' ],
-                       [ '../..', '' ],
-                       [ '../.', '' ],
-                       [ './..', '' ],
-                       [ './.', '' ],
-                       [ '/../../a', '/a' ],
-                       [ '/.././a', '/a' ],
-                       [ '/./../a', '/a' ],
-                       [ '/././a', '/a' ],
-                       [ '/../../', '/' ],
-                       [ '/.././', '/' ],
-                       [ '/./../', '/' ],
-                       [ '/././', '/' ],
-                       [ '/../..', '/' ],
-                       [ '/../.', '/' ],
-                       [ '/./..', '/' ],
-                       [ '/./.', '/' ],
-                       [ 'b/../../a', '/a' ],
-                       [ 'b/.././a', '/a' ],
-                       [ 'b/./../a', '/a' ],
-                       [ 'b/././a', 'b/a' ],
-                       [ 'b/../../', '/' ],
-                       [ 'b/.././', '/' ],
-                       [ 'b/./../', '/' ],
-                       [ 'b/././', 'b/' ],
-                       [ 'b/../..', '/' ],
-                       [ 'b/../.', '/' ],
-                       [ 'b/./..', '/' ],
-                       [ 'b/./.', 'b/' ],
-                       [ '/b/../../a', '/a' ],
-                       [ '/b/.././a', '/a' ],
-                       [ '/b/./../a', '/a' ],
-                       [ '/b/././a', '/b/a' ],
-                       [ '/b/../../', '/' ],
-                       [ '/b/.././', '/' ],
-                       [ '/b/./../', '/' ],
-                       [ '/b/././', '/b/' ],
-                       [ '/b/../..', '/' ],
-                       [ '/b/../.', '/' ],
-                       [ '/b/./..', '/' ],
-                       [ '/b/./.', '/b/' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php b/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php
deleted file mode 100644 (file)
index 40b2e63..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfShorthandToInteger
- */
-class WfShorthandToIntegerTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideABunchOfShorthands
-        */
-       public function testWfShorthandToInteger( $input, $output, $description ) {
-               $this->assertEquals(
-                       wfShorthandToInteger( $input ),
-                       $output,
-                       $description
-               );
-       }
-
-       public static function provideABunchOfShorthands() {
-               return [
-                       [ '', -1, 'Empty string' ],
-                       [ '     ', -1, 'String of spaces' ],
-                       [ '1G', 1024 * 1024 * 1024, 'One gig uppercased' ],
-                       [ '1g', 1024 * 1024 * 1024, 'One gig lowercased' ],
-                       [ '1M', 1024 * 1024, 'One meg uppercased' ],
-                       [ '1m', 1024 * 1024, 'One meg lowercased' ],
-                       [ '1K', 1024, 'One kb uppercased' ],
-                       [ '1k', 1024, 'One kb lowercased' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php b/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php
deleted file mode 100644 (file)
index 7f56b60..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfStringToBool
- */
-class WfStringToBoolTest extends MediaWikiTestCase {
-
-       public function getTestCases() {
-               return [
-                       [ 'true', true ],
-                       [ 'on', true ],
-                       [ 'yes', true ],
-                       [ 'TRUE', true ],
-                       [ 'YeS', true ],
-                       [ 'On', true ],
-                       [ '1', true ],
-                       [ '+1', true ],
-                       [ '01', true ],
-                       [ '-001', true ],
-                       [ '  1', true ],
-                       [ '-1  ', true ],
-                       [ '', false ],
-                       [ '0', false ],
-                       [ 'false', false ],
-                       [ 'NO', false ],
-                       [ 'NOT', false ],
-                       [ 'never', false ],
-                       [ '!&', false ],
-                       [ '-0', false ],
-                       [ '+0', false ],
-                       [ 'forget about it', false ],
-                       [ ' on', false ],
-                       [ 'true ', false ],
-               ];
-       }
-
-       /**
-        * @dataProvider getTestCases
-        * @param string $str
-        * @param bool $bool
-        */
-       public function testStr2Bool( $str, $bool ) {
-               if ( $bool ) {
-                       $this->assertTrue( wfStringToBool( $str ) );
-               } else {
-                       $this->assertFalse( wfStringToBool( $str ) );
-               }
-       }
-
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php b/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php
deleted file mode 100644 (file)
index a70f136..0000000
+++ /dev/null
@@ -1,194 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfTimestamp
- */
-class WfTimestampTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideNormalTimestamps
-        */
-       public function testNormalTimestamps( $input, $format, $output, $desc ) {
-               $this->assertEquals( $output, wfTimestamp( $format, $input ), $desc );
-       }
-
-       public static function provideNormalTimestamps() {
-               $t = gmmktime( 12, 34, 56, 1, 15, 2001 );
-
-               return [
-                       // TS_UNIX
-                       [ $t, TS_MW, '20010115123456', 'TS_UNIX to TS_MW' ],
-                       [ -30281104, TS_MW, '19690115123456', 'Negative TS_UNIX to TS_MW' ],
-                       [ $t, TS_UNIX, 979562096, 'TS_UNIX to TS_UNIX' ],
-                       [ $t, TS_DB, '2001-01-15 12:34:56', 'TS_UNIX to TS_DB' ],
-                       [ $t + 0.01, TS_MW, '20010115123456', 'TS_UNIX float to TS_MW' ],
-
-                       [ $t, TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_ISO_8601_BASIC to TS_DB' ],
-
-                       // TS_MW
-                       [ '20010115123456', TS_MW, '20010115123456', 'TS_MW to TS_MW' ],
-                       [ '20010115123456', TS_UNIX, 979562096, 'TS_MW to TS_UNIX' ],
-                       [ '20010115123456', TS_DB, '2001-01-15 12:34:56', 'TS_MW to TS_DB' ],
-                       [ '20010115123456', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_MW to TS_ISO_8601_BASIC' ],
-
-                       // TS_DB
-                       [ '2001-01-15 12:34:56', TS_MW, '20010115123456', 'TS_DB to TS_MW' ],
-                       [ '2001-01-15 12:34:56', TS_UNIX, 979562096, 'TS_DB to TS_UNIX' ],
-                       [ '2001-01-15 12:34:56', TS_DB, '2001-01-15 12:34:56', 'TS_DB to TS_DB' ],
-                       [
-                               '2001-01-15 12:34:56',
-                               TS_ISO_8601_BASIC,
-                               '20010115T123456Z',
-                               'TS_DB to TS_ISO_8601_BASIC'
-                       ],
-
-                       # rfc2822 section 3.3
-                       [ '20010115123456', TS_RFC2822, 'Mon, 15 Jan 2001 12:34:56 GMT', 'TS_MW to TS_RFC2822' ],
-                       [ 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ],
-                       [
-                               ' Mon, 15 Jan 2001 12:34:56 GMT',
-                               TS_MW,
-                               '20010115123456',
-                               'TS_RFC2822 with leading space to TS_MW'
-                       ],
-                       [
-                               '15 Jan 2001 12:34:56 GMT',
-                               TS_MW,
-                               '20010115123456',
-                               'TS_RFC2822 without optional day-of-week to TS_MW'
-                       ],
-
-                       # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space
-                       # obs-FWS = 1*WSP *(CRLF 1*WSP) ; Section 4.2
-                       [ 'Mon, 15         Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ],
-
-                       # WSP = SP / HTAB ; rfc2234
-                       [
-                               "Mon, 15 Jan\x092001 12:34:56 GMT",
-                               TS_MW,
-                               '20010115123456',
-                               'TS_RFC2822 with HTAB to TS_MW'
-                       ],
-                       [
-                               "Mon, 15 Jan\x09 \x09  2001 12:34:56 GMT",
-                               TS_MW,
-                               '20010115123456',
-                               'TS_RFC2822 with HTAB and SP to TS_MW'
-                       ],
-                       [
-                               'Sun, 6 Nov 94 08:49:37 GMT',
-                               TS_MW,
-                               '19941106084937',
-                               'TS_RFC2822 with obsolete year to TS_MW'
-                       ],
-               ];
-       }
-
-       /**
-        * This test checks wfTimestamp() with values outside.
-        * It needs PHP 64 bits or PHP > 5.1.
-        * See r74778 and T27451
-        * @dataProvider provideOldTimestamps
-        */
-       public function testOldTimestamps( $input, $outputType, $output, $message ) {
-               $timestamp = wfTimestamp( $outputType, $input );
-               if ( substr( $output, 0, 1 ) === '/' ) {
-                       // T66946: Day of the week calculations for very old
-                       // timestamps varies from system to system.
-                       $this->assertRegExp( $output, $timestamp, $message );
-               } else {
-                       $this->assertEquals( $output, $timestamp, $message );
-               }
-       }
-
-       public static function provideOldTimestamps() {
-               return [
-                       [
-                               '19011213204554',
-                               TS_RFC2822,
-                               'Fri, 13 Dec 1901 20:45:54 GMT',
-                               'Earliest time according to PHP documentation'
-                       ],
-                       [ '20380119031407', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:07 GMT', 'Latest 32 bit time' ],
-                       [ '19011213204552', TS_UNIX, '-2147483648', 'Earliest 32 bit unix time' ],
-                       [ '20380119031407', TS_UNIX, '2147483647', 'Latest 32 bit unix time' ],
-                       [ '19011213204552', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:52 GMT', 'Earliest 32 bit time' ],
-                       [
-                               '19011213204551',
-                               TS_RFC2822,
-                               'Fri, 13 Dec 1901 20:45:51 GMT', 'Earliest 32 bit time - 1'
-                       ],
-                       [ '20380119031408', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:08 GMT', 'Latest 32 bit time + 1' ],
-                       [ '19011212000000', TS_MW, '19011212000000', 'Convert to itself r74778#c10645' ],
-                       [ '19011213204551', TS_UNIX, '-2147483649', 'Earliest 32 bit unix time - 1' ],
-                       [ '20380119031408', TS_UNIX, '2147483648', 'Latest 32 bit unix time + 1' ],
-                       [ '-2147483649', TS_MW, '19011213204551', '1901 negative unix time to MediaWiki' ],
-                       [ '-5331871504', TS_MW, '18010115123456', '1801 negative unix time to MediaWiki' ],
-                       [
-                               '0117-08-09 12:34:56',
-                               TS_RFC2822,
-                               '/, 09 Aug 0117 12:34:56 GMT$/',
-                               'Death of Roman Emperor [[Trajan]]'
-                       ],
-
-                       /* @todo FIXME: 00 to 101 years are taken as being in [1970-2069] */
-                       [ '-58979923200', TS_RFC2822, '/, 01 Jan 0101 00:00:00 GMT$/', '1/1/101' ],
-                       [ '-62135596800', TS_RFC2822, 'Mon, 01 Jan 0001 00:00:00 GMT', 'Year 1' ],
-
-                       /* It is not clear if we should generate a year 0 or not
-                        * We are completely off RFC2822 requirement of year being
-                        * 1900 or later.
-                        */
-                       [
-                               '-62142076800',
-                               TS_RFC2822,
-                               'Wed, 18 Oct 0000 00:00:00 GMT',
-                               'ISO 8601:2004 [[year 0]], also called [[1 BC]]'
-                       ],
-               ];
-       }
-
-       /**
-        * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
-        * @dataProvider provideHttpDates
-        */
-       public function testHttpDate( $input, $output, $desc ) {
-               $this->assertEquals( $output, wfTimestamp( TS_MW, $input ), $desc );
-       }
-
-       public static function provideHttpDates() {
-               return [
-                       [ 'Sun, 06 Nov 1994 08:49:37 GMT', '19941106084937', 'RFC 822 date' ],
-                       [ 'Sunday, 06-Nov-94 08:49:37 GMT', '19941106084937', 'RFC 850 date' ],
-                       [ 'Sun Nov  6 08:49:37 1994', '19941106084937', "ANSI C's asctime() format" ],
-                       // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171
-                       [
-                               'Mon, 22 Nov 2010 14:12:42 GMT; length=52626',
-                               '20101122141242',
-                               'Netscape extension to HTTP/1.0'
-                       ],
-               ];
-       }
-
-       /**
-        * There are a number of assumptions in our codebase where wfTimestamp()
-        * should give the current date but it is not given a 0 there. See r71751 CR
-        */
-       public function testTimestampParameter() {
-               $now = wfTimestamp( TS_UNIX );
-               // We check that wfTimestamp doesn't return false (error) and use a LessThan assert
-               // for the cases where the test is run in a second boundary.
-
-               $zero = wfTimestamp( TS_UNIX, 0 );
-               $this->assertNotEquals( false, $zero );
-               $this->assertLessThan( 5, $zero - $now );
-
-               $empty = wfTimestamp( TS_UNIX, '' );
-               $this->assertNotEquals( false, $empty );
-               $this->assertLessThan( 5, $empty - $now );
-
-               $null = wfTimestamp( TS_UNIX, null );
-               $this->assertNotEquals( false, $null );
-               $this->assertLessThan( 5, $null - $now );
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
deleted file mode 100644 (file)
index f9735c1..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-/**
- * The function only need a string parameter and might react to IIS7.0
- *
- * @group GlobalFunctions
- * @covers ::wfUrlencode
- */
-class WfUrlencodeTest extends MediaWikiTestCase {
-       # ### TESTS ##############################################################
-
-       /**
-        * @dataProvider provideURLS
-        */
-       public function testEncodingUrlWith( $input, $expected ) {
-               $this->verifyEncodingFor( 'Apache', $input, $expected );
-       }
-
-       /**
-        * @dataProvider provideURLS
-        */
-       public function testEncodingUrlWithMicrosoftIis7( $input, $expected ) {
-               $this->verifyEncodingFor( 'Microsoft-IIS/7', $input, $expected );
-       }
-
-       # ### HELPERS #############################################################
-
-       /**
-        * Internal helper that actually run the test.
-        * Called by the public methods testEncodingUrlWith...()
-        */
-       private function verifyEncodingFor( $server, $input, $expectations ) {
-               $expected = $this->extractExpect( $server, $expectations );
-
-               // save up global
-               $old = $_SERVER['SERVER_SOFTWARE'] ?? null;
-               $_SERVER['SERVER_SOFTWARE'] = $server;
-               wfUrlencode( null );
-
-               // do the requested test
-               $this->assertEquals(
-                       $expected,
-                       wfUrlencode( $input ),
-                       "Encoding '$input' for server '$server' should be '$expected'"
-               );
-
-               // restore global
-               if ( $old === null ) {
-                       unset( $_SERVER['SERVER_SOFTWARE'] );
-               } else {
-                       $_SERVER['SERVER_SOFTWARE'] = $old;
-               }
-               wfUrlencode( null );
-       }
-
-       /**
-        * Interprets the provider array. Return expected value depending
-        * the HTTP server name.
-        */
-       private function extractExpect( $server, $expectations ) {
-               if ( is_string( $expectations ) ) {
-                       return $expectations;
-               } elseif ( is_array( $expectations ) ) {
-                       if ( !array_key_exists( $server, $expectations ) ) {
-                               throw new MWException( __METHOD__ . " expectation does not have any "
-                                       . "value for server name $server. Check the provider array.\n" );
-                       } else {
-                               return $expectations[$server];
-                       }
-               } else {
-                       throw new MWException( __METHOD__ . " given invalid expectation for "
-                               . "'$server'. Should be a string or an array [ <http server name> => <string> ].\n" );
-               }
-       }
-
-       # ### PROVIDERS ###########################################################
-
-       /**
-        * Format is either:
-        *   [ 'input', 'expected' ];
-        * Or:
-        *   [ 'input',
-        *       [ 'Apache', 'expected' ],
-        *       [ 'Microsoft-IIS/7', 'expected' ],
-        *   ],
-        * If you want to add other HTTP server name, you will have to add a new
-        * testing method much like the testEncodingUrlWith() method above.
-        */
-       public static function provideURLS() {
-               return [
-                       # ## RFC 1738 chars
-                       // + is not safe
-                       [ '+', '%2B' ],
-                       // & and = not safe in queries
-                       [ '&', '%26' ],
-                       [ '=', '%3D' ],
-
-                       [ ':', [
-                               'Apache' => ':',
-                               'Microsoft-IIS/7' => '%3A',
-                       ] ],
-
-                       // remaining chars do not need encoding
-                       [
-                               ';@$-_.!*',
-                               ';@$-_.!*',
-                       ],
-
-                       # ## Other tests
-                       // slash remain unchanged. %2F seems to break things
-                       [ '/', '/' ],
-                       // T105265
-                       [ '~', '~' ],
-
-                       // Other 'funnies' chars
-                       [ '[]', '%5B%5D' ],
-                       [ '<>', '%3C%3E' ],
-
-                       // Apostrophe is encoded
-                       [ '\'', '%27' ],
-               ];
-       }
-}
index 448eec8..00b8d18 100644 (file)
@@ -2537,35 +2537,42 @@ class OutputPageTest extends MediaWikiTestCase {
                $rl = $out->getResourceLoader();
                $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
                $rl->register( [
-                       'test.foo' => new ResourceLoaderTestModule( [
+                       'test.foo' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.test.foo( { a: true } );',
                                'styles' => '.mw-test-foo { content: "style"; }',
-                       ] ),
-                       'test.bar' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.bar' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.test.bar( { a: true } );',
                                'styles' => '.mw-test-bar { content: "style"; }',
-                       ] ),
-                       'test.baz' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.baz' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.test.baz( { a: true } );',
                                'styles' => '.mw-test-baz { content: "style"; }',
-                       ] ),
-                       'test.quux' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.quux' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.test.baz( { token: 123 } );',
                                'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
                                'group' => 'private',
-                       ] ),
-                       'test.noscript' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.noscript' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'styles' => '.stuff { color: red; }',
                                'group' => 'noscript',
-                       ] ),
-                       'test.group.foo' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.group.foo' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.doStuff( "foo" );',
                                'group' => 'foo',
-                       ] ),
-                       'test.group.bar' => new ResourceLoaderTestModule( [
+                       ],
+                       'test.group.bar' => [
+                               'class' => ResourceLoaderTestModule::class,
                                'script' => 'mw.doStuff( "bar" );',
                                'group' => 'bar',
-                       ] ),
+                       ],
                ] );
                $links = $method->invokeArgs( $out, $args );
                $actualHtml = strval( $links );
@@ -2648,17 +2655,16 @@ class OutputPageTest extends MediaWikiTestCase {
                        ->setConstructorArgs( [ $ctx ] )
                        ->setMethods( [ 'buildCssLinksArray' ] )
                        ->getMock();
-               $op->expects( $this->any() )
-                       ->method( 'buildCssLinksArray' )
+               $op->method( 'buildCssLinksArray' )
                        ->willReturn( [] );
                $rl = $op->getResourceLoader();
                $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
 
                // Register custom modules
                $rl->register( [
-                       'example.site.a' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
-                       'example.site.b' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
-                       'example.user' => new ResourceLoaderTestModule( [ 'group' => 'user' ] ),
+                       'example.site.a' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'site' ],
+                       'example.site.b' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'site' ],
+                       'example.user' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'user' ],
                ] );
 
                $op = TestingAccessWrapper::newFromObject( $op );
diff --git a/tests/phpunit/includes/PathRouterTest.php b/tests/phpunit/includes/PathRouterTest.php
deleted file mode 100644 (file)
index d891675..0000000
+++ /dev/null
@@ -1,325 +0,0 @@
-<?php
-
-/**
- * Tests for the PathRouter parsing.
- *
- * @covers PathRouter
- */
-class PathRouterTest extends MediaWikiTestCase {
-
-       /**
-        * @var PathRouter
-        */
-       protected $basicRouter;
-
-       protected function setUp() {
-               parent::setUp();
-               $router = new PathRouter;
-               $router->add( "/wiki/$1" );
-               $this->basicRouter = $router;
-       }
-
-       public static function provideParse() {
-               $tests = [
-                       // Basic path parsing
-                       'Basic path parsing' => [
-                               "/wiki/$1",
-                               "/wiki/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       //
-                       'Loose path auto-$1: /$1' => [
-                               "/",
-                               "/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       'Loose path auto-$1: /wiki' => [
-                               "/wiki",
-                               "/wiki/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       'Loose path auto-$1: /wiki/' => [
-                               "/wiki/",
-                               "/wiki/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       // Ensure that path is based on specificity, not order
-                       'Order, /$1 added first' => [
-                               [ "/$1", "/a/$1", "/b/$1" ],
-                               "/a/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       'Order, /$1 added last' => [
-                               [ "/b/$1", "/a/$1", "/$1" ],
-                               "/a/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       // Handling of key based arrays with a url parameter
-                       'Key based array' => [
-                               [ [
-                                       'path' => [ 'edit' => "/edit/$1" ],
-                                       'params' => [ 'action' => '$key' ],
-                               ] ],
-                               "/edit/Foo",
-                               [ 'title' => "Foo", 'action' => 'edit' ]
-                       ],
-                       // Additional parameter
-                       'Basic $2' => [
-                               [ [
-                                       'path' => '/$2/$1',
-                                       'params' => [ 'test' => '$2' ]
-                               ] ],
-                               "/asdf/Foo",
-                               [ 'title' => "Foo", 'test' => 'asdf' ]
-                       ],
-               ];
-               // Shared patterns for restricted value parameter tests
-               $restrictedPatterns = [
-                       [
-                               'path' => '/$2/$1',
-                               'params' => [ 'test' => '$2' ],
-                               'options' => [ '$2' => [ 'a', 'b' ] ]
-                       ],
-                       [
-                               'path' => '/$2/$1',
-                               'params' => [ 'test2' => '$2' ],
-                               'options' => [ '$2' => 'c' ]
-                       ],
-                       '/$1'
-               ];
-               $tests += [
-                       // Restricted value parameter tests
-                       'Restricted 1' => [
-                               $restrictedPatterns,
-                               "/asdf/Foo",
-                               [ 'title' => "asdf/Foo" ]
-                       ],
-                       'Restricted 2' => [
-                               $restrictedPatterns,
-                               "/a/Foo",
-                               [ 'title' => "Foo", 'test' => 'a' ]
-                       ],
-                       'Restricted 3' => [
-                               $restrictedPatterns,
-                               "/c/Foo",
-                               [ 'title' => "Foo", 'test2' => 'c' ]
-                       ],
-
-                       // Callback test
-                       'Callback' => [
-                               [ [
-                                       'path' => "/$1",
-                                       'params' => [ 'a' => 'b', 'data:foo' => 'bar' ],
-                                       'options' => [ 'callback' => [ __CLASS__, 'callbackForTest' ] ]
-                               ] ],
-                               '/Foo',
-                               [
-                                       'title' => "Foo",
-                                       'x' => 'Foo',
-                                       'a' => 'b',
-                                       'foo' => 'bar'
-                               ]
-                       ],
-
-                       // Test to ensure that matches are not made if a parameter expects nonexistent input
-                       'Fail' => [
-                               [ [
-                                       'path' => "/wiki/$1",
-                                       'params' => [ 'title' => "$1$2" ],
-                               ] ],
-                               "/wiki/A",
-                               []
-                       ],
-
-                       // Make sure the router handles titles like Special:Recentchanges correctly
-                       'Special title' => [
-                               "/wiki/$1",
-                               "/wiki/Special:Recentchanges",
-                               [ 'title' => "Special:Recentchanges" ]
-                       ],
-
-                       // Make sure the router decodes urlencoding properly
-                       'URL encoding' => [
-                               "/wiki/$1",
-                               "/wiki/Title_With%20Space",
-                               [ 'title' => "Title_With Space" ]
-                       ],
-
-                       // Double slash and dot expansion
-                       'Double slash in prefix' => [
-                               '/wiki/$1',
-                               '//wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-                       'Double slash at start of $1' => [
-                               '/wiki/$1',
-                               '/wiki//Foo',
-                               [ 'title' => '/Foo' ]
-                       ],
-                       'Double slash in middle of $1' => [
-                               '/wiki/$1',
-                               '/wiki/.hack//SIGN',
-                               [ 'title' => '.hack//SIGN' ]
-                       ],
-                       'Dots removed 1' => [
-                               '/wiki/$1',
-                               '/x/../wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-                       'Dots removed 2' => [
-                               '/wiki/$1',
-                               '/./wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-                       'Dots retained 1' => [
-                               '/wiki/$1',
-                               '/wiki/../wiki/Foo',
-                               [ 'title' => '../wiki/Foo' ]
-                       ],
-                       'Dots retained 2' => [
-                               '/wiki/$1',
-                               '/wiki/./Foo',
-                               [ 'title' => './Foo' ]
-                       ],
-                       'Triple slash' => [
-                               '/wiki/$1',
-                               '///wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-                       // '..' only traverses one slash, see e.g. RFC 3986
-                       'Dots traversing double slash 1' => [
-                               '/wiki/$1',
-                               '/a//b/../../wiki/Foo',
-                               []
-                       ],
-                       'Dots traversing double slash 2' => [
-                               '/wiki/$1',
-                               '/a//b/../../../wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-               ];
-
-               // Make sure the router doesn't break on special characters like $ used in regexp replacements
-               foreach ( [ "$", "$1", "\\", "\\$1" ] as $char ) {
-                       $tests["Regexp character $char"] = [
-                               "/wiki/$1",
-                               "/wiki/$char",
-                               [ 'title' => "$char" ]
-                       ];
-               }
-
-               $tests += [
-                       // Make sure the router handles characters like +&() properly
-                       "Special characters" => [
-                               "/wiki/$1",
-                               "/wiki/Plus+And&Dollar\\Stuff();[]{}*",
-                               [ 'title' => "Plus+And&Dollar\\Stuff();[]{}*" ],
-                       ],
-
-                       // Make sure the router handles unicode characters correctly
-                       "Unicode 1" => [
-                               "/wiki/$1",
-                               "/wiki/Spécial:Modifications_récentes" ,
-                               [ 'title' => "Spécial:Modifications_récentes" ],
-                       ],
-
-                       "Unicode 2" => [
-                               "/wiki/$1",
-                               "/wiki/Sp%C3%A9cial:Modifications_r%C3%A9centes",
-                               [ 'title' => "Spécial:Modifications_récentes" ],
-                       ]
-               ];
-
-               // Ensure the router doesn't choke on long paths.
-               $lorem = "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_" .
-                       "tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_" .
-                        "nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._" .
-                        "Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_" .
-                        "eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_" .
-                        "in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum.";
-
-               $tests += [
-                       "Long path" => [
-                               "/wiki/$1",
-                               "/wiki/$lorem",
-                               [ 'title' => $lorem ]
-                       ],
-
-                       // Ensure that the php passed site of parameter values are not urldecoded
-                       "Pattern urlencoding" => [
-                               [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => '%20:$1' ] ] ],
-                               "/wiki/Foo",
-                               [ 'title' => '%20:Foo' ]
-                       ],
-
-                       // Ensure that raw parameter values do not have any variable replacements or urldecoding
-                       "Raw param value" => [
-                               [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => [ 'value' => 'bar%20$1' ] ] ] ],
-                               "/wiki/Foo",
-                               [ 'title' => 'bar%20$1' ]
-                       ]
-               ];
-
-               return $tests;
-       }
-
-       /**
-        * Test path parsing
-        * @dataProvider provideParse
-        */
-       public function testParse( $patterns, $path, $expected ) {
-               $patterns = (array)$patterns;
-
-               $router = new PathRouter;
-               foreach ( $patterns as $pattern ) {
-                       if ( is_array( $pattern ) ) {
-                               $router->add( $pattern['path'], $pattern['params'] ?? [],
-                                       $pattern['options'] ?? [] );
-                       } else {
-                               $router->add( $pattern );
-                       }
-               }
-               $matches = $router->parse( $path );
-               $this->assertEquals( $matches, $expected );
-       }
-
-       public static function callbackForTest( &$matches, $data ) {
-               $matches['x'] = $data['$1'];
-               $matches['foo'] = $data['foo'];
-       }
-
-       public static function provideWeight() {
-               return [
-                       [ '/Foo', [ 'title' => 'Foo' ] ],
-                       [ '/Bar', [ 'ping' => 'pong' ] ],
-                       [ '/Baz', [ 'marco' => 'polo' ] ],
-                       [ '/asdf-foo', [ 'title' => 'qwerty-foo' ] ],
-                       [ '/qwerty-bar', [ 'title' => 'asdf-bar' ] ],
-                       [ '/a/Foo', [ 'title' => 'Foo' ] ],
-                       [ '/asdf/Foo', [ 'title' => 'Foo' ] ],
-                       [ '/qwerty/Foo', [ 'title' => 'Foo', 'qwerty' => 'qwerty' ] ],
-                       [ '/baz/Foo', [ 'title' => 'Foo', 'unrestricted' => 'baz' ] ],
-                       [ '/y/Foo', [ 'title' => 'Foo', 'restricted-to-y' => 'y' ] ],
-               ];
-       }
-
-       /**
-        * Test to ensure weight of paths is handled correctly
-        * @dataProvider provideWeight
-        */
-       public function testWeight( $path, $expected ) {
-               $router = new PathRouter;
-               $router->addStrict( "/Bar", [ 'ping' => 'pong' ] );
-               $router->add( "/asdf-$1", [ 'title' => 'qwerty-$1' ] );
-               $router->add( "/$1" );
-               $router->add( "/qwerty-$1", [ 'title' => 'asdf-$1' ] );
-               $router->addStrict( "/Baz", [ 'marco' => 'polo' ] );
-               $router->add( "/a/$1" );
-               $router->add( "/asdf/$1" );
-               $router->add( "/$2/$1", [ 'unrestricted' => '$2' ] );
-               $router->add( [ 'qwerty' => "/qwerty/$1" ], [ 'qwerty' => '$key' ] );
-               $router->add( "/$2/$1", [ 'restricted-to-y' => '$2' ], [ '$2' => 'y' ] );
-
-               $this->assertEquals( $router->parse( $path ), $expected );
-       }
-}
diff --git a/tests/phpunit/includes/Rest/ResponseFactoryTest.php b/tests/phpunit/includes/Rest/ResponseFactoryTest.php
deleted file mode 100644 (file)
index ae71272..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Rest;
-
-use ArrayIterator;
-use MediaWiki\Rest\HttpException;
-use MediaWiki\Rest\ResponseFactory;
-use MediaWikiTestCase;
-
-/** @covers \MediaWiki\Rest\ResponseFactory */
-class ResponseFactoryTest extends MediaWikiTestCase {
-       public static function provideEncodeJson() {
-               return [
-                       [ (object)[], '{}' ],
-                       [ '/', '"/"' ],
-                       [ '£', '"£"' ],
-                       [ [], '[]' ],
-               ];
-       }
-
-       /** @dataProvider provideEncodeJson */
-       public function testEncodeJson( $input, $expected ) {
-               $rf = new ResponseFactory;
-               $this->assertSame( $expected, $rf->encodeJson( $input ) );
-       }
-
-       public function testCreateJson() {
-               $rf = new ResponseFactory;
-               $response = $rf->createJson( [] );
-               $response->getBody()->rewind();
-               $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) );
-               $this->assertSame( '[]', $response->getBody()->getContents() );
-               // Make sure getSize() is functional, since testCreateNoContent() depends on it
-               $this->assertSame( 2, $response->getBody()->getSize() );
-       }
-
-       public function testCreateNoContent() {
-               $rf = new ResponseFactory;
-               $response = $rf->createNoContent();
-               $this->assertSame( [], $response->getHeader( 'Content-Type' ) );
-               $this->assertSame( 0, $response->getBody()->getSize() );
-               $this->assertSame( 204, $response->getStatusCode() );
-       }
-
-       public function testCreatePermanentRedirect() {
-               $rf = new ResponseFactory;
-               $response = $rf->createPermanentRedirect( 'http://www.example.com/' );
-               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
-               $this->assertSame( 301, $response->getStatusCode() );
-       }
-
-       public function testCreateLegacyTemporaryRedirect() {
-               $rf = new ResponseFactory;
-               $response = $rf->createLegacyTemporaryRedirect( 'http://www.example.com/' );
-               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
-               $this->assertSame( 302, $response->getStatusCode() );
-       }
-
-       public function testCreateTemporaryRedirect() {
-               $rf = new ResponseFactory;
-               $response = $rf->createTemporaryRedirect( 'http://www.example.com/' );
-               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
-               $this->assertSame( 307, $response->getStatusCode() );
-       }
-
-       public function testCreateSeeOther() {
-               $rf = new ResponseFactory;
-               $response = $rf->createSeeOther( 'http://www.example.com/' );
-               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
-               $this->assertSame( 303, $response->getStatusCode() );
-       }
-
-       public function testCreateNotModified() {
-               $rf = new ResponseFactory;
-               $response = $rf->createNotModified();
-               $this->assertSame( 0, $response->getBody()->getSize() );
-               $this->assertSame( 304, $response->getStatusCode() );
-       }
-
-       /** @expectedException \InvalidArgumentException */
-       public function testCreateHttpErrorInvalid() {
-               $rf = new ResponseFactory;
-               $rf->createHttpError( 200 );
-       }
-
-       public function testCreateHttpError() {
-               $rf = new ResponseFactory;
-               $response = $rf->createHttpError( 415, [ 'message' => '...' ] );
-               $this->assertSame( 415, $response->getStatusCode() );
-               $body = $response->getBody();
-               $body->rewind();
-               $data = json_decode( $body->getContents(), true );
-               $this->assertSame( 415, $data['httpCode'] );
-               $this->assertSame( '...', $data['message'] );
-       }
-
-       public function testCreateFromExceptionUnlogged() {
-               $rf = new ResponseFactory;
-               $response = $rf->createFromException( new HttpException( 'hello', 415 ) );
-               $this->assertSame( 415, $response->getStatusCode() );
-               $body = $response->getBody();
-               $body->rewind();
-               $data = json_decode( $body->getContents(), true );
-               $this->assertSame( 415, $data['httpCode'] );
-               $this->assertSame( 'hello', $data['message'] );
-       }
-
-       public function testCreateFromExceptionLogged() {
-               $rf = new ResponseFactory;
-               $response = $rf->createFromException( new \Exception( "hello", 415 ) );
-               $this->assertSame( 500, $response->getStatusCode() );
-               $body = $response->getBody();
-               $body->rewind();
-               $data = json_decode( $body->getContents(), true );
-               $this->assertSame( 500, $data['httpCode'] );
-               $this->assertSame( 'Error: exception of type Exception', $data['message'] );
-       }
-
-       public static function provideCreateFromReturnValue() {
-               return [
-                       [ 'hello', '{"value":"hello"}' ],
-                       [ true, '{"value":true}' ],
-                       [ [ 'x' => 'y' ], '{"x":"y"}' ],
-                       [ [ 'x', 'y' ], '["x","y"]' ],
-                       [ [ 'a', 'x' => 'y' ], '{"0":"a","x":"y"}' ],
-                       [ (object)[ 'a', 'x' => 'y' ], '{"0":"a","x":"y"}' ],
-                       [ [], '[]' ],
-                       [ (object)[], '{}' ],
-               ];
-       }
-
-       /** @dataProvider provideCreateFromReturnValue */
-       public function testCreateFromReturnValue( $input, $expected ) {
-               $rf = new ResponseFactory;
-               $response = $rf->createFromReturnValue( $input );
-               $body = $response->getBody();
-               $body->rewind();
-               $this->assertSame( $expected, $body->getContents() );
-       }
-
-       /** @expectedException \InvalidArgumentException */
-       public function testCreateFromReturnValueInvalid() {
-               $rf = new ResponseFactory;
-               $rf->createFromReturnValue( new ArrayIterator );
-       }
-}
diff --git a/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php
deleted file mode 100644 (file)
index 5e32574..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use MediaWiki\Revision\MainSlotRoleHandler;
-use MediaWikiTestCase;
-use PHPUnit\Framework\MockObject\MockObject;
-use Title;
-
-/**
- * @covers \MediaWiki\Revision\MainSlotRoleHandler
- */
-class MainSlotRoleHandlerTest extends MediaWikiTestCase {
-
-       private function makeTitleObject( $ns ) {
-               /** @var Title|MockObject $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $title->method( 'getNamespace' )
-                       ->willReturn( $ns );
-
-               return $title;
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
-        */
-       public function testConstruction() {
-               $handler = new MainSlotRoleHandler( [] );
-               $this->assertSame( 'main', $handler->getRole() );
-               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
-
-               $hints = $handler->getOutputLayoutHints();
-               $this->assertArrayHasKey( 'display', $hints );
-               $this->assertArrayHasKey( 'region', $hints );
-               $this->assertArrayHasKey( 'placement', $hints );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
-        */
-       public function testFetDefaultModel() {
-               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
-
-               // For the main handler, the namespace determins the default model
-               $titleMain = $this->makeTitleObject( NS_MAIN );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
-
-               $title100 = $this->makeTitleObject( 100 );
-               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedModel() {
-               $handler = new MainSlotRoleHandler( [] );
-
-               // For the main handler, (nearly) all models are allowed
-               $title = $this->makeTitleObject( NS_MAIN );
-               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
-               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
-        */
-       public function testSupportsArticleCount() {
-               $handler = new MainSlotRoleHandler( [] );
-
-               $this->assertTrue( $handler->supportsArticleCount() );
-       }
-
-}
index 033e2fe..d4393dd 100644 (file)
@@ -185,7 +185,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideDomainCheck
-        * @covers \MediaWiki\Revision\RevisionStore::checkDatabaseWikiId
+        * @covers \MediaWiki\Revision\RevisionStore::checkDatabaseDomain
         */
        public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) {
                $this->setMwGlobals(
diff --git a/tests/phpunit/includes/Revision/SlotRecordTest.php b/tests/phpunit/includes/Revision/SlotRecordTest.php
deleted file mode 100644 (file)
index 6495967..0000000
+++ /dev/null
@@ -1,416 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use InvalidArgumentException;
-use LogicException;
-use MediaWiki\Revision\IncompleteRevisionException;
-use MediaWiki\Revision\SlotRecord;
-use MediaWiki\Revision\SuppressedDataException;
-use MediaWikiTestCase;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Revision\SlotRecord
- */
-class SlotRecordTest extends MediaWikiTestCase {
-
-       private function makeRow( $data = [] ) {
-               $data = $data + [
-                       'slot_id' => 1234,
-                       'slot_content_id' => 33,
-                       'content_size' => '5',
-                       'content_sha1' => 'someHash',
-                       'content_address' => 'tt:456',
-                       'model_name' => CONTENT_MODEL_WIKITEXT,
-                       'format_name' => CONTENT_FORMAT_WIKITEXT,
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '1',
-                       'role_name' => 'myRole',
-               ];
-               return (object)$data;
-       }
-
-       public function testCompleteConstruction() {
-               $row = $this->makeRow();
-               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasContentId() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertTrue( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 5, $record->getSize() );
-               $this->assertSame( 'someHash', $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 1, $record->getOrigin() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( 33, $record->getContentId() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testConstructionDeferred() {
-               $row = $this->makeRow( [
-                       'content_size' => null, // to be computed
-                       'content_sha1' => null, // to be computed
-                       'format_name' => function () {
-                               return CONTENT_FORMAT_WIKITEXT;
-                       },
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '2',
-                       'slot_content_id' => function () {
-                               return null;
-                       },
-               ] );
-
-               $content = function () {
-                       return new WikitextContent( 'A' );
-               };
-
-               $record = new SlotRecord( $row, $content );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotEmpty( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testNewUnsaved() {
-               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
-
-               $this->assertFalse( $record->hasAddress() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->hasRevision() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertFalse( $record->hasOrigin() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotEmpty( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function provideInvalidConstruction() {
-               yield 'both null' => [ null, null ];
-               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
-               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
-               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
-               yield 'null content' => [ (object)[], null ];
-       }
-
-       /**
-        * @dataProvider provideInvalidConstruction
-        */
-       public function testInvalidConstruction( $row, $content ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new SlotRecord( $row, $content );
-       }
-
-       public function testGetContentId_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getContentId();
-       }
-
-       public function testGetAddress_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getAddress();
-       }
-
-       public function provideIncomplete() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               yield 'unsaved' => [ $unsaved ];
-
-               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $inherited = SlotRecord::newInherited( $parent );
-               yield 'inherited' => [ $inherited ];
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetRevision_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getRevision();
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetOrigin_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getOrigin();
-       }
-
-       public function provideHashStability() {
-               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
-               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
-       }
-
-       /**
-        * @dataProvider provideHashStability
-        */
-       public function testHashStability( $text, $hash ) {
-               // Changing the output of the hash function will break things horribly!
-
-               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
-
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
-               $this->assertSame( $hash, $record->getSha1() );
-       }
-
-       public function testHashComputed() {
-               $row = $this->makeRow();
-               $row->content_sha1 = '';
-
-               $rec = new SlotRecord( $row, new WikitextContent( 'A' ) );
-               $this->assertNotEmpty( $rec->getSha1() );
-       }
-
-       public function testNewWithSuppressedContent() {
-               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $output = SlotRecord::newWithSuppressedContent( $input );
-
-               $this->setExpectedException( SuppressedDataException::class );
-               $output->getContent();
-       }
-
-       public function testNewInherited() {
-               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
-               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, before saving revision meta-data.
-               $inherited = SlotRecord::newInherited( $parent );
-
-               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
-               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
-               $this->assertSame( $parent->getContent(), $inherited->getContent() );
-               $this->assertTrue( $inherited->isInherited() );
-               $this->assertTrue( $inherited->hasOrigin() );
-               $this->assertFalse( $inherited->hasRevision() );
-
-               // make sure we didn't mess with the internal state of $parent
-               $this->assertFalse( $parent->isInherited() );
-               $this->assertSame( 7, $parent->getRevision() );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved(
-                       10,
-                       $inherited->getContentId(),
-                       $inherited->getAddress(),
-                       $inherited
-               );
-               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
-               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
-               $this->assertSame( $parent->getContent(), $saved->getContent() );
-               $this->assertTrue( $saved->isInherited() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertSame( 10, $saved->getRevision() );
-
-               // make sure we didn't mess with the internal state of $parent or $inherited
-               $this->assertSame( 7, $parent->getRevision() );
-               $this->assertFalse( $inherited->hasRevision() );
-       }
-
-       public function testNewSaved() {
-               // This would happen while doing an edit, before saving revision meta-data.
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
-               $this->assertFalse( $saved->isInherited() );
-               $this->assertTrue( $saved->hasOrigin() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertTrue( $saved->hasAddress() );
-               $this->assertTrue( $saved->hasContentId() );
-               $this->assertSame( 'theNewAddress', $saved->getAddress() );
-               $this->assertSame( 20, $saved->getContentId() );
-               $this->assertSame( 'A', $saved->getContent()->getText() );
-               $this->assertSame( 10, $saved->getRevision() );
-               $this->assertSame( 10, $saved->getOrigin() );
-
-               // make sure we didn't mess with the internal state of $unsaved
-               $this->assertFalse( $unsaved->hasAddress() );
-               $this->assertFalse( $unsaved->hasContentId() );
-               $this->assertFalse( $unsaved->hasRevision() );
-       }
-
-       public function provideNewSaved_LogicException() {
-               $freshRow = $this->makeRow( [
-                       'content_id' => 10,
-                       'content_address' => 'address:1',
-                       'slot_origin' => 1,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
-               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
-               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
-               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
-
-               $inheritedRow = $this->makeRow( [
-                       'content_id' => null,
-                       'content_address' => null,
-                       'slot_origin' => 0,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
-               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_LogicException
-        */
-       public function testNewSaved_LogicException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( LogicException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideNewSaved_InvalidArgumentException() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
-               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
-               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_InvalidArgumentException
-        */
-       public function testNewSaved_InvalidArgumentException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideHasSameContent() {
-               $fail = function () {
-                       self::fail( 'There should be no need to actually load the content.' );
-               };
-
-               $a100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a1b = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100null = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => null,
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a2 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $b100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'B',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a200a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 200,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100x1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-x',
-                                       'content_address' => 'xxx:x1',
-                               ]
-                       ),
-                       $fail
-               );
-
-               yield 'same instance' => [ $a100a1, $a100a1, true ];
-               yield 'no address' => [ $a100a1, $a100null, true ];
-               yield 'same address' => [ $a100a1, $a100a1b, true ];
-               yield 'different address' => [ $a100a1, $a100a2, true ];
-               yield 'different model' => [ $a100a1, $b100a1, false ];
-               yield 'different size' => [ $a100a1, $a200a1, false ];
-               yield 'different hash' => [ $a100a1, $a100x1, false ];
-       }
-
-       /**
-        * @dataProvider provideHasSameContent
-        */
-       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
-               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
-               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/TitleArrayFromResultTest.php b/tests/phpunit/includes/TitleArrayFromResultTest.php
deleted file mode 100644 (file)
index 32c7571..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-
-/**
- * @author Addshore
- * @covers TitleArrayFromResult
- */
-class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
-               $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class )
-                       ->disableOriginalConstructor();
-
-               $resultWrapper = $resultWrapper->getMock();
-               $resultWrapper->expects( $this->atLeastOnce() )
-                       ->method( 'current' )
-                       ->will( $this->returnValue( $row ) );
-               $resultWrapper->expects( $this->any() )
-                       ->method( 'numRows' )
-                       ->will( $this->returnValue( $numRows ) );
-
-               return $resultWrapper;
-       }
-
-       private function getRowWithTitle( $namespace = 3, $title = 'foo' ) {
-               $row = new stdClass();
-               $row->page_namespace = $namespace;
-               $row->page_title = $title;
-               return $row;
-       }
-
-       /**
-        * @covers TitleArrayFromResult::__construct
-        */
-       public function testConstructionWithFalseRow() {
-               $row = false;
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = new TitleArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertEquals( $row, $object->current );
-       }
-
-       /**
-        * @covers TitleArrayFromResult::__construct
-        */
-       public function testConstructionWithRow() {
-               $namespace = 0;
-               $title = 'foo';
-               $row = $this->getRowWithTitle( $namespace, $title );
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = new TitleArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertInstanceOf( Title::class, $object->current );
-               $this->assertEquals( $namespace, $object->current->mNamespace );
-               $this->assertEquals( $title, $object->current->mTextform );
-       }
-
-       public static function provideNumberOfRows() {
-               return [
-                       [ 0 ],
-                       [ 1 ],
-                       [ 122 ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideNumberOfRows
-        * @covers TitleArrayFromResult::count
-        */
-       public function testCountWithVaryingValues( $numRows ) {
-               $object = new TitleArrayFromResult( $this->getMockResultWrapper(
-                       $this->getRowWithTitle(),
-                       $numRows
-               ) );
-               $this->assertEquals( $numRows, $object->count() );
-       }
-
-       /**
-        * @covers TitleArrayFromResult::current
-        */
-       public function testCurrentAfterConstruction() {
-               $namespace = 0;
-               $title = 'foo';
-               $row = $this->getRowWithTitle( $namespace, $title );
-               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $row ) );
-               $this->assertInstanceOf( Title::class, $object->current() );
-               $this->assertEquals( $namespace, $object->current->mNamespace );
-               $this->assertEquals( $title, $object->current->mTextform );
-       }
-
-       public function provideTestValid() {
-               return [
-                       [ $this->getRowWithTitle(), true ],
-                       [ false, false ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideTestValid
-        * @covers TitleArrayFromResult::valid
-        */
-       public function testValid( $input, $expected ) {
-               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $input ) );
-               $this->assertEquals( $expected, $object->valid() );
-       }
-
-       // @todo unit test for key()
-       // @todo unit test for next()
-       // @todo unit test for rewind()
-}
index 4ffef02..913f56d 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\Interwiki\InterwikiLookup;
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
@@ -553,6 +554,10 @@ class TitleTest extends MediaWikiTestCase {
                        # Title, expected base, optional message
                        [ 'User:John_Doe/subOne/subTwo', 'John Doe' ],
                        [ 'User:Foo / Bar / Baz', 'Foo ' ],
+                       [ 'Talk:////', '////' ],
+                       [ 'Template:////', '////' ],
+                       [ 'Template:Foo////', 'Foo' ],
+                       [ 'Template:Foo////Bar', 'Foo' ],
                ];
        }
 
@@ -577,6 +582,41 @@ class TitleTest extends MediaWikiTestCase {
                ];
        }
 
+       public function provideSubpage() {
+               // NOTE: avoid constructing Title objects in the provider, since it may access the database.
+               return [
+                       [ 'Foo', 'x', new TitleValue( NS_MAIN, 'Foo/x' ) ],
+                       [ 'Foo#bar', 'x', new TitleValue( NS_MAIN, 'Foo/x' ) ],
+                       [ 'User:Foo', 'x', new TitleValue( NS_USER, 'Foo/x' ) ],
+                       [ 'wiki:User:Foo', 'x', new TitleValue( NS_MAIN, 'User:Foo/x', '', 'wiki' ) ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideSubpage
+        * @covers Title::getSubpage
+        */
+       public function testSubpage( $title, $sub, LinkTarget $expected ) {
+               $interwikiLookup = $this->getMock( InterwikiLookup::class );
+               $interwikiLookup->expects( $this->any() )
+                       ->method( 'isValidInterwiki' )
+                       ->willReturnCallback(
+                               function ( $prefix ) {
+                                       return $prefix == 'wiki';
+                               }
+                       );
+
+               $this->setService( 'InterwikiLookup', $interwikiLookup );
+
+               $title = Title::newFromText( $title );
+               $expected = Title::newFromLinkTarget( $expected );
+               $actual = $title->getSubpage( $sub );
+
+               // NOTE: convert to string for comparison
+               $this->assertSame( $expected->getPrefixedText(), $actual->getPrefixedText(), 'text form' );
+               $this->assertTrue( $expected->equals( $actual ), 'Title equality' );
+       }
+
        public static function provideNewFromTitleValue() {
                return [
                        [ new TitleValue( NS_MAIN, 'Foo' ) ],
index 6850a24..6fe9218 100644 (file)
@@ -236,7 +236,7 @@ class WikiMapTest extends MediaWikiLangTestCase {
                $this->assertEquals( $wiki, WikiMap::getWikiFromUrl( $url ) );
        }
 
-       public function provideGetWikiIdFromDomain() {
+       public function provideGetWikiIdFromDbDomain() {
                return [
                        [ 'db-prefix_', 'db-prefix_' ],
                        [ wfWikiID(), wfWikiID() ],
@@ -249,10 +249,10 @@ class WikiMapTest extends MediaWikiLangTestCase {
        }
 
        /**
-        * @dataProvider provideGetWikiIdFromDomain
+        * @dataProvider provideGetWikiIdFromDbDomain
         * @covers WikiMap::getWikiIdFromDbDomain()
         */
-       public function testGetWikiIdFromDomain( $domain, $wikiId ) {
+       public function testGetWikiIdFromDbDomain( $domain, $wikiId ) {
                $this->assertEquals( $wikiId, WikiMap::getWikiIdFromDbDomain( $domain ) );
        }
 
diff --git a/tests/phpunit/includes/WikiReferenceTest.php b/tests/phpunit/includes/WikiReferenceTest.php
deleted file mode 100644 (file)
index e4b21ce..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-<?php
-
-/**
- * @covers WikiReference
- */
-class WikiReferenceTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function provideGetDisplayName() {
-               return [
-                       'http' => [ 'foo.bar', 'http://foo.bar' ],
-                       'https' => [ 'foo.bar', 'http://foo.bar' ],
-
-                       // apparently, this is the expected behavior
-                       'invalid' => [ 'purple kittens', 'purple kittens' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetDisplayName
-        */
-       public function testGetDisplayName( $expected, $canonicalServer ) {
-               $reference = new WikiReference( $canonicalServer, '/wiki/$1' );
-               $this->assertEquals( $expected, $reference->getDisplayName() );
-       }
-
-       public function testGetCanonicalServer() {
-               $reference = new WikiReference( 'https://acme.com', '/wiki/$1', '//acme.com' );
-               $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() );
-       }
-
-       public function provideGetCanonicalUrl() {
-               return [
-                       'no fragment' => [
-                               'https://acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               null
-                       ],
-                       'empty fragment' => [
-                               'https://acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               ''
-                       ],
-                       'fragment' => [
-                               'https://acme.com/wiki/Foo#Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar'
-                       ],
-                       'double fragment' => [
-                               'https://acme.com/wiki/Foo#Bar%23Xus',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar#Xus'
-                       ],
-                       'escaped fragment' => [
-                               'https://acme.com/wiki/Foo%23Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo#Bar',
-                               null
-                       ],
-                       'empty path' => [
-                               'https://acme.com/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/$1',
-                               'Foo',
-                               null
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetCanonicalUrl
-        */
-       public function testGetCanonicalUrl(
-               $expected, $canonicalServer, $server, $path, $page, $fragmentId
-       ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) );
-       }
-
-       /**
-        * @dataProvider provideGetCanonicalUrl
-        * @note getUrl is an alias for getCanonicalUrl
-        */
-       public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) );
-       }
-
-       public function provideGetFullUrl() {
-               return [
-                       'no fragment' => [
-                               '//acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               null
-                       ],
-                       'empty fragment' => [
-                               '//acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               ''
-                       ],
-                       'fragment' => [
-                               '//acme.com/wiki/Foo#Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar'
-                       ],
-                       'double fragment' => [
-                               '//acme.com/wiki/Foo#Bar%23Xus',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar#Xus'
-                       ],
-                       'escaped fragment' => [
-                               '//acme.com/wiki/Foo%23Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo#Bar',
-                               null
-                       ],
-                       'empty path' => [
-                               '//acme.com/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/$1',
-                               'Foo',
-                               null
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetFullUrl
-        */
-       public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) );
-       }
-
-}
index 2af63c4..c554fb3 100644 (file)
@@ -476,7 +476,7 @@ class ApiQueryWatchlistRawIntegrationTest extends ApiTestCase {
                        new TitleValue( 1, 'ApiQueryWatchlistRawIntegrationTestPage1' ),
                ] );
 
-               ObjectCache::getMainWANInstance()->clearProcessCache();
+               MediaWikiServices::getInstance()->getMainWANObjectCache()->clearProcessCache();
                $result = $this->doListWatchlistRawRequest( [
                        'wrowner' => $otherUser->getName(),
                        'wrtoken' => '1234567890',
index 123b080..424c64b 100644 (file)
@@ -315,6 +315,7 @@ class LBFactoryTest extends MediaWikiTestCase {
                                }
                        ) );
                $lb1->method( 'getMasterPos' )->willReturn( $m1Pos );
+               $lb1->method( 'getReplicaResumePos' )->willReturn( $m1Pos );
                $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
                // Master DB 2
                $mockDB2 = $this->getMockBuilder( IDatabase::class )
@@ -342,6 +343,7 @@ class LBFactoryTest extends MediaWikiTestCase {
                        }
                ) );
                $lb2->method( 'getMasterPos' )->willReturn( $m2Pos );
+               $lb2->method( 'getReplicaResumePos' )->willReturn( $m2Pos );
                $lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' );
 
                $bag = new HashBagOStuff();
index f1bcd98..8510109 100644 (file)
@@ -51,8 +51,11 @@ class LoadBalancerTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers LoadBalancer::getLocalDomainID()
-        * @covers LoadBalancer::resolveDomainID()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getLocalDomainID()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::resolveDomainID()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::haveIndex()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::isNonZeroLoad()
         */
        public function testWithoutReplica() {
                global $wgDBname;
@@ -68,6 +71,15 @@ class LoadBalancerTest extends MediaWikiTestCase {
                        }
                ] );
 
+               $this->assertEquals( 1, $lb->getServerCount() );
+               $this->assertFalse( $lb->hasReplicaServers() );
+               $this->assertFalse( $lb->hasStreamingReplicaServers() );
+
+               $this->assertTrue( $lb->haveIndex( 0 ) );
+               $this->assertFalse( $lb->haveIndex( 1 ) );
+               $this->assertFalse( $lb->isNonZeroLoad( 0 ) );
+               $this->assertFalse( $lb->isNonZeroLoad( 1 ) );
+
                $ld = DatabaseDomain::newFromId( $lb->getLocalDomainID() );
                $this->assertEquals( $wgDBname, $ld->getDatabase(), 'local domain DB set' );
                $this->assertEquals( $this->dbPrefix(), $ld->getTablePrefix(), 'local domain prefix set' );
@@ -108,6 +120,17 @@ class LoadBalancerTest extends MediaWikiTestCase {
                $lb->closeAll();
        }
 
+       /**
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getReaderIndex()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::haveIndex()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::isNonZeroLoad()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getServerName()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getServerInfo()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getServerType()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getServerAttributes()
+        */
        public function testWithReplica() {
                global $wgDBserver;
 
@@ -118,6 +141,18 @@ class LoadBalancerTest extends MediaWikiTestCase {
                $this->assertTrue( $lb->hasReplicaServers() );
                $this->assertTrue( $lb->hasStreamingReplicaServers() );
 
+               $this->assertTrue( $lb->haveIndex( 0 ) );
+               $this->assertTrue( $lb->haveIndex( 1 ) );
+               $this->assertFalse( $lb->isNonZeroLoad( 0 ) );
+               $this->assertTrue( $lb->isNonZeroLoad( 1 ) );
+
+               for ( $i = 0; $i < $lb->getServerCount(); ++$i ) {
+                       $this->assertType( 'string', $lb->getServerName( $i ) );
+                       $this->assertType( 'array', $lb->getServerInfo( $i ) );
+                       $this->assertType( 'string', $lb->getServerType( $i ) );
+                       $this->assertType( 'array', $lb->getServerAttributes( $i ) );
+               }
+
                $dbw = $lb->getConnection( DB_MASTER );
                $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
                $this->assertEquals(
@@ -136,6 +171,7 @@ class LoadBalancerTest extends MediaWikiTestCase {
                        'cluster master set' );
                $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
                $this->assertWriteForbidden( $dbr );
+               $this->assertEquals( $dbr->getLBInfo( 'serverIndex' ), $lb->getReaderIndex() );
 
                if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
                        $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
@@ -389,8 +425,10 @@ class LoadBalancerTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers LoadBalancer::openConnection()
-        * @covers LoadBalancer::getAnyOpenConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::openConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getAnyOpenConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
         */
        function testOpenConnection() {
                $lb = $this->newSingleServerLocalLoadBalancer();
@@ -436,6 +474,18 @@ class LoadBalancerTest extends MediaWikiTestCase {
                $lb->closeAll();
        }
 
+       /**
+        * @covers \Wikimedia\Rdbms\LoadBalancer::openConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::forEachOpenMasterConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::setTransactionListener()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::beginMasterChanges()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::finalizeMasterChanges()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::approveMasterChanges()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::commitMasterChanges()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::runMasterTransactionIdleCallbacks()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::runMasterTransactionListenerCallbacks()
+        */
        public function testTransactionCallbackChains() {
                global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
 
@@ -523,6 +573,10 @@ class LoadBalancerTest extends MediaWikiTestCase {
                $conn2->close();
        }
 
+       /**
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+        */
        public function testDBConnRefReadsMasterAndReplicaRoles() {
                $lb = $this->newSingleServerLocalLoadBalancer();
 
@@ -547,6 +601,7 @@ class LoadBalancerTest extends MediaWikiTestCase {
        }
 
        /**
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
         * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
         */
        public function testDBConnRefWritesReplicaRole() {
@@ -558,6 +613,7 @@ class LoadBalancerTest extends MediaWikiTestCase {
        }
 
        /**
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
         * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
         */
        public function testDBConnRefWritesReplicaRoleIndex() {
@@ -569,6 +625,7 @@ class LoadBalancerTest extends MediaWikiTestCase {
        }
 
        /**
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
         * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
         */
        public function testDBConnRefWritesReplicaRoleInsert() {
@@ -579,6 +636,10 @@ class LoadBalancerTest extends MediaWikiTestCase {
                $rConn->insert( 'test', [ 't' => 1 ], __METHOD__ );
        }
 
+       /**
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getMaintenanceConnectionRef()
+        */
        public function testQueryGroupIndex() {
                $lb = $this->newMultiServerLocalLoadBalancer( [ 'defaultGroup' => false ] );
                /** @var LoadBalancer $lbWrapper */
@@ -599,9 +660,13 @@ class LoadBalancerTest extends MediaWikiTestCase {
 
                $rRC = $lb->getConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
                $rWL = $lb->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
+               $rRCMaint = $lb->getMaintenanceConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
+               $rWLMaint = $lb->getMaintenanceConnectionRef( DB_REPLICA, [ 'watchlist' ] );
 
                $this->assertEquals( 3, $rRC->getLBInfo( 'serverIndex' ) );
                $this->assertEquals( 3, $rWL->getLBInfo( 'serverIndex' ) );
+               $this->assertEquals( 3, $rRCMaint->getLBInfo( 'serverIndex' ) );
+               $this->assertEquals( 3, $rWLMaint->getLBInfo( 'serverIndex' ) );
 
                $rLog = $lb->getConnectionRef( DB_REPLICA, [ 'logging', 'watchlist' ] );
                $logIndexPicked = $rLog->getLBInfo( 'serverIndex' );
@@ -628,4 +693,30 @@ class LoadBalancerTest extends MediaWikiTestCase {
                $rGeneric = $lb->getConnectionRef( DB_REPLICA );
                $this->assertEquals( $lb->getWriterIndex(), $rGeneric->getLBInfo( 'serverIndex' ) );
        }
+
+       /**
+        * @covers \Wikimedia\Rdbms\LoadBalancer::getLazyConnectionRef
+        */
+       public function testGetLazyConnectionRef() {
+               $lb = $this->newMultiServerLocalLoadBalancer();
+
+               $rMaster = $lb->getLazyConnectionRef( DB_MASTER );
+               $rReplica = $lb->getLazyConnectionRef( 1 );
+               $this->assertFalse( $lb->getAnyOpenConnection( 0 ) );
+               $this->assertFalse( $lb->getAnyOpenConnection( 1 ) );
+
+               $rMaster->getType();
+               $rReplica->getType();
+               $rMaster->getDomainID();
+               $rReplica->getDomainID();
+               $this->assertFalse( $lb->getAnyOpenConnection( 0 ) );
+               $this->assertFalse( $lb->getAnyOpenConnection( 1 ) );
+
+               $rMaster->query( "SELECT 1", __METHOD__ );
+               $this->assertNotFalse( $lb->getAnyOpenConnection( 0 ) );
+
+               $rReplica->query( "SELECT 1", __METHOD__ );
+               $this->assertNotFalse( $lb->getAnyOpenConnection( 0 ) );
+               $this->assertNotFalse( $lb->getAnyOpenConnection( 1 ) );
+       }
 }
diff --git a/tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php
deleted file mode 100644 (file)
index b30c7a4..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-namespace MediaWiki\Logger\Monolog;
-
-/**
- * Flay per https://phabricator.wikimedia.org/T218688.
- *
- * @group Broken
- * @covers \MediaWiki\Logger\Monolog\CeeFormatter
- */
-class CeeFormatterTest extends \PHPUnit\Framework\TestCase {
-       public function testV1() {
-               $ls_formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
-               $cee_formatter = new CeeFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
-               $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ];
-               $this->assertSame(
-                       $cee_formatter->format( $record ),
-                       "@cee: " . $ls_formatter->format( $record ) );
-       }
-}
index 83e9a47..ccfcc18 100644 (file)
@@ -42,11 +42,13 @@ class SiteStatsUpdateTest extends MediaWikiTestCase {
                $fi = SiteStats::images();
                $ai = SiteStats::articles();
 
+               $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount() );
+
                $dbw->begin( __METHOD__ ); // block opportunistic updates
 
-               $update = SiteStatsUpdate::factory( [ 'pages' => 2, 'images' => 1, 'edits' => 2 ] );
-               $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount() );
-               $update->doUpdate();
+               DeferredUpdates::addUpdate(
+                       SiteStatsUpdate::factory( [ 'pages' => 2, 'images' => 1, 'edits' => 2 ] )
+               );
                $this->assertEquals( 1, DeferredUpdates::pendingUpdatesCount() );
 
                // Still the same
diff --git a/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php b/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php
deleted file mode 100644 (file)
index fe129b7..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @covers DifferenceEngineSlotDiffRenderer
- */
-class DifferenceEngineSlotDiffRendererTest extends \PHPUnit\Framework\TestCase {
-
-       public function testGetDiff() {
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $oldContent = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
-               $newContent = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
-
-               $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
-               $this->assertEquals( 'xxx|yyy', $diff );
-
-               $diff = $slotDiffRenderer->getDiff( null, $newContent );
-               $this->assertEquals( '|yyy', $diff );
-
-               $diff = $slotDiffRenderer->getDiff( $oldContent, null );
-               $this->assertEquals( 'xxx|', $diff );
-       }
-
-       public function testAddModules() {
-               $output = $this->getMockBuilder( OutputPage::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'addModules' ] )
-                       ->getMock();
-               $output->expects( $this->once() )
-                       ->method( 'addModules' )
-                       ->with( 'foo' );
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $slotDiffRenderer->addModules( $output );
-       }
-
-       public function testGetExtraCacheKeys() {
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $extraCacheKeys = $slotDiffRenderer->getExtraCacheKeys();
-               $this->assertSame( [ 'foo' ], $extraCacheKeys );
-       }
-
-}
diff --git a/tests/phpunit/includes/diff/SlotDiffRendererTest.php b/tests/phpunit/includes/diff/SlotDiffRendererTest.php
deleted file mode 100644 (file)
index a03280d..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-use Wikimedia\Assert\ParameterTypeException;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers SlotDiffRenderer
- */
-class SlotDiffRendererTest extends \PHPUnit\Framework\TestCase {
-
-       /**
-        * @dataProvider provideNormalizeContents
-        */
-       public function testNormalizeContents(
-               $oldContent, $newContent, $allowedClasses,
-               $expectedOldContent, $expectedNewContent, $expectedExceptionClass
-       ) {
-               $slotDiffRenderer = $this->getMockBuilder( SlotDiffRenderer::class )
-                       ->getMock();
-               try {
-                       // __call needs help deciding which parameter to take by reference
-                       call_user_func_array( [ TestingAccessWrapper::newFromObject( $slotDiffRenderer ),
-                               'normalizeContents' ], [ &$oldContent, &$newContent, $allowedClasses ] );
-                       $this->assertEquals( $expectedOldContent, $oldContent );
-                       $this->assertEquals( $expectedNewContent, $newContent );
-               } catch ( Exception $e ) {
-                       if ( !$expectedExceptionClass ) {
-                               throw $e;
-                       }
-                       $this->assertInstanceOf( $expectedExceptionClass, $e );
-               }
-       }
-
-       public function provideNormalizeContents() {
-               return [
-                       'both null' => [ null, null, null, null, null, InvalidArgumentException::class ],
-                       'left null' => [
-                               null, new WikitextContent( 'abc' ), null,
-                               new WikitextContent( '' ), new WikitextContent( 'abc' ), null,
-                       ],
-                       'right null' => [
-                               new WikitextContent( 'def' ), null, null,
-                               new WikitextContent( 'def' ), new WikitextContent( '' ), null,
-                       ],
-                       'type filter' => [
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
-                       ],
-                       'type filter (subclass)' => [
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), TextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
-                       ],
-                       'type filter (null)' => [
-                               new WikitextContent( 'abc' ), null, TextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( '' ), null,
-                       ],
-                       'type filter failure (left)' => [
-                               new TextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
-                               null, null, ParameterTypeException::class,
-                       ],
-                       'type filter failure (right)' => [
-                               new WikitextContent( 'abc' ), new TextContent( 'def' ), WikitextContent::class,
-                               null, null, ParameterTypeException::class,
-                       ],
-                       'type filter (array syntax)' => [
-                               new WikitextContent( 'abc' ), new JsonContent( 'def' ),
-                               [ JsonContent::class, WikitextContent::class ],
-                               new WikitextContent( 'abc' ), new JsonContent( 'def' ), null,
-                       ],
-                       'type filter failure (array syntax)' => [
-                               new WikitextContent( 'abc' ), new CssContent( 'def' ),
-                               [ JsonContent::class, WikitextContent::class ],
-                               null, null, ParameterTypeException::class,
-                       ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/filebackend/HTTPFileStreamerTest.php b/tests/phpunit/includes/filebackend/HTTPFileStreamerTest.php
new file mode 100644 (file)
index 0000000..bb025b6
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+class HTTPFileStreamerTest extends TestCase {
+
+       /**
+        * @covers HTTPFileStreamer::preprocessHeaders
+        * @dataProvider providePreprocessHeaders
+        */
+       public function testPreprocessHeaders( array $input, array $expectedRaw, array $expectedOpt ) {
+               list( $actualRaw, $actualOpt ) = HTTPFileStreamer::preprocessHeaders( $input );
+               $this->assertSame( $expectedRaw, $actualRaw );
+               $this->assertSame( $expectedOpt, $actualOpt );
+       }
+
+       public function providePreprocessHeaders() {
+               return [
+                       [
+                               [ 'Vary' => 'cookie', 'Cache-Control' => 'private' ],
+                               [ 'Vary: cookie', 'Cache-Control: private' ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'Range' => 'bytes=(123-456)',
+                                       'Content-Type' => 'video/mp4',
+                                       'If-Modified-Since' => 'Wed, 21 Oct 2015 07:28:00 GMT',
+                               ],
+                               [ 'Content-Type: video/mp4' ],
+                               [ 'range' => 'bytes=(123-456)', 'if-modified-since' => 'Wed, 21 Oct 2015 07:28:00 GMT' ],
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php
deleted file mode 100644 (file)
index 346be7a..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-
-class FileBackendDBRepoWrapperTest extends MediaWikiTestCase {
-       protected $backendName = 'foo-backend';
-       protected $repoName = 'pureTestRepo';
-
-       /**
-        * @dataProvider getBackendPathsProvider
-        * @covers FileBackendDBRepoWrapper::getBackendPaths
-        */
-       public function testGetBackendPaths(
-               $mocks,
-               $latest,
-               $dbReadsExpected,
-               $dbReturnValue,
-               $originalPath,
-               $expectedBackendPath,
-               $message ) {
-               list( $dbMock, $backendMock, $wrapperMock ) = $mocks;
-
-               $dbMock->expects( $dbReadsExpected )
-                       ->method( 'selectField' )
-                       ->will( $this->returnValue( $dbReturnValue ) );
-
-               $newPaths = $wrapperMock->getBackendPaths( [ $originalPath ], $latest );
-
-               $this->assertEquals(
-                       $expectedBackendPath,
-                       $newPaths[0],
-                       $message );
-       }
-
-       public function getBackendPathsProvider() {
-               $prefix = 'mwstore://' . $this->backendName . '/' . $this->repoName;
-               $mocksForCaching = $this->getMocks();
-
-               return [
-                       [
-                               $mocksForCaching,
-                               false,
-                               $this->once(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'Public path translated correctly',
-                       ],
-                       [
-                               $mocksForCaching,
-                               false,
-                               $this->never(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'LRU cache leveraged',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->once(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'Latest obtained',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->never(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-deleted/f/o/foobar.jpg',
-                               $prefix . '-original/f/o/o/foobar',
-                               'Deleted path translated correctly',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->once(),
-                               null,
-                               $prefix . '-public/b/a/baz.jpg',
-                               $prefix . '-public/b/a/baz.jpg',
-                               'Path left untouched if no sha1 can be found',
-                       ],
-               ];
-       }
-
-       /**
-        * @covers FileBackendDBRepoWrapper::getFileContentsMulti
-        */
-       public function testGetFileContentsMulti() {
-               list( $dbMock, $backendMock, $wrapperMock ) = $this->getMocks();
-
-               $sha1Path = 'mwstore://' . $this->backendName . '/' . $this->repoName
-                       . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9';
-               $filenamePath = 'mwstore://' . $this->backendName . '/' . $this->repoName
-                       . '-public/f/o/foobar.jpg';
-
-               $dbMock->expects( $this->once() )
-                       ->method( 'selectField' )
-                       ->will( $this->returnValue( '96246614d75ba1703bdfd5d7660bb57407aaf5d9' ) );
-
-               $backendMock->expects( $this->once() )
-                       ->method( 'getFileContentsMulti' )
-                       ->will( $this->returnValue( [ $sha1Path => 'foo' ] ) );
-
-               $result = $wrapperMock->getFileContentsMulti( [ 'srcs' => [ $filenamePath ] ] );
-
-               $this->assertEquals(
-                       [ $filenamePath => 'foo' ],
-                       $result,
-                       'File contents paths translated properly'
-               );
-       }
-
-       protected function getMocks() {
-               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
-                       ->disableOriginalClone()
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $backendMock = $this->getMockBuilder( FSFileBackend::class )
-                       ->setConstructorArgs( [ [
-                                       'name' => $this->backendName,
-                                       'wikiId' => wfWikiID()
-                               ] ] )
-                       ->getMock();
-
-               $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class )
-                       ->setMethods( [ 'getDB' ] )
-                       ->setConstructorArgs( [ [
-                                       'backend' => $backendMock,
-                                       'repoName' => $this->repoName,
-                                       'dbHandleFactory' => null
-                               ] ] )
-                       ->getMock();
-
-               $wrapperMock->expects( $this->any() )->method( 'getDB' )->will( $this->returnValue( $dbMock ) );
-
-               return [ $dbMock, $backendMock, $wrapperMock ];
-       }
-}
diff --git a/tests/phpunit/includes/filerepo/file/ForeignDBFileTest.php b/tests/phpunit/includes/filerepo/file/ForeignDBFileTest.php
deleted file mode 100644 (file)
index 3c92ecb..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/** @covers ForeignDBFile */
-class ForeignDBFileTest extends \PHPUnit\Framework\TestCase {
-
-       use PHPUnit4And6Compat;
-
-       public function testShouldConstructCorrectInstanceFromTitle() {
-               $title = Title::makeTitle( NS_FILE, 'Awesome_file' );
-               $repoMock = $this->createMock( LocalRepo::class );
-
-               $file = ForeignDBFile::newFromTitle( $title, $repoMock );
-
-               $this->assertInstanceOf( ForeignDBFile::class, $file );
-       }
-}
diff --git a/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php b/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php
deleted file mode 100644 (file)
index 05c567d..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-
-/**
- * @covers HTMLCheckMatrix
- */
-class HTMLCheckMatrixTest extends MediaWikiTestCase {
-       private static $defaultOptions = [
-               'rows' => [ 'r1', 'r2' ],
-               'columns' => [ 'c1', 'c2' ],
-               'fieldname' => 'test',
-       ];
-
-       public function testPlainInstantiation() {
-               try {
-                       new HTMLCheckMatrix( [] );
-               } catch ( MWException $e ) {
-                       $this->assertInstanceOf( HTMLFormFieldRequiredOptionsException::class, $e );
-                       return;
-               }
-
-               $this->fail( 'Expected MWException indicating missing parameters but none was thrown.' );
-       }
-
-       public function testInstantiationWithMinimumRequiredParameters() {
-               new HTMLCheckMatrix( self::$defaultOptions );
-               $this->assertTrue( true ); // form instantiation must throw exception on failure
-       }
-
-       public function testValidateCallsUserDefinedValidationCallback() {
-               $called = false;
-               $field = new HTMLCheckMatrix( self::$defaultOptions + [
-                       'validation-callback' => function () use ( &$called ) {
-                               $called = true;
-
-                               return false;
-                       },
-               ] );
-               $this->assertEquals( false, $this->validate( $field, [] ) );
-               $this->assertTrue( $called );
-       }
-
-       public function testValidateRequiresArrayInput() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions );
-               $this->assertEquals( false, $this->validate( $field, null ) );
-               $this->assertEquals( false, $this->validate( $field, true ) );
-               $this->assertEquals( false, $this->validate( $field, 'abc' ) );
-               $this->assertEquals( false, $this->validate( $field, new stdClass ) );
-               $this->assertEquals( true, $this->validate( $field, [] ) );
-       }
-
-       public function testValidateAllowsOnlyKnownTags() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions );
-               $this->assertInstanceOf( Message::class, $this->validate( $field, [ 'foo' ] ) );
-       }
-
-       public function testValidateAcceptsPartialTagList() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions );
-               $this->assertTrue( $this->validate( $field, [] ) );
-               $this->assertTrue( $this->validate( $field, [ 'c1-r1' ] ) );
-               $this->assertTrue( $this->validate( $field, [ 'c1-r1', 'c1-r2', 'c2-r1', 'c2-r2' ] ) );
-       }
-
-       /**
-        * This form object actually has no visibility into what happens later on, but essentially
-        * if the data submitted by the user passes validate the following is run:
-        * foreach ( $field->filterDataForSubmit( $data ) as $k => $v ) {
-        *     $user->setOption( $k, $v );
-        * }
-        */
-       public function testValuesForcedOnRemainOn() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions + [
-                               'force-options-on' => [ 'c2-r1' ],
-                       ] );
-               $expected = [
-                       'c1-r1' => false,
-                       'c1-r2' => false,
-                       'c2-r1' => true,
-                       'c2-r2' => false,
-               ];
-               $this->assertEquals( $expected, $field->filterDataForSubmit( [] ) );
-       }
-
-       public function testValuesForcedOffRemainOff() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions + [
-                               'force-options-off' => [ 'c1-r2', 'c2-r2' ],
-                       ] );
-               $expected = [
-                       'c1-r1' => true,
-                       'c1-r2' => false,
-                       'c2-r1' => true,
-                       'c2-r2' => false,
-               ];
-               // array_keys on the result simulates submitting all fields checked
-               $this->assertEquals( $expected, $field->filterDataForSubmit( array_keys( $expected ) ) );
-       }
-
-       protected function validate( HTMLFormField $field, $submitted ) {
-               return $field->validate(
-                       $submitted,
-                       [ self::$defaultOptions['fieldname'] => $submitted ]
-               );
-       }
-
-}
index 09bcfc9..ef499a1 100644 (file)
@@ -7,20 +7,6 @@
  */
 class HttpTest extends MediaWikiTestCase {
 
-       /**
-        * Test Http::isValidURI()
-        * T29854 : Http::isValidURI is too lax
-        * @dataProvider provideURI
-        * @covers Http::isValidURI
-        */
-       public function testIsValidUri( $expect, $URI, $message = '' ) {
-               $this->assertEquals(
-                       $expect,
-                       (bool)Http::isValidURI( $URI ),
-                       $message
-               );
-       }
-
        /**
         * @covers Http::getProxy
         */
@@ -41,71 +27,4 @@ class HttpTest extends MediaWikiTestCase {
                );
        }
 
-       /**
-        * Feeds URI to test a long regular expression in Http::isValidURI
-        */
-       public static function provideURI() {
-               /** Format: 'boolean expectation', 'URI to test', 'Optional message' */
-               return [
-                       [ false, '¿non sens before!! http://a', 'Allow anything before URI' ],
-
-                       # (http|https) - only two schemes allowed
-                       [ true, 'http://www.example.org/' ],
-                       [ true, 'https://www.example.org/' ],
-                       [ true, 'http://www.example.org', 'URI without directory' ],
-                       [ true, 'http://a', 'Short name' ],
-                       [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star'
-                       [ false, '\\host\directory', 'CIFS share' ],
-                       [ false, 'gopher://host/dir', 'Reject gopher scheme' ],
-                       [ false, 'telnet://host', 'Reject telnet scheme' ],
-
-                       # :\/\/ - double slashes
-                       [ false, 'http//example.org', 'Reject missing colon in protocol' ],
-                       [ false, 'http:/example.org', 'Reject missing slash in protocol' ],
-                       [ false, 'http:example.org', 'Must have two slashes' ],
-                       # Following fail since hostname can be made of anything
-                       [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ],
-
-                       # (\w+:{0,1}\w*@)? - optional user:pass
-                       [ true, 'http://user@host', 'Username provided' ],
-                       [ true, 'http://user:@host', 'Username provided, no password' ],
-                       [ true, 'http://user:pass@host', 'Username and password provided' ],
-
-                       # (\S+) - host part is made of anything not whitespaces
-                       // commented these out in order to remove @group Broken
-                       // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them
-                       // [ false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ],
-                       // [ false, 'http://exam:ple.org/', 'hostname can not use colons!' ],
-
-                       # (:[0-9]+)? - port number
-                       [ true, 'http://example.org:80/' ],
-                       [ true, 'https://example.org:80/' ],
-                       [ true, 'http://example.org:443/' ],
-                       [ true, 'https://example.org:443/' ],
-
-                       # Part after the hostname is / or / with something else
-                       [ true, 'http://example/#' ],
-                       [ true, 'http://example/!' ],
-                       [ true, 'http://example/:' ],
-                       [ true, 'http://example/.' ],
-                       [ true, 'http://example/?' ],
-                       [ true, 'http://example/+' ],
-                       [ true, 'http://example/=' ],
-                       [ true, 'http://example/&' ],
-                       [ true, 'http://example/%' ],
-                       [ true, 'http://example/@' ],
-                       [ true, 'http://example/-' ],
-                       [ true, 'http://example//' ],
-                       [ true, 'http://example/&' ],
-
-                       # Fragment
-                       [ true, 'http://exam#ple.org', ], # This one is valid, really!
-                       [ true, 'http://example.org:80#anchor' ],
-                       [ true, 'http://example.org/?id#anchor' ],
-                       [ true, 'http://example.org/?#anchor' ],
-
-                       [ false, 'http://a ¿non !!sens after', 'Allow anything after URI' ],
-               ];
-       }
-
 }
index 50d5177..24ec2e4 100644 (file)
@@ -69,15 +69,6 @@ class RefreshLinksJobTest extends MediaWikiTestCase {
                $job = new RefreshLinksJob( $page->getTitle(), [ 'parseThreshold' => 0 ] );
                $job->run();
 
-               // assert state
-               $options = ParserOptions::newCanonical( 'canonical' );
-               $out = $parserCache->get( $page, $options );
-               $this->assertNotFalse( $out, 'parser cache entry' );
-
-               $text = $out->getText();
-               $this->assertContains( 'MAIN', $text );
-               $this->assertContains( 'AUX', $text );
-
                $this->assertSelect(
                        'pagelinks',
                        'pl_title',
@@ -92,4 +83,60 @@ class RefreshLinksJobTest extends MediaWikiTestCase {
                );
        }
 
+       public function testRunForMultiPage() {
+               MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+                       'aux',
+                       CONTENT_MODEL_WIKITEXT
+               );
+
+               $fname = __METHOD__;
+
+               $mainContent = new WikitextContent( 'MAIN [[Kittens]]' );
+               $auxContent = new WikitextContent( 'AUX [[Category:Goats]]' );
+               $page1 = $this->createPage( "$fname-1", [ 'main' => $mainContent, 'aux' => $auxContent ] );
+
+               $mainContent = new WikitextContent( 'MAIN [[Dogs]]' );
+               $auxContent = new WikitextContent( 'AUX [[Category:Hamsters]]' );
+               $page2 = $this->createPage( "$fname-2", [ 'main' => $mainContent, 'aux' => $auxContent ] );
+
+               // clear state
+               $parserCache = MediaWikiServices::getInstance()->getParserCache();
+               $parserCache->deleteOptionsKey( $page1 );
+               $parserCache->deleteOptionsKey( $page2 );
+
+               $this->db->delete( 'pagelinks', '*', __METHOD__ );
+               $this->db->delete( 'categorylinks', '*', __METHOD__ );
+
+               // run job
+               $job = new RefreshLinksJob(
+                       Title::newMainPage(),
+                       [ 'pages' => [ [ 0, "$fname-1" ], [ 0, "$fname-2" ] ] ]
+               );
+               $job->run();
+
+               $this->assertSelect(
+                       'pagelinks',
+                       'pl_title',
+                       [ 'pl_from' => $page1->getId() ],
+                       [ [ 'Kittens' ] ]
+               );
+               $this->assertSelect(
+                       'categorylinks',
+                       'cl_to',
+                       [ 'cl_from' => $page1->getId() ],
+                       [ [ 'Goats' ] ]
+               );
+               $this->assertSelect(
+                       'pagelinks',
+                       'pl_title',
+                       [ 'pl_from' => $page2->getId() ],
+                       [ [ 'Dogs' ] ]
+               );
+               $this->assertSelect(
+                       'categorylinks',
+                       'cl_to',
+                       [ 'cl_from' => $page2->getId() ],
+                       [ [ 'Hamsters' ] ]
+               );
+       }
 }
diff --git a/tests/phpunit/includes/json/FormatJsonTest.php b/tests/phpunit/includes/json/FormatJsonTest.php
deleted file mode 100644 (file)
index 1a99775..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-
-/**
- * @covers FormatJson
- */
-class FormatJsonTest extends MediaWikiTestCase {
-
-       /**
-        * Test data for testParseTryFixing.
-        *
-        * Some PHP interpreters use json-c rather than the JSON.org canonical
-        * parser to avoid being encumbered by the "shall be used for Good, not
-        * Evil" clause of the JSON.org parser's license. By default, json-c
-        * parses in a non-strict mode which allows trailing commas for array and
-        * object delarations among other things, so our JSON_ERROR_SYNTAX rescue
-        * block is not always triggered. It however isn't lenient in exactly the
-        * same ways as our TRY_FIXING mode, so the assertions in this test are
-        * a bit more complicated than they ideally would be:
-        *
-        * Optional third argument: true if json-c parses the value without
-        * intervention, false otherwise. Defaults to true.
-        *
-        * Optional fourth argument: expected cannonical JSON serialization of
-        * json-c parsed result. Defaults to the second argument's value.
-        */
-       public static function provideParseTryFixing() {
-               return [
-                       [ "[,]", '[]', false ],
-                       [ "[ , ]", '[]', false ],
-                       [ "[ , }", false ],
-                       [ '[1],', false, true, '[1]' ],
-                       [ "[1,]", '[1]' ],
-                       [ "[1\n,]", '[1]' ],
-                       [ "[1,\n]", '[1]' ],
-                       [ "[1,]\n", '[1]' ],
-                       [ "[1\n,\n]\n", '[1]' ],
-                       [ '["a,",]', '["a,"]' ],
-                       [ "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ],
-                       // I wish we could parse this, but would need quote parsing
-                       [ '[[1,],[2,],[3,]]', false, true, '[[1],[2],[3]]' ],
-                       [ '[1,,]', false, false, '[1]' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideParseTryFixing
-        * @param string $value
-        * @param string|bool $expected Expected result with strict parser
-        * @param bool $jsoncParses Will json-c parse this value without TRY_FIXING?
-        * @param string|bool $expectedJsonc Expected result with lenient parser
-        * if different from the strict expectation
-        */
-       public function testParseTryFixing(
-               $value, $expected,
-               $jsoncParses = true, $expectedJsonc = null
-       ) {
-               // PHP5 results are always expected to have isGood() === false
-               $expectedGoodStatus = false;
-
-               // Check to see if json parser allows trailing commas
-               if ( json_decode( '[1,]' ) !== null ) {
-                       // Use json-c specific expected result if provided
-                       $expected = ( $expectedJsonc === null ) ? $expected : $expectedJsonc;
-                       // If json-c parses the value natively, expect isGood() === true
-                       $expectedGoodStatus = $jsoncParses;
-               }
-
-               $st = FormatJson::parse( $value, FormatJson::TRY_FIXING );
-               $this->assertInstanceOf( Status::class, $st );
-               if ( $expected === false ) {
-                       $this->assertFalse( $st->isOK(), 'Expected isOK() == false' );
-               } else {
-                       $this->assertSame( $expectedGoodStatus, $st->isGood(),
-                               'Expected isGood() == ' . ( $expectedGoodStatus ? 'true' : 'false' )
-                       );
-                       $this->assertTrue( $st->isOK(), 'Expected isOK == true' );
-                       $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK );
-                       $this->assertEquals( $expected, $val );
-               }
-       }
-
-}
index 7147c6f..0c8dc68 100644 (file)
@@ -69,6 +69,21 @@ class MapCacheLRUTest extends PHPUnit\Framework\TestCase {
                );
        }
 
+       /**
+        * @covers MapCacheLRU::has()
+        * @covers MapCacheLRU::get()
+        * @covers MapCacheLRU::set()
+        */
+       function testMissing() {
+               $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
+               $cache = MapCacheLRU::newFromArray( $raw, 3 );
+
+               $this->assertFalse( $cache->has( 'd' ) );
+               $this->assertNull( $cache->get( 'd' ) );
+               $this->assertNull( $cache->get( 'd', 0.0, null ) );
+               $this->assertFalse( $cache->get( 'd', 0.0, false ) );
+       }
+
        /**
         * @covers MapCacheLRU::has()
         * @covers MapCacheLRU::get()
index 017d745..6d32201 100644 (file)
@@ -11,7 +11,7 @@ use Wikimedia\TestingAccessWrapper;
  * @covers WANObjectCache::getWarmupKeyMisses
  * @covers WANObjectCache::prefixCacheKeys
  * @covers WANObjectCache::getProcessCache
- * @covers WANObjectCache::getNonProcessCachedKeys
+ * @covers WANObjectCache::getNonProcessCachedMultiKeys
  * @covers WANObjectCache::getRawKeysForWarmup
  * @covers WANObjectCache::getInterimValue
  * @covers WANObjectCache::setInterimValue
@@ -47,18 +47,26 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
         * @param int $ttl
         */
        public function testSetAndGet( $value, $ttl ) {
+               $cache = $this->cache;
+
                $curTTL = null;
                $asOf = null;
-               $key = $this->cache->makeKey( 'x', wfRandomString() );
+               $key = $cache->makeKey( 'x', wfRandomString() );
 
-               $this->cache->get( $key, $curTTL, [], $asOf );
+               $cache->get( $key, $curTTL, [], $asOf );
                $this->assertNull( $curTTL, "Current TTL is null" );
                $this->assertNull( $asOf, "Current as-of-time is infinite" );
 
                $t = microtime( true );
-               $this->cache->set( $key, $value, $ttl );
 
-               $this->assertEquals( $value, $this->cache->get( $key, $curTTL, [], $asOf ) );
+               $cache->set( $key, $value, $cache::TTL_UNCACHEABLE );
+               $cache->get( $key, $curTTL, [], $asOf );
+               $this->assertNull( $curTTL, "Current TTL is null (TTL_UNCACHEABLE)" );
+               $this->assertNull( $asOf, "Current as-of-time is infinite (TTL_UNCACHEABLE)" );
+
+               $cache->set( $key, $value, $ttl );
+
+               $this->assertEquals( $value, $cache->get( $key, $curTTL, [], $asOf ) );
                if ( is_infinite( $ttl ) || $ttl == 0 ) {
                        $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" );
                } else {
@@ -157,7 +165,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( 6, $hit, "New values cached" );
 
                foreach ( $keys as $i => $key ) {
-                       // Should evict from process cache
+                       // Should not evict from process cache
                        $this->cache->delete( $key );
                        $mockWallClock += 0.001; // cached values will be newer than tombstone
                        // Get into cache (specific process cache group)
@@ -192,7 +200,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
        /**
         * @dataProvider getWithSetCallback_provider
         * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
+        * @covers WANObjectCache::fetchOrRegenerate()
         * @param array $extOpts
         * @param bool $versioned
         */
@@ -268,11 +276,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
 
                $curTTL = null;
                $v = $cache->get( $key, $curTTL, [ $cKey1, $cKey2 ] );
-               if ( $versioned ) {
-                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
-               } else {
-                       $this->assertEquals( $value, $v, "Value returned" );
-               }
+               $this->assertEquals( $value, $v, "Value returned" );
                $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
 
                $wasSet = 0;
@@ -378,7 +382,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
        /**
         * @dataProvider getWithSetCallback_provider
         * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
+        * @covers WANObjectCache::fetchOrRegenerate()
         * @param array $extOpts
         * @param bool $versioned
         */
@@ -544,15 +548,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( 2, $wasSet, "Value re-calculated" );
        }
 
-       /**
-        * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
-        */
-       public function testGetWithSetCallback_invalidCallback() {
-               $this->setExpectedException( InvalidArgumentException::class );
-               $this->cache->getWithSetCallback( 'key', 30, 'invalid callback' );
-       }
-
        /**
         * @dataProvider getMultiWithSetCallback_provider
         * @covers WANObjectCache::getMultiWithSetCallback
@@ -606,15 +601,16 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $value = "@efef$";
                $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
                $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
                $this->assertEquals( $value, $v[$keyB], "Value returned" );
                $this->assertEquals( 1, $wasSet, "Value regenerated" );
-               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
+               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
+
                $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
                $this->assertEquals( $value, $v[$keyB], "Value returned" );
                $this->assertEquals( 1, $wasSet, "Value not regenerated" );
-               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
+               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
 
                $mockWallClock += 1;
 
@@ -649,11 +645,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
 
                $curTTL = null;
                $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
-               if ( $versioned ) {
-                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
-               } else {
-                       $this->assertEquals( $value, $v, "Value returned" );
-               }
+               $this->assertEquals( $value, $v, "Value returned" );
                $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
 
                $wasSet = 0;
@@ -780,12 +772,13 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
                $this->assertEquals( $value, $v[$keyB], "Value returned" );
                $this->assertEquals( 1, $wasSet, "Value regenerated" );
-               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
+               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
+
                $v = $cache->getMultiWithUnionSetCallback(
                        $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
                $this->assertEquals( $value, $v[$keyB], "Value returned" );
                $this->assertEquals( 1, $wasSet, "Value not regenerated" );
-               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
+               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
 
                $mockWallClock += 1;
 
@@ -818,11 +811,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
 
                $curTTL = null;
                $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
-               if ( $versioned ) {
-                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
-               } else {
-                       $this->assertEquals( $value, $v, "Value returned" );
-               }
+               $this->assertEquals( $value, $v, "Value returned" );
                $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
 
                $wasSet = 0;
@@ -880,7 +869,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
 
        /**
         * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
+        * @covers WANObjectCache::fetchOrRegenerate()
         */
        public function testLockTSE() {
                $cache = $this->cache;
@@ -924,7 +913,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
 
        /**
         * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
+        * @covers WANObjectCache::fetchOrRegenerate()
         * @covers WANObjectCache::set()
         */
        public function testLockTSESlow() {
@@ -1005,7 +994,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
 
        /**
         * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
+        * @covers WANObjectCache::fetchOrRegenerate()
         */
        public function testBusyValue() {
                $cache = $this->cache;
@@ -1083,7 +1072,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $cache->set( $key2, $value2, 10 );
 
                $curTTLs = [];
-               $this->assertEquals(
+               $this->assertSame(
                        [ $key1 => $value1, $key2 => $value2 ],
                        $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
                        'Result array populated'
@@ -1099,7 +1088,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $mockWallClock += 1;
 
                $curTTLs = [];
-               $this->assertEquals(
+               $this->assertSame(
                        [ $key1 => $value1, $key2 => $value2 ],
                        $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
                        "Result array populated even with new check keys"
@@ -1160,7 +1149,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        'key2' => $check2,
                        'key3' => $check3,
                ] );
-               $this->assertEquals(
+               $this->assertSame(
                        [ 'key1' => $value1, 'key2' => $value2 ],
                        $result,
                        'Initial values'
@@ -1180,7 +1169,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        'key2' => $check2,
                        'key3' => $check3,
                ] );
-               $this->assertEquals(
+               $this->assertSame(
                        [ 'key1' => $value1, 'key2' => $value2 ],
                        $result,
                        'key1 expired by check1, but value still provided'
@@ -1283,7 +1272,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
        /**
         * @dataProvider getWithSetCallback_versions_provider
         * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
+        * @covers WANObjectCache::fetchOrRegenerate()
         * @param array $extOpts
         * @param bool $versioned
         */
@@ -1523,7 +1512,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $this->internalCache->set(
                        WANObjectCache::VALUE_KEY_PREFIX . $vKey1,
                        [
-                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+                               WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
                                WANObjectCache::FLD_VALUE => $value,
                                WANObjectCache::FLD_TTL => 3600,
                                WANObjectCache::FLD_TIME => $goodTime
@@ -1532,7 +1521,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $this->internalCache->set(
                        WANObjectCache::VALUE_KEY_PREFIX . $vKey2,
                        [
-                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+                               WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
                                WANObjectCache::FLD_VALUE => $value,
                                WANObjectCache::FLD_TTL => 3600,
                                WANObjectCache::FLD_TIME => $badTime
@@ -1569,7 +1558,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        ->setMethods( [ 'get', 'changeTTL' ] )->getMock();
                $backend->expects( $this->once() )->method( 'get' )
                        ->willReturn( [
-                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+                               WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
                                WANObjectCache::FLD_VALUE => 'value',
                                WANObjectCache::FLD_TTL => 3600,
                                WANObjectCache::FLD_TIME => 300,
@@ -1850,6 +1839,137 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
 
                $this->assertEquals( $class, $wanCache->determineKeyClassForStats( $key ) );
        }
+
+       /**
+        * @covers WANObjectCache::makeMultiKeys
+        */
+       public function testMakeMultiKeys() {
+               $cache = $this->cache;
+
+               $ids = [ 1, 2, 3, 4, 4, 5, 6, 6, 7, 7 ];
+               $keyCallback = function ( $id, WANObjectCache $cache ) {
+                       return $cache->makeKey( 'key', $id );
+               };
+               $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+               $expected = [
+                       "local:key:1" => 1,
+                       "local:key:2" => 2,
+                       "local:key:3" => 3,
+                       "local:key:4" => 4,
+                       "local:key:5" => 5,
+                       "local:key:6" => 6,
+                       "local:key:7" => 7
+               ];
+               $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+
+               $ids = [ '1', '2', '3', '4', '4', '5', '6', '6', '7', '7' ];
+               $keyCallback = function ( $id, WANObjectCache $cache ) {
+                       return $cache->makeGlobalKey( 'key', $id, 'a', $id, 'b' );
+               };
+               $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+               $expected = [
+                       "global:key:1:a:1:b" => '1',
+                       "global:key:2:a:2:b" => '2',
+                       "global:key:3:a:3:b" => '3',
+                       "global:key:4:a:4:b" => '4',
+                       "global:key:5:a:5:b" => '5',
+                       "global:key:6:a:6:b" => '6',
+                       "global:key:7:a:7:b" => '7'
+               ];
+               $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+       }
+
+       /**
+        * @covers WANObjectCache::makeMultiKeys
+        */
+       public function testMakeMultiKeysIntString() {
+               $cache = $this->cache;
+               $ids = [ 1, 2, 3, 4, '4', 5, 6, 6, 7, '7' ];
+               $keyCallback = function ( $id, WANObjectCache $cache ) {
+                       return $cache->makeGlobalKey( 'key', $id, 'a', $id, 'b' );
+               };
+
+               $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+               $expected = [
+                       "global:key:1:a:1:b" => 1,
+                       "global:key:2:a:2:b" => 2,
+                       "global:key:3:a:3:b" => 3,
+                       "global:key:4:a:4:b" => 4,
+                       "global:key:5:a:5:b" => 5,
+                       "global:key:6:a:6:b" => 6,
+                       "global:key:7:a:7:b" => 7
+               ];
+               $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+       }
+
+       /**
+        * @covers WANObjectCache::makeMultiKeys
+        * @expectedException UnexpectedValueException
+        */
+       public function testMakeMultiKeysCollision() {
+               $ids = [ 1, 2, 3, 4, '4', 5, 6, 6, 7 ];
+
+               $this->cache->makeMultiKeys(
+                       $ids,
+                       function ( $id ) {
+                               return "keymod:" . $id % 3;
+                       }
+               );
+       }
+
+       /**
+        * @covers WANObjectCache::multiRemap
+        */
+       public function testMultiRemap() {
+               $a = [ 'a', 'b', 'c' ];
+               $res = [ 'keyA' => 1, 'keyB' => 2, 'keyC' => 3 ];
+
+               $this->assertEquals(
+                       [ 'a' => 1, 'b' => 2, 'c' => 3 ],
+                       $this->cache->multiRemap( $a, $res )
+               );
+
+               $a = [ 'a', 'b', 'c', 'c', 'd' ];
+               $res = [ 'keyA' => 1, 'keyB' => 2, 'keyC' => 3, 'keyD' => 4 ];
+
+               $this->assertEquals(
+                       [ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 ],
+                       $this->cache->multiRemap( $a, $res )
+               );
+       }
+
+       /**
+        * @covers WANObjectCache::hash256
+        */
+       public function testHash256() {
+               $bag = new HashBagOStuff();
+               $cache = new WANObjectCache( [ 'cache' => $bag, 'epoch' => 5 ] );
+               $this->assertEquals(
+                       'f402bce76bfa1136adc705d8d5719911ce1fe61f0ad82ddf79a15f3c4de6ec4c',
+                       $cache->hash256( 'x' )
+               );
+
+               $cache = new WANObjectCache( [ 'cache' => $bag, 'epoch' => 50 ] );
+               $this->assertEquals(
+                       'f79a126722f0a682c4c500509f1b61e836e56c4803f92edc89fc281da5caa54e',
+                       $cache->hash256( 'x' )
+               );
+
+               $cache = new WANObjectCache( [ 'cache' => $bag, 'secret' => 'garden' ] );
+               $this->assertEquals(
+                       '48cd57016ffe29981a1114c45e5daef327d30fc6206cb73edc3cb94b4d8fe093',
+                       $cache->hash256( 'x' )
+               );
+
+               $cache = new WANObjectCache( [ 'cache' => $bag, 'secret' => 'garden', 'epoch' => 3 ] );
+               $this->assertEquals(
+                       '48cd57016ffe29981a1114c45e5daef327d30fc6206cb73edc3cb94b4d8fe093',
+                       $cache->hash256( 'x' )
+               );
+       }
 }
 
 class NearExpiringWANObjectCache extends WANObjectCache {
diff --git a/tests/phpunit/includes/libs/rdbms/resultwrapper/FakeResultWrapperTest.php b/tests/phpunit/includes/libs/rdbms/resultwrapper/FakeResultWrapperTest.php
new file mode 100644 (file)
index 0000000..cecdc71
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * Holds tests for FakeResultWrapper MediaWiki class.
+ *
+ * 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
+ */
+
+use Wikimedia\Rdbms\FakeResultWrapper;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\FakeResultWrapper
+ */
+class FakeResultWrapperTest extends PHPUnit\Framework\TestCase {
+       public function testIteration() {
+               $res = new FakeResultWrapper( [
+                       [ 'colA' => 1, 'colB' => 'a' ],
+                       [ 'colA' => 2, 'colB' => 'b' ],
+                       (object)[ 'colA' => 3, 'colB' => 'c' ],
+                       [ 'colA' => 4, 'colB' => 'd' ],
+                       [ 'colA' => 5, 'colB' => 'e' ],
+                       (object)[ 'colA' => 6, 'colB' => 'f' ],
+                       (object)[ 'colA' => 7, 'colB' => 'g' ],
+                       [ 'colA' => 8, 'colB' => 'h' ]
+               ] );
+
+               $expectedRows = [
+                       0 => (object)[ 'colA' => 1, 'colB' => 'a' ],
+                       1 => (object)[ 'colA' => 2, 'colB' => 'b' ],
+                       2 => (object)[ 'colA' => 3, 'colB' => 'c' ],
+                       3 => (object)[ 'colA' => 4, 'colB' => 'd' ],
+                       4 => (object)[ 'colA' => 5, 'colB' => 'e' ],
+                       5 => (object)[ 'colA' => 6, 'colB' => 'f' ],
+                       6 => (object)[ 'colA' => 7, 'colB' => 'g' ],
+                       7 => (object)[ 'colA' => 8, 'colB' => 'h' ]
+               ];
+
+               $this->assertEquals( 8, $res->numRows() );
+
+               $res->seek( 7 );
+               $this->assertEquals( [ 'colA' => 8, 'colB' => 'h' ], $res->fetchRow() );
+               $res->seek( 7 );
+               $this->assertEquals( (object)[ 'colA' => 8, 'colB' => 'h' ], $res->fetchObject() );
+
+               $this->assertEquals( $expectedRows, iterator_to_array( $res, true ) );
+
+               $rows = [];
+               foreach ( $res as $i => $row ) {
+                       $rows[$i] = $row;
+               }
+               $this->assertEquals( $expectedRows, $rows );
+       }
+}
diff --git a/tests/phpunit/includes/libs/rdbms/resultwrapper/ResultWrapperTest.php b/tests/phpunit/includes/libs/rdbms/resultwrapper/ResultWrapperTest.php
new file mode 100644 (file)
index 0000000..ae61966
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * Holds tests for ResultWrapper MediaWiki class.
+ *
+ * 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
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\ResultWrapper
+ */
+class ResultWrapperTest extends PHPUnit\Framework\TestCase {
+       /**
+        * @return IDatabase
+        * @param array[] $rows
+        */
+       private function getDatabaseMock( array $rows ) {
+               $db = $this->getMockBuilder( IDatabase::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $db->method( 'select' )->willReturnCallback(
+                       function () use ( $db, $rows ) {
+                               return new ResultWrapper( $db, $rows );
+                       }
+               );
+               $db->method( 'dataSeek' )->willReturnCallback(
+                       function ( ResultWrapper $res, $pos ) use ( $db ) {
+                               // Position already set in ResultWrapper
+                       }
+               );
+               $db->method( 'fetchRow' )->willReturnCallback(
+                       function ( ResultWrapper $res ) use ( $db ) {
+                               $row = $res::unwrap( $res )[$res->key()] ?? false;
+
+                               return $row;
+                       }
+               );
+               $db->method( 'fetchObject' )->willReturnCallback(
+                       function ( ResultWrapper $res ) use ( $db ) {
+                               $row = $res::unwrap( $res )[$res->key()] ?? false;
+
+                               return $row ? (object)$row : false;
+                       }
+               );
+               $db->method( 'numRows' )->willReturnCallback(
+                       function ( ResultWrapper $res ) use ( $db ) {
+                               return count( $res::unwrap( $res ) );
+                       }
+               );
+
+               return $db;
+       }
+
+       public function testIteration() {
+               $db = $this->getDatabaseMock( [
+                       [ 'colA' => 1, 'colB' => 'a' ],
+                       [ 'colA' => 2, 'colB' => 'b' ],
+                       [ 'colA' => 3, 'colB' => 'c' ],
+                       [ 'colA' => 4, 'colB' => 'd' ],
+                       [ 'colA' => 5, 'colB' => 'e' ],
+                       [ 'colA' => 6, 'colB' => 'f' ],
+                       [ 'colA' => 7, 'colB' => 'g' ],
+                       [ 'colA' => 8, 'colB' => 'h' ]
+               ] );
+
+               $expectedRows = [
+                       0 => (object)[ 'colA' => 1, 'colB' => 'a' ],
+                       1 => (object)[ 'colA' => 2, 'colB' => 'b' ],
+                       2 => (object)[ 'colA' => 3, 'colB' => 'c' ],
+                       3 => (object)[ 'colA' => 4, 'colB' => 'd' ],
+                       4 => (object)[ 'colA' => 5, 'colB' => 'e' ],
+                       5 => (object)[ 'colA' => 6, 'colB' => 'f' ],
+                       6 => (object)[ 'colA' => 7, 'colB' => 'g' ],
+                       7 => (object)[ 'colA' => 8, 'colB' => 'h' ]
+               ];
+
+               $res = $db->select( 'faketable', [ 'colA', 'colB' ], '1 = 1', __METHOD__ );
+               $this->assertEquals( 8, $res->numRows() );
+
+               $res->seek( 7 );
+               $this->assertEquals( [ 'colA' => 8, 'colB' => 'h' ], $res->fetchRow() );
+               $res->seek( 7 );
+               $this->assertEquals( (object)[ 'colA' => 8, 'colB' => 'h' ], $res->fetchObject() );
+
+               $this->assertEquals( $expectedRows, iterator_to_array( $res, true ) );
+
+               $rows = [];
+               foreach ( $res as $i => $row ) {
+                       $rows[$i] = $row;
+               }
+               $this->assertEquals( $expectedRows, $rows );
+       }
+}
index d4e1961..b26a247 100644 (file)
@@ -138,9 +138,10 @@ class LinkRendererTest extends MediaWikiLangTestCase {
        }
 
        public function testGetLinkClasses() {
-               $wanCache = ObjectCache::getMainWANInstance();
-               $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
-               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               $services = MediaWikiServices::getInstance();
+               $wanCache = $services->getMainWANObjectCache();
+               $titleFormatter = $services->getTitleFormatter();
+               $nsInfo = $services->getNamespaceInfo();
                $linkCache = new LinkCache( $titleFormatter, $wanCache, $nsInfo );
                $foobarTitle = new TitleValue( NS_MAIN, 'FooBar' );
                $redirectTitle = new TitleValue( NS_MAIN, 'Redirect' );
diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
deleted file mode 100644 (file)
index c943cef..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-<?php
-/**
- * @todo Could use a test of extended XMP segments. Hard to find programs that
- * create example files, and creating my own in vim propbably wouldn't
- * serve as a very good "test". (Adobe photoshop probably creates such files
- * but it costs money). The implementation of it currently in MediaWiki is based
- * solely on reading the standard, without any real world test files.
- *
- * @group Media
- * @covers JpegMetadataExtractor
- */
-class JpegMetadataExtractorTest extends MediaWikiTestCase {
-
-       protected $filePath;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->filePath = __DIR__ . '/../../data/media/';
-       }
-
-       /**
-        * We also use this test to test padding bytes don't
-        * screw stuff up
-        *
-        * @param string $file Filename
-        *
-        * @dataProvider provideUtf8Comment
-        */
-       public function testUtf8Comment( $file ) {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
-               $this->assertEquals( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] );
-       }
-
-       public static function provideUtf8Comment() {
-               return [
-                       [ 'jpeg-comment-utf.jpg' ],
-                       [ 'jpeg-padding-even.jpg' ],
-                       [ 'jpeg-padding-odd.jpg' ],
-               ];
-       }
-
-       /** The file is iso-8859-1, but it should get auto converted */
-       public function testIso88591Comment() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
-               $this->assertEquals( [ 'ISO-8859-1 JPEG Comment - ¼' ], $res['COM'] );
-       }
-
-       /** Comment values that are non-textual (random binary junk) should not be shown.
-        * The example test file has a comment with a 0x5 byte in it which is a control character
-        * and considered binary junk for our purposes.
-        */
-       public function testBinaryCommentStripped() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
-               $this->assertEmpty( $res['COM'] );
-       }
-
-       /* Very rarely a file can have multiple comments.
-        *   Order of comments is based on order inside the file.
-        */
-       public function testMultipleComment() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
-               $this->assertEquals( [ 'foo', 'bar' ], $res['COM'] );
-       }
-
-       public function testXMPExtraction() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
-               $this->assertEquals( $expected, $res['XMP'] );
-       }
-
-       public function testPSIRExtraction() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $expected = '50686f746f73686f7020332e30003842494d04040000000'
-                       . '000181c02190004746573741c02190003666f6f1c020000020004';
-               $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
-       }
-
-       public function testXMPExtractionAltAppId() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
-               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
-               $this->assertEquals( $expected, $res['XMP'] );
-       }
-
-       public function testIPTCHashComparisionNoHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-no-hash', $res );
-       }
-
-       public function testIPTCHashComparisionBadHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-bad-hash', $res );
-       }
-
-       public function testIPTCHashComparisionGoodHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-good-hash', $res );
-       }
-
-       public function testExifByteOrder() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
-               $expected = 'BE';
-               $this->assertEquals( $expected, $res['byteOrder'] );
-       }
-
-       public function testInfiniteRead() {
-               // test file truncated right after a segment, which previously
-               // caused an infinite loop looking for the next segment byte.
-               // Should get past infinite loop and throw in wfUnpack()
-               $this->setExpectedException( 'MWException' );
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' );
-       }
-
-       public function testInfiniteRead2() {
-               // test file truncated after a segment's marker and size, which
-               // would cause a seek past end of file. Seek past end of file
-               // doesn't actually fail, but prevents further reading and was
-               // devolving into the previous case (testInfiniteRead).
-               $this->setExpectedException( 'MWException' );
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' );
-       }
-}
diff --git a/tests/phpunit/includes/page/ArticleTest.php b/tests/phpunit/includes/page/ArticleTest.php
deleted file mode 100644 (file)
index df4a281..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-class ArticleTest extends MediaWikiTestCase {
-
-       /**
-        * @var Title
-        */
-       private $title;
-       /**
-        * @var Article
-        */
-       private $article;
-
-       /** creates a title object and its article object */
-       protected function setUp() {
-               parent::setUp();
-               $this->title = Title::makeTitle( NS_MAIN, 'SomePage' );
-               $this->article = new Article( $this->title );
-       }
-
-       /** cleanup title object and its article object */
-       protected function tearDown() {
-               parent::tearDown();
-               $this->title = null;
-               $this->article = null;
-       }
-
-       /**
-        * @covers Article::__get
-        */
-       public function testImplementsGetMagic() {
-               $this->assertEquals( false, $this->article->mLatest, "Article __get magic" );
-       }
-
-       /**
-        * @depends testImplementsGetMagic
-        * @covers Article::__set
-        */
-       public function testImplementsSetMagic() {
-               $this->article->mLatest = 2;
-               $this->assertEquals( 2, $this->article->mLatest, "Article __set magic" );
-       }
-
-       /**
-        * @covers Article::__get
-        * @covers Article::__set
-        */
-       public function testGetOrSetOnNewProperty() {
-               $this->article->ext_someNewProperty = 12;
-               $this->assertEquals( 12, $this->article->ext_someNewProperty,
-                       "Article get/set magic on new field" );
-
-               $this->article->ext_someNewProperty = -8;
-               $this->assertEquals( -8, $this->article->ext_someNewProperty,
-                       "Article get/set magic on update to new field" );
-       }
-}
index e094d92..628ddb1 100644 (file)
@@ -11,6 +11,8 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
        use MediaWikiCoversValidator;
        use PHPUnit4And6Compat;
 
+       const NAME = 'test.blobstore';
+
        protected function setUp() {
                parent::setUp();
                // MediaWiki's test wrapper sets $wgMainWANCache to CACHE_NONE.
@@ -24,12 +26,15 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
        }
 
        public function testBlobCreation() {
-               $module = $this->makeModule( [ 'mainpage' ] );
                $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
+               $rl->register( self::NAME, [
+                       'factory' => function () {
+                               return $this->makeModule( [ 'mainpage' ] );
+                       }
+               ] );
 
                $blobStore = $this->makeBlobStore( null, $rl );
-               $blob = $blobStore->getBlob( $module, 'en' );
+               $blob = $blobStore->getBlob( $rl->getModule( self::NAME ), 'en' );
 
                $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' );
        }
@@ -37,7 +42,6 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
        public function testBlobCreation_empty() {
                $module = $this->makeModule( [] );
                $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
 
                $blobStore = $this->makeBlobStore( null, $rl );
                $blob = $blobStore->getBlob( $module, 'en' );
@@ -48,7 +52,6 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
        public function testBlobCreation_unknownMessage() {
                $module = $this->makeModule( [ 'i-dont-exist', 'mainpage', 'i-dont-exist2' ] );
                $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( null, $rl );
 
                // Generating a blob should continue without errors,
@@ -58,9 +61,15 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
        }
 
        public function testMessageCachingAndPurging() {
-               $module = $this->makeModule( [ 'example' ] );
                $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
+               // Register it so that MessageBlobStore::updateMessage can
+               // discover it from the registry as a module that uses this message.
+               $rl->register( self::NAME, [
+                       'factory' => function () {
+                               return $this->makeModule( [ 'example' ] );
+                       }
+               ] );
+               $module = $rl->getModule( self::NAME );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
 
                // Advance this new WANObjectCache instance to a normal state,
@@ -105,7 +114,6 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
        public function testPurgeEverything() {
                $module = $this->makeModule( [ 'example' ] );
                $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                // Advance this new WANObjectCache instance to a normal state.
                $blobStore->getBlob( $module, 'en' );
@@ -139,7 +147,6 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
                // Arrange version 1 of a module
                $module = $this->makeModule( [ 'foo' ] );
                $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->once() )
                        ->method( 'fetchMessage' )
@@ -158,7 +165,6 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
                // We do not receive purges for this because no messages were changed.
                $module = $this->makeModule( [ 'foo', 'bar' ] );
                $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->exactly( 2 ) )
                        ->method( 'fetchMessage' )
@@ -191,7 +197,7 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
 
        private function makeModule( array $messages ) {
                $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] );
-               $module->setName( 'test.blobstore' );
+               $module->setName( self::NAME );
                return $module;
        }
 }
index 408a0a2..e1ee324 100644 (file)
@@ -374,7 +374,7 @@ Deprecation message.' ]
        }
 
        private static function makeModule( array $options = [] ) {
-               return new ResourceLoaderTestModule( $options );
+               return $options + [ 'class' => ResourceLoaderTestModule::class ];
        }
 
        private static function makeSampleModules() {
index 3f6e9b0..60b4073 100644 (file)
@@ -86,6 +86,7 @@ class ResourceLoaderModuleTest extends ResourceLoaderTestCase {
                $context = $this->getResourceLoaderContext();
 
                $module = new ResourceLoaderTestModule( [
+                       'mayValidateScript' => true,
                        'script' => "var a = 'this is';\n {\ninvalid"
                ] );
                $this->assertEquals(
index bc7cb69..2691ccc 100644 (file)
@@ -22,7 +22,7 @@ mw.loader.register( [] );'
                        [ [
                                'msg' => 'Basic registry',
                                'modules' => [
-                                       'test.blank' => new ResourceLoaderTestModule(),
+                                       'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -38,10 +38,22 @@ mw.loader.register( [
                        [ [
                                'msg' => 'Optimise the dependency tree (basic case)',
                                'modules' => [
-                                       'a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'b', 'c', 'd' ] ] ),
-                                       'b' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'c' ] ] ),
-                                       'c' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
-                                       'd' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+                                       'a' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'b', 'c', 'd' ],
+                                       ],
+                                       'b' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'c' ],
+                                       ],
+                                       'c' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [],
+                                       ],
+                                       'd' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [],
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -76,9 +88,18 @@ mw.loader.register( [
                        [ [
                                'msg' => 'Optimise the dependency tree (tolerate unknown deps)',
                                'modules' => [
-                                       'a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'b', 'c', 'x' ] ] ),
-                                       'b' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'c', 'x' ] ] ),
-                                       'c' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+                                       'a' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'b', 'c', 'x' ]
+                                       ],
+                                       'b' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'c', 'x' ]
+                                       ],
+                                       'c' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => []
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -111,11 +132,26 @@ mw.loader.register( [
                                // Regression test for T223402.
                                'msg' => 'Optimise the dependency tree (indirect circular dependency)',
                                'modules' => [
-                                       'top' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'middle1', 'util' ] ] ),
-                                       'middle1' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'middle2', 'util' ] ] ),
-                                       'middle2' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'bottom' ] ] ),
-                                       'bottom' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'top' ] ] ),
-                                       'util' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+                                       'top' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'middle1', 'util' ],
+                                       ],
+                                       'middle1' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'middle2', 'util' ],
+                                       ],
+                                       'middle2' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'bottom' ],
+                                       ],
+                                       'bottom' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'top' ],
+                                       ],
+                                       'util' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [],
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -162,8 +198,14 @@ mw.loader.register( [
                                // Regression test for T223402.
                                'msg' => 'Optimise the dependency tree (direct circular dependency)',
                                'modules' => [
-                                       'top' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'util', 'top' ] ] ),
-                                       'util' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+                                       'top' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [ 'util', 'top' ],
+                                       ],
+                                       'util' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'dependencies' => [],
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -187,13 +229,16 @@ mw.loader.register( [
                        [ [
                                'msg' => 'Version falls back gracefully if getVersionHash throws',
                                'modules' => [
-                                       'test.fail' => (
-                                               ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
-                                                       ->setMethods( [ 'getVersionHash' ] )->getMock() )
-                                               && $mock->method( 'getVersionHash' )->will(
-                                                       $this->throwException( new Exception )
-                                               )
-                                       ) ? $mock : $mock
+                                       'test.fail' => [
+                                               'factory' => function () {
+                                                       $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+                                                               ->setMethods( [ 'getVersionHash' ] )->getMock();
+                                                       $mock->method( 'getVersionHash' )->will(
+                                                               $this->throwException( new Exception )
+                                                       );
+                                                       return $mock;
+                                               }
+                                       ]
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -212,11 +257,14 @@ mw.loader.state( {
                        [ [
                                'msg' => 'Use version from getVersionHash',
                                'modules' => [
-                                       'test.version' => (
-                                               ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
-                                                       ->setMethods( [ 'getVersionHash' ] )->getMock() )
-                                               && $mock->method( 'getVersionHash' )->willReturn( '1234567' )
-                                       ) ? $mock : $mock
+                                       'test.version' => [
+                                               'factory' => function () {
+                                                       $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+                                                               ->setMethods( [ 'getVersionHash' ] )->getMock();
+                                                       $mock->method( 'getVersionHash' )->willReturn( '1234567' );
+                                                       return $mock;
+                                               }
+                                       ]
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -232,11 +280,14 @@ mw.loader.register( [
                        [ [
                                'msg' => 'Re-hash version from getVersionHash if too long',
                                'modules' => [
-                                       'test.version' => (
-                                               ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
-                                                       ->setMethods( [ 'getVersionHash' ] )->getMock() )
-                                               && $mock->method( 'getVersionHash' )->willReturn( '12345678' )
-                                       ) ? $mock : $mock
+                                       'test.version' => [
+                                               'factory' => function () {
+                                                       $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+                                                               ->setMethods( [ 'getVersionHash' ] )->getMock();
+                                                       $mock->method( 'getVersionHash' )->willReturn( '12345678' );
+                                                       return $mock;
+                                               }
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -252,9 +303,15 @@ mw.loader.register( [
                        [ [
                                'msg' => 'Group signature',
                                'modules' => [
-                                       'test.blank' => new ResourceLoaderTestModule(),
-                                       'test.group.foo' => new ResourceLoaderTestModule( [ 'group' => 'x-foo' ] ),
-                                       'test.group.bar' => new ResourceLoaderTestModule( [ 'group' => 'x-bar' ] ),
+                                       'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+                                       'test.group.foo' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'group' => 'x-foo',
+                                       ],
+                                       'test.group.bar' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'group' => 'x-bar',
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -282,8 +339,11 @@ mw.loader.register( [
                        [ [
                                'msg' => 'Different target (non-test should not be registered)',
                                'modules' => [
-                                       'test.blank' => new ResourceLoaderTestModule(),
-                                       'test.target.foo' => new ResourceLoaderTestModule( [ 'targets' => [ 'x-foo' ] ] ),
+                                       'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+                                       'test.target.foo' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'targets' => [ 'x-foo' ],
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -300,16 +360,19 @@ mw.loader.register( [
                                'msg' => 'Safemode disabled (default; register all modules)',
                                'modules' => [
                                        // Default origin: ORIGIN_CORE_SITEWIDE
-                                       'test.blank' => new ResourceLoaderTestModule(),
-                                       'test.core-generated' => new ResourceLoaderTestModule( [
+                                       'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+                                       'test.core-generated' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'origin' => ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
-                                       ] ),
-                                       'test.sitewide' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.sitewide' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'origin' => ResourceLoaderModule::ORIGIN_USER_SITEWIDE
-                                       ] ),
-                                       'test.user' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.user' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'origin' => ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL
-                                       ] ),
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -339,16 +402,19 @@ mw.loader.register( [
                                'extraQuery' => [ 'safemode' => '1' ],
                                'modules' => [
                                        // Default origin: ORIGIN_CORE_SITEWIDE
-                                       'test.blank' => new ResourceLoaderTestModule(),
-                                       'test.core-generated' => new ResourceLoaderTestModule( [
+                                       'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+                                       'test.core-generated' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'origin' => ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
-                                       ] ),
-                                       'test.sitewide' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.sitewide' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'origin' => ResourceLoaderModule::ORIGIN_USER_SITEWIDE
-                                       ] ),
-                                       'test.user' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.user' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'origin' => ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL
-                                       ] ),
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -374,7 +440,10 @@ mw.loader.register( [
                                        ],
                                ],
                                'modules' => [
-                                       'test.blank' => new ResourceLoaderTestModule( [ 'source' => 'example' ] ),
+                                       'test.blank' => [
+                                               'class' => ResourceLoaderTestModule::class,
+                                               'source' => 'example'
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -394,25 +463,28 @@ mw.loader.register( [
                        [ [
                                'msg' => 'Conditional dependency function',
                                'modules' => [
-                                       'test.x.core' => new ResourceLoaderTestModule(),
-                                       'test.x.polyfill' => new ResourceLoaderTestModule( [
+                                       'test.x.core' => [ 'class' => ResourceLoaderTestModule::class ],
+                                       'test.x.polyfill' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'skipFunction' => 'return true;'
-                                       ] ),
-                                       'test.y.polyfill' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.y.polyfill' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'skipFunction' =>
                                                        'return !!(' .
                                                        '    window.JSON &&' .
                                                        '    JSON.parse &&' .
                                                        '    JSON.stringify' .
                                                        ');'
-                                       ] ),
-                                       'test.z.foo' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.z.foo' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'dependencies' => [
                                                        'test.x.core',
                                                        'test.x.polyfill',
                                                        'test.y.polyfill',
                                                ],
-                                       ] ),
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -463,52 +535,62 @@ mw.loader.register( [
                                        ],
                                ],
                                'modules' => [
-                                       'test.blank' => new ResourceLoaderTestModule(),
-                                       'test.x.core' => new ResourceLoaderTestModule(),
-                                       'test.x.util' => new ResourceLoaderTestModule( [
+                                       'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+                                       'test.x.core' => [ 'class' => ResourceLoaderTestModule::class ],
+                                       'test.x.util' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'dependencies' => [
                                                        'test.x.core',
                                                ],
-                                       ] ),
-                                       'test.x.foo' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.x.foo' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'dependencies' => [
                                                        'test.x.core',
                                                ],
-                                       ] ),
-                                       'test.x.bar' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.x.bar' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'dependencies' => [
                                                        'test.x.core',
                                                        'test.x.util',
                                                ],
-                                       ] ),
-                                       'test.x.quux' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.x.quux' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'dependencies' => [
                                                        'test.x.foo',
                                                        'test.x.bar',
                                                        'test.x.util',
                                                        'test.x.unknown',
                                                ],
-                                       ] ),
-                                       'test.group.foo.1' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.group.foo.1' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'group' => 'x-foo',
-                                       ] ),
-                                       'test.group.foo.2' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.group.foo.2' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'group' => 'x-foo',
-                                       ] ),
-                                       'test.group.bar.1' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.group.bar.1' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'group' => 'x-bar',
-                                       ] ),
-                                       'test.group.bar.2' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.group.bar.2' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'group' => 'x-bar',
                                                'source' => 'example',
-                                       ] ),
-                                       'test.target.foo' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.target.foo' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'targets' => [ 'x-foo' ],
-                                       ] ),
-                                       'test.target.bar' => new ResourceLoaderTestModule( [
+                                       ],
+                                       'test.target.bar' => [
+                                               'class' => ResourceLoaderTestModule::class,
                                                'source' => 'example',
                                                'targets' => [ 'x-foo' ],
-                                       ] ),
+                                       ],
                                ],
                                'out' => '
 mw.loader.addSource( {
@@ -614,8 +696,9 @@ mw.loader.register( [
        public static function provideRegistrations() {
                return [
                        [ [
-                               'test.blank' => new ResourceLoaderTestModule(),
-                               'test.min' => new ResourceLoaderTestModule( [
+                               'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+                               'test.min' => [
+                                       'class' => ResourceLoaderTestModule::class,
                                        'skipFunction' =>
                                                'return !!(' .
                                                '    window.JSON &&' .
@@ -625,7 +708,7 @@ mw.loader.register( [
                                        'dependencies' => [
                                                'test.blank',
                                        ],
-                               ] ),
+                               ],
                        ] ]
                ];
        }
@@ -728,8 +811,8 @@ mw.loader.register( [
                $context1 = $this->getResourceLoaderContext();
                $rl1 = $context1->getResourceLoader();
                $rl1->register( [
-                       'test.a' => new ResourceLoaderTestModule(),
-                       'test.b' => new ResourceLoaderTestModule(),
+                       'test.a' => [ 'class' => ResourceLoaderTestModule::class ],
+                       'test.b' => [ 'class' => ResourceLoaderTestModule::class ],
                ] );
                $module = new ResourceLoaderStartupModule();
                $version1 = $module->getVersionHash( $context1 );
@@ -737,8 +820,8 @@ mw.loader.register( [
                $context2 = $this->getResourceLoaderContext();
                $rl2 = $context2->getResourceLoader();
                $rl2->register( [
-                       'test.b' => new ResourceLoaderTestModule(),
-                       'test.c' => new ResourceLoaderTestModule(),
+                       'test.b' => [ 'class' => ResourceLoaderTestModule::class ],
+                       'test.c' => [ 'class' => ResourceLoaderTestModule::class ],
                ] );
                $module = new ResourceLoaderStartupModule();
                $version2 = $module->getVersionHash( $context2 );
@@ -746,8 +829,11 @@ mw.loader.register( [
                $context3 = $this->getResourceLoaderContext();
                $rl3 = $context3->getResourceLoader();
                $rl3->register( [
-                       'test.a' => new ResourceLoaderTestModule(),
-                       'test.b' => new ResourceLoaderTestModule( [ 'script' => 'different' ] ),
+                       'test.a' => [ 'class' => ResourceLoaderTestModule::class ],
+                       'test.b' => [
+                               'class' => ResourceLoaderTestModule::class,
+                               'script' => 'different',
+                       ],
                ] );
                $module = new ResourceLoaderStartupModule();
                $version3 = $module->getVersionHash( $context3 );
@@ -773,7 +859,10 @@ mw.loader.register( [
                $context = $this->getResourceLoaderContext();
                $rl = $context->getResourceLoader();
                $rl->register( [
-                       'test.a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'x', 'y' ] ] ),
+                       'test.a' => [
+                               'class' => ResourceLoaderTestModule::class,
+                               'dependencies' => [ 'x', 'y' ],
+                       ],
                ] );
                $module = new ResourceLoaderStartupModule();
                $version1 = $module->getVersionHash( $context );
@@ -781,7 +870,10 @@ mw.loader.register( [
                $context = $this->getResourceLoaderContext();
                $rl = $context->getResourceLoader();
                $rl->register( [
-                       'test.a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'x', 'z' ] ] ),
+                       'test.a' => [
+                               'class' => ResourceLoaderTestModule::class,
+                               'dependencies' => [ 'x', 'z' ],
+                       ],
                ] );
                $module = new ResourceLoaderStartupModule();
                $version2 = $module->getVersionHash( $context );
index 544afae..f47bdaf 100644 (file)
@@ -70,28 +70,19 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
                $this->assertTrue( ResourceLoader::isValidModuleName( $name ) );
        }
 
-       /**
-        * @covers ResourceLoader::register
-        * @covers ResourceLoader::getModule
-        */
-       public function testRegisterValidObject() {
-               $module = new ResourceLoaderTestModule();
-               $resourceLoader = new EmptyResourceLoader();
-               $resourceLoader->register( 'test', $module );
-               $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
-       }
-
        /**
         * @covers ResourceLoader::register
         * @covers ResourceLoader::getModule
         */
        public function testRegisterValidArray() {
-               $module = new ResourceLoaderTestModule();
                $resourceLoader = new EmptyResourceLoader();
                // Covers case of register() setting $rl->moduleInfos,
                // but $rl->modules lazy-populated by getModule()
-               $resourceLoader->register( 'test', [ 'object' => $module ] );
-               $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
+               $resourceLoader->register( 'test', [ 'class' => ResourceLoaderTestModule::class ] );
+               $this->assertInstanceOf(
+                       ResourceLoaderTestModule::class,
+                       $resourceLoader->getModule( 'test' )
+               );
        }
 
        /**
@@ -99,10 +90,12 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
         * @group medium
         */
        public function testRegisterEmptyString() {
-               $module = new ResourceLoaderTestModule();
                $resourceLoader = new EmptyResourceLoader();
-               $resourceLoader->register( '', $module );
-               $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
+               $resourceLoader->register( '', [ 'class' => ResourceLoaderTestModule::class ] );
+               $this->assertInstanceOf(
+                       ResourceLoaderTestModule::class,
+                       $resourceLoader->getModule( '' )
+               );
        }
 
        /**
@@ -112,7 +105,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
        public function testRegisterInvalidName() {
                $resourceLoader = new EmptyResourceLoader();
                $this->setExpectedException( MWException::class, "name 'test!invalid' is invalid" );
-               $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
+               $resourceLoader->register( 'test!invalid', [] );
        }
 
        /**
@@ -133,11 +126,13 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
                        ->method( 'warning' );
                $resourceLoader = new EmptyResourceLoader( null, $logger );
 
-               $module1 = new ResourceLoaderTestModule();
-               $module2 = new ResourceLoaderTestModule();
-               $resourceLoader->register( 'test', $module1 );
-               $resourceLoader->register( 'test', $module2 );
-               $this->assertSame( $module2, $resourceLoader->getModule( 'test' ) );
+               $resourceLoader->register( 'test', [ 'class' => ResourceLoaderSkinModule::class ] );
+               $resourceLoader->register( 'test', [ 'class' => ResourceLoaderStartUpModule::class ] );
+               $this->assertInstanceOf(
+                       ResourceLoaderStartUpModule::class,
+                       $resourceLoader->getModule( 'test' ),
+                       'last one wins'
+               );
        }
 
        /**
@@ -146,8 +141,8 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
        public function testGetModuleNames() {
                // Use an empty one so that core and extension modules don't get in.
                $resourceLoader = new EmptyResourceLoader();
-               $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
-               $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
+               $resourceLoader->register( 'test.foo', [] );
+               $resourceLoader->register( 'test.bar', [] );
                $this->assertEquals(
                        [ 'startup', 'test.foo', 'test.bar' ],
                        $resourceLoader->getModuleNames()
@@ -155,15 +150,21 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
        }
 
        public function provideTestIsFileModule() {
-               $fileModuleObj = $this->getMockBuilder( ResourceLoaderFileModule::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
+               $fileModuleObj = $this->createMock( ResourceLoaderFileModule::class );
                return [
-                       'object' => [ false,
-                               new ResourceLoaderTestModule()
+                       'factory ignored' => [ false,
+                               [
+                                       'factory' => function () {
+                                               return new ResourceLoaderTestModule();
+                                       }
+                               ]
                        ],
-                       'FileModule object' => [ false,
-                               $fileModuleObj
+                       'factory ignored (actual FileModule)' => [ false,
+                               [
+                                       'factory' => function () use ( $fileModuleObj ) {
+                                               return $fileModuleObj;
+                                       }
+                               ]
                        ],
                        'simple empty' => [ true,
                                []
@@ -214,7 +215,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
         */
        public function testIsModuleRegistered() {
                $rl = new EmptyResourceLoader();
-               $rl->register( 'test', new ResourceLoaderTestModule() );
+               $rl->register( 'test', [] );
                $this->assertTrue( $rl->isModuleRegistered( 'test' ) );
                $this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
        }
@@ -709,9 +710,13 @@ END
                        // Disable log from outputErrorAndLog
                        ->setMethods( [ 'outputErrorAndLog' ] )->getMock();
                $rl->register( [
-                       'foo' => self::getSimpleModuleMock(),
-                       'ferry' => self::getFailFerryMock(),
-                       'bar' => self::getSimpleModuleMock(),
+                       'foo' => [ 'class' => ResourceLoaderTestModule::class ],
+                       'ferry' => [
+                               'factory' => function () {
+                                       return self::getFailFerryMock();
+                               }
+                       ],
+                       'bar' => [ 'class' => ResourceLoaderTestModule::class ],
                ] );
                $context = $this->getResourceLoaderContext( [], $rl );
 
@@ -800,7 +805,6 @@ END
                $modules = array_map( function ( $script ) {
                        return self::getSimpleModuleMock( $script );
                }, $scripts );
-               $rl->register( $modules );
 
                $context = $this->getResourceLoaderContext(
                        [
@@ -845,7 +849,6 @@ END
                        'bar' => self::getSimpleModuleMock( 'bar();' ),
                ];
                $rl = new EmptyResourceLoader();
-               $rl->register( $modules );
                $context = $this->getResourceLoaderContext(
                        [
                                'modules' => 'foo|ferry|bar',
@@ -885,7 +888,6 @@ END
                        'bar' => self::getSimpleStyleModuleMock( '.bar{}' ),
                ];
                $rl = new EmptyResourceLoader();
-               $rl->register( $modules );
                $context = $this->getResourceLoaderContext(
                        [
                                'modules' => 'foo|ferry|bar',
@@ -922,9 +924,15 @@ END
                // provide the full Config object here.
                $rl = new EmptyResourceLoader( MediaWikiServices::getInstance()->getMainConfig() );
                $rl->register( [
-                       'foo' => self::getSimpleModuleMock( 'foo();' ),
-                       'ferry' => self::getFailFerryMock(),
-                       'bar' => self::getSimpleModuleMock( 'bar();' ),
+                       'foo' => [ 'factory' => function () {
+                               return self::getSimpleModuleMock( 'foo();' );
+                       } ],
+                       'ferry' => [ 'factory' => function () {
+                               return self::getFailFerryMock();
+                       } ],
+                       'bar' => [ 'factory' => function () {
+                               return self::getSimpleModuleMock( 'bar();' );
+                       } ],
                ] );
                $context = $this->getResourceLoaderContext(
                        [
@@ -981,15 +989,12 @@ END
                ] );
 
                $rl = new EmptyResourceLoader();
-               $rl->register( [
-                       'foo' => $module,
-               ] );
                $context = $this->getResourceLoaderContext(
                        [ 'modules' => 'foo', 'only' => 'scripts' ],
                        $rl
                );
 
-               $modules = [ 'foo' => $rl->getModule( 'foo' ) ];
+               $modules = [ 'foo' => $module ];
                $response = $rl->makeModuleResponse( $context, $modules );
                $extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
 
@@ -1022,13 +1027,12 @@ END
                ] );
 
                $rl = new EmptyResourceLoader();
-               $rl->register( [ 'foo' => $foo, 'bar' => $bar ] );
                $context = $this->getResourceLoaderContext(
                        [ 'modules' => 'foo|bar', 'only' => 'scripts' ],
                        $rl
                );
 
-               $modules = [ 'foo' => $rl->getModule( 'foo' ), 'bar' => $rl->getModule( 'bar' ) ];
+               $modules = [ 'foo' => $foo, 'bar' => $bar ];
                $response = $rl->makeModuleResponse( $context, $modules );
                $extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
                $this->assertEquals(
@@ -1073,7 +1077,11 @@ END
                                'makeModuleResponse',
                        ] )
                        ->getMock();
-               $rl->register( 'test', $module );
+               $rl->register( 'test', [
+                       'factory' => function () use ( $module ) {
+                               return $module;
+                       }
+               ] );
                $context = $this->getResourceLoaderContext(
                        [ 'modules' => 'test', 'only' => null ],
                        $rl
@@ -1102,7 +1110,11 @@ END
                                'sendResponseHeaders',
                        ] )
                        ->getMock();
-               $rl->register( 'test', $module );
+               $rl->register( 'test', [
+                       'factory' => function () use ( $module ) {
+                               return $module;
+                       }
+               ] );
                $context = $this->getResourceLoaderContext( [ 'modules' => 'test' ], $rl );
                // Disable logging from outputErrorAndLog
                $this->setLogger( 'exception', new Psr\Log\NullLogger() );
index c1bdebe..e8a0884 100644 (file)
@@ -230,7 +230,6 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase {
                $module::$returnFetchTitleInfo = $titleInfo;
 
                $rl = new EmptyResourceLoader();
-               $rl->register( 'testmodule', $module );
                $context = new ResourceLoaderContext( $rl, new FauxRequest() );
 
                TestResourceLoaderWikiModule::invalidateModuleCache(
@@ -253,20 +252,16 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase {
         * @covers ResourceLoaderWikiModule::preloadTitleInfo
         */
        public function testGetPreloadedBadTitle() {
-               // Mock values
-               $pages = [
-                       // Covers else branch for invalid page name
-                       '[x]' => [ 'type' => 'styles' ],
-               ];
-               $titleInfo = [];
-
-               // Set up objects
-               $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class )
-                       ->setMethods( [ 'getPages' ] )->getMock();
-               $module->method( 'getPages' )->willReturn( $pages );
-               $module::$returnFetchTitleInfo = $titleInfo;
+               // Set up
+               TestResourceLoaderWikiModule::$returnFetchTitleInfo = [];
                $rl = new EmptyResourceLoader();
-               $rl->register( 'testmodule', $module );
+               $rl->getConfig()->set( 'UseSiteJs', true );
+               $rl->getConfig()->set( 'UseSiteCss', true );
+               $rl->register( 'testmodule', [
+                       'class' => TestResourceLoaderWikiModule::class,
+                       // Covers preloadTitleInfo branch for invalid page name
+                       'styles' => [ '[x]' ],
+               ] );
                $context = new ResourceLoaderContext( $rl, new FauxRequest() );
 
                // Act
@@ -277,8 +272,8 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase {
                );
 
                // Assert
-               $module = TestingAccessWrapper::newFromObject( $module );
-               $this->assertEquals( $titleInfo, $module->getTitleInfo( $context ), 'Title info' );
+               $module = TestingAccessWrapper::newFromObject( $rl->getModule( 'testmodule' ) );
+               $this->assertSame( [], $module->getTitleInfo( $context ), 'Title info' );
        }
 
        /**
@@ -371,7 +366,6 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase {
                $module->method( 'getPages' )->willReturn( $pages );
 
                $rl = new EmptyResourceLoader();
-               $rl->register( 'testmodule', $module );
                $context = new DerivativeResourceLoaderContext(
                        new ResourceLoaderContext( $rl, new FauxRequest() )
                );
index a74056d..0031cb3 100644 (file)
@@ -13,214 +13,6 @@ use Wikimedia\TestingAccessWrapper;
  */
 class SessionTest extends MediaWikiTestCase {
 
-       public function testConstructor() {
-               $backend = TestUtils::getDummySessionBackend();
-               TestingAccessWrapper::newFromObject( $backend )->requests = [ -1 => 'dummy' ];
-               TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
-
-               $session = new Session( $backend, 42, new \TestLogger );
-               $priv = TestingAccessWrapper::newFromObject( $session );
-               $this->assertSame( $backend, $priv->backend );
-               $this->assertSame( 42, $priv->index );
-
-               $request = new \FauxRequest();
-               $priv2 = TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
-               $this->assertSame( $backend, $priv2->backend );
-               $this->assertNotSame( $priv->index, $priv2->index );
-               $this->assertSame( $request, $priv2->getRequest() );
-       }
-
-       /**
-        * @dataProvider provideMethods
-        * @param string $m Method to test
-        * @param array $args Arguments to pass to the method
-        * @param bool $index Whether the backend method gets passed the index
-        * @param bool $ret Whether the method returns a value
-        */
-       public function testMethods( $m, $args, $index, $ret ) {
-               $mock = $this->getMockBuilder( DummySessionBackend::class )
-                       ->setMethods( [ $m, 'deregisterSession' ] )
-                       ->getMock();
-               $mock->expects( $this->once() )->method( 'deregisterSession' )
-                       ->with( $this->identicalTo( 42 ) );
-
-               $tmp = $mock->expects( $this->once() )->method( $m );
-               $expectArgs = [];
-               if ( $index ) {
-                       $expectArgs[] = $this->identicalTo( 42 );
-               }
-               foreach ( $args as $arg ) {
-                       $expectArgs[] = $this->identicalTo( $arg );
-               }
-               $tmp = call_user_func_array( [ $tmp, 'with' ], $expectArgs );
-
-               $retval = new \stdClass;
-               $tmp->will( $this->returnValue( $retval ) );
-
-               $session = TestUtils::getDummySession( $mock, 42 );
-
-               if ( $ret ) {
-                       $this->assertSame( $retval, call_user_func_array( [ $session, $m ], $args ) );
-               } else {
-                       $this->assertNull( call_user_func_array( [ $session, $m ], $args ) );
-               }
-
-               // Trigger Session destructor
-               $session = null;
-       }
-
-       public static function provideMethods() {
-               return [
-                       [ 'getId', [], false, true ],
-                       [ 'getSessionId', [], false, true ],
-                       [ 'resetId', [], false, true ],
-                       [ 'getProvider', [], false, true ],
-                       [ 'isPersistent', [], false, true ],
-                       [ 'persist', [], false, false ],
-                       [ 'unpersist', [], false, false ],
-                       [ 'shouldRememberUser', [], false, true ],
-                       [ 'setRememberUser', [ true ], false, false ],
-                       [ 'getRequest', [], true, true ],
-                       [ 'getUser', [], false, true ],
-                       [ 'getAllowedUserRights', [], false, true ],
-                       [ 'canSetUser', [], false, true ],
-                       [ 'setUser', [ new \stdClass ], false, false ],
-                       [ 'suggestLoginUsername', [], true, true ],
-                       [ 'shouldForceHTTPS', [], false, true ],
-                       [ 'setForceHTTPS', [ true ], false, false ],
-                       [ 'getLoggedOutTimestamp', [], false, true ],
-                       [ 'setLoggedOutTimestamp', [ 123 ], false, false ],
-                       [ 'getProviderMetadata', [], false, true ],
-                       [ 'save', [], false, false ],
-                       [ 'delaySave', [], false, true ],
-                       [ 'renew', [], false, false ],
-               ];
-       }
-
-       public function testDataAccess() {
-               $session = TestUtils::getDummySession();
-               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
-
-               $this->assertEquals( 1, $session->get( 'foo' ) );
-               $this->assertEquals( 'zero', $session->get( 0 ) );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertEquals( null, $session->get( 'null' ) );
-               $this->assertEquals( 'default', $session->get( 'null', 'default' ) );
-               $this->assertFalse( $backend->dirty );
-
-               $session->set( 'foo', 55 );
-               $this->assertEquals( 55, $backend->data['foo'] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->set( 1, 'one' );
-               $this->assertEquals( 'one', $backend->data[1] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->set( 1, 'one' );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertTrue( $session->exists( 'foo' ) );
-               $this->assertTrue( $session->exists( 1 ) );
-               $this->assertFalse( $session->exists( 'null' ) );
-               $this->assertFalse( $session->exists( 100 ) );
-               $this->assertFalse( $backend->dirty );
-
-               $session->remove( 'foo' );
-               $this->assertArrayNotHasKey( 'foo', $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-               $session->remove( 1 );
-               $this->assertArrayNotHasKey( 1, $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->remove( 101 );
-               $this->assertFalse( $backend->dirty );
-
-               $backend->data = [ 'a', 'b', '?' => 'c' ];
-               $this->assertSame( 3, $session->count() );
-               $this->assertSame( 3, count( $session ) );
-               $this->assertFalse( $backend->dirty );
-
-               $data = [];
-               foreach ( $session as $key => $value ) {
-                       $data[$key] = $value;
-               }
-               $this->assertEquals( $backend->data, $data );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertEquals( $backend->data, iterator_to_array( $session ) );
-               $this->assertFalse( $backend->dirty );
-       }
-
-       public function testArrayAccess() {
-               $logger = new \TestLogger;
-               $session = TestUtils::getDummySession( null, -1, $logger );
-               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
-
-               $this->assertEquals( 1, $session['foo'] );
-               $this->assertEquals( 'zero', $session[0] );
-               $this->assertFalse( $backend->dirty );
-
-               $logger->setCollect( true );
-               $this->assertEquals( null, $session['null'] );
-               $logger->setCollect( false );
-               $this->assertFalse( $backend->dirty );
-               $this->assertSame( [
-                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ]
-               ], $logger->getBuffer() );
-               $logger->clearBuffer();
-
-               $session['foo'] = 55;
-               $this->assertEquals( 55, $backend->data['foo'] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session[1] = 'one';
-               $this->assertEquals( 'one', $backend->data[1] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session[1] = 'one';
-               $this->assertFalse( $backend->dirty );
-
-               $session['bar'] = [ 'baz' => [] ];
-               $session['bar']['baz']['quux'] = 2;
-               $this->assertEquals( [ 'baz' => [ 'quux' => 2 ] ], $backend->data['bar'] );
-
-               $logger->setCollect( true );
-               $session['bar2']['baz']['quux'] = 3;
-               $logger->setCollect( false );
-               $this->assertEquals( [ 'baz' => [ 'quux' => 3 ] ], $backend->data['bar2'] );
-               $this->assertSame( [
-                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ]
-               ], $logger->getBuffer() );
-               $logger->clearBuffer();
-
-               $backend->dirty = false;
-               $this->assertTrue( isset( $session['foo'] ) );
-               $this->assertTrue( isset( $session[1] ) );
-               $this->assertFalse( isset( $session['null'] ) );
-               $this->assertFalse( isset( $session['missing'] ) );
-               $this->assertFalse( isset( $session[100] ) );
-               $this->assertFalse( $backend->dirty );
-
-               unset( $session['foo'] );
-               $this->assertArrayNotHasKey( 'foo', $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-               unset( $session[1] );
-               $this->assertArrayNotHasKey( 1, $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               unset( $session[101] );
-               $this->assertFalse( $backend->dirty );
-       }
-
        public function testClear() {
                $session = TestUtils::getDummySession();
                $priv = TestingAccessWrapper::newFromObject( $session );
@@ -268,66 +60,6 @@ class SessionTest extends MediaWikiTestCase {
                $this->assertTrue( $backend->dirty );
        }
 
-       public function testTokens() {
-               $session = TestUtils::getDummySession();
-               $priv = TestingAccessWrapper::newFromObject( $session );
-               $backend = $priv->backend;
-
-               $token = TestingAccessWrapper::newFromObject( $session->getToken() );
-               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
-               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
-               $secret = $backend->data['wsTokenSecrets']['default'];
-               $this->assertSame( $secret, $token->secret );
-               $this->assertSame( '', $token->salt );
-               $this->assertTrue( $token->wasNew() );
-
-               $token = TestingAccessWrapper::newFromObject( $session->getToken( 'foo' ) );
-               $this->assertSame( $secret, $token->secret );
-               $this->assertSame( 'foo', $token->salt );
-               $this->assertFalse( $token->wasNew() );
-
-               $backend->data['wsTokenSecrets']['secret'] = 'sekret';
-               $token = TestingAccessWrapper::newFromObject(
-                       $session->getToken( [ 'bar', 'baz' ], 'secret' )
-               );
-               $this->assertSame( 'sekret', $token->secret );
-               $this->assertSame( 'bar|baz', $token->salt );
-               $this->assertFalse( $token->wasNew() );
-
-               $session->resetToken( 'secret' );
-               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
-               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
-               $this->assertArrayNotHasKey( 'secret', $backend->data['wsTokenSecrets'] );
-
-               $session->resetAllTokens();
-               $this->assertArrayNotHasKey( 'wsTokenSecrets', $backend->data );
-       }
-
-       /**
-        * @dataProvider provideSecretsRoundTripping
-        * @param mixed $data
-        */
-       public function testSecretsRoundTripping( $data ) {
-               $session = TestUtils::getDummySession();
-
-               // Simple round-trip
-               $session->setSecret( 'secret', $data );
-               $this->assertNotEquals( $data, $session->get( 'secret' ) );
-               $this->assertEquals( $data, $session->getSecret( 'secret', 'defaulted' ) );
-       }
-
-       public static function provideSecretsRoundTripping() {
-               return [
-                       [ 'Foobar' ],
-                       [ 42 ],
-                       [ [ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
-                       [ (object)[ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
-                       [ true ],
-                       [ false ],
-                       [ null ],
-               ];
-       }
-
        public function testSecrets() {
                $logger = new \TestLogger;
                $session = TestUtils::getDummySession( null, -1, $logger );
@@ -370,4 +102,29 @@ class SessionTest extends MediaWikiTestCase {
                \Wikimedia\restoreWarnings();
        }
 
+       /**
+        * @dataProvider provideSecretsRoundTripping
+        * @param mixed $data
+        */
+       public function testSecretsRoundTripping( $data ) {
+               $session = TestUtils::getDummySession();
+
+               // Simple round-trip
+               $session->setSecret( 'secret', $data );
+               $this->assertNotEquals( $data, $session->get( 'secret' ) );
+               $this->assertEquals( $data, $session->getSecret( 'secret', 'defaulted' ) );
+       }
+
+       public static function provideSecretsRoundTripping() {
+               return [
+                       [ 'Foobar' ],
+                       [ 42 ],
+                       [ [ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
+                       [ (object)[ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
+                       [ true ],
+                       [ false ],
+                       [ null ],
+               ];
+       }
+
 }
diff --git a/tests/phpunit/includes/session/TokenTest.php b/tests/phpunit/includes/session/TokenTest.php
deleted file mode 100644 (file)
index 4797652..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use MediaWikiTestCase;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group Session
- * @covers MediaWiki\Session\Token
- */
-class TokenTest extends MediaWikiTestCase {
-
-       public function testBasics() {
-               $token = $this->getMockBuilder( Token::class )
-                       ->setMethods( [ 'toStringAtTimestamp' ] )
-                       ->setConstructorArgs( [ 'sekret', 'salty', true ] )
-                       ->getMock();
-               $token->expects( $this->any() )->method( 'toStringAtTimestamp' )
-                       ->will( $this->returnValue( 'faketoken+\\' ) );
-
-               $this->assertSame( 'faketoken+\\', $token->toString() );
-               $this->assertSame( 'faketoken+\\', (string)$token );
-               $this->assertTrue( $token->wasNew() );
-
-               $token = new Token( 'sekret', 'salty', false );
-               $this->assertFalse( $token->wasNew() );
-       }
-
-       public function testToStringAtTimestamp() {
-               $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) );
-
-               $this->assertSame(
-                       'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\',
-                       $token->toStringAtTimestamp( 1447362018 )
-               );
-               $this->assertSame(
-                       'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\',
-                       $token->toStringAtTimestamp( 1447362026 )
-               );
-       }
-
-       public function testGetTimestamp() {
-               $this->assertSame(
-                       1447362018, Token::getTimestamp( 'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\' )
-               );
-               $this->assertSame(
-                       1447362026, Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\' )
-               );
-               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) );
-               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be+\\' ) );
-
-               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9x76c224cfb400d43be5644fdea+\\' ) );
-       }
-
-       public function testMatch() {
-               $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) );
-
-               $test = $token->toStringAtTimestamp( time() - 10 );
-               $this->assertTrue( $token->match( $test ) );
-               $this->assertTrue( $token->match( $test, 12 ) );
-               $this->assertFalse( $token->match( $test, 8 ) );
-
-               $this->assertFalse( $token->match( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/shell/FirejailCommandTest.php b/tests/phpunit/includes/shell/FirejailCommandTest.php
deleted file mode 100644 (file)
index 681c3dc..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-/**
- * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
- *
- * 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.
- *
- */
-
-use MediaWiki\Shell\FirejailCommand;
-use MediaWiki\Shell\Shell;
-use Wikimedia\TestingAccessWrapper;
-
-class FirejailCommandTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function provideBuildFinalCommand() {
-               global $IP;
-               // phpcs:ignore Generic.Files.LineLength
-               $env = "'MW_INCLUDE_STDERR=;MW_CPU_LIMIT=180; MW_CGROUP='\'''\''; MW_MEM_LIMIT=307200; MW_FILE_SIZE_LIMIT=102400; MW_WALL_CLOCK_LIMIT=180; MW_USE_LOG_PIPE=yes'";
-               $limit = "/bin/bash '$IP/includes/shell/limit.sh'";
-               $profile = "--profile=$IP/includes/shell/firejail.profile";
-               $blacklist = '--blacklist=' . realpath( MW_CONFIG_FILE );
-               $default = "$blacklist --noroot --seccomp --private-dev";
-               return [
-                       [
-                               'No restrictions',
-                               'ls', 0, "$limit ''\''ls'\''' $env"
-                       ],
-                       [
-                               'default restriction',
-                               'ls', Shell::RESTRICT_DEFAULT,
-                               "$limit 'firejail --quiet $profile $default -- '\''ls'\''' $env"
-                       ],
-                       [
-                               'no network',
-                               'ls', Shell::NO_NETWORK,
-                               "$limit 'firejail --quiet $profile --net=none -- '\''ls'\''' $env"
-                       ],
-                       [
-                               'default restriction & no network',
-                               'ls', Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK,
-                               "$limit 'firejail --quiet $profile $default --net=none -- '\''ls'\''' $env"
-                       ],
-                       [
-                               'seccomp',
-                               'ls', Shell::SECCOMP,
-                               "$limit 'firejail --quiet $profile --seccomp -- '\''ls'\''' $env"
-                       ],
-                       [
-                               'seccomp & no execve',
-                               'ls', Shell::SECCOMP | Shell::NO_EXECVE,
-                               "$limit 'firejail --quiet $profile --shell=none --seccomp=execve -- '\''ls'\''' $env"
-                       ],
-               ];
-       }
-
-       /**
-        * @covers \MediaWiki\Shell\FirejailCommand::buildFinalCommand()
-        * @dataProvider provideBuildFinalCommand
-        */
-       public function testBuildFinalCommand( $desc, $params, $flags, $expected ) {
-               $command = new FirejailCommand( 'firejail' );
-               $command
-                       ->params( $params )
-                       ->restrict( $flags );
-               $wrapper = TestingAccessWrapper::newFromObject( $command );
-               $output = $wrapper->buildFinalCommand( $wrapper->command );
-               $this->assertEquals( $expected, $output[0], $desc );
-       }
-
-}
diff --git a/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php b/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php
deleted file mode 100644 (file)
index 15894a3..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-
-use MediaWiki\Site\MediaWikiPageNameNormalizer;
-
-/**
- * @covers MediaWiki\Site\MediaWikiPageNameNormalizer
- *
- * 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
- *
- * @since 1.27
- *
- * @group Site
- * @group medium
- *
- * @author Marius Hoch
- */
-class MediaWikiPageNameNormalizerTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @dataProvider normalizePageTitleProvider
-        */
-       public function testNormalizePageTitle( $expected, $pageName, $getResponse ) {
-               MediaWikiPageNameNormalizerTestMockHttp::$response = $getResponse;
-
-               $normalizer = new MediaWikiPageNameNormalizer(
-                       new MediaWikiPageNameNormalizerTestMockHttp()
-               );
-
-               $this->assertSame(
-                       $expected,
-                       $normalizer->normalizePageName( $pageName, 'https://www.wikidata.org/w/api.php' )
-               );
-       }
-
-       public function normalizePageTitleProvider() {
-               // Response are taken from wikidata and kkwiki using the following API request
-               // api.php?action=query&prop=info&redirects=1&converttitles=1&format=json&titles=…
-               return [
-                       'universe (Q1)' => [
-                               'Q1',
-                               'Q1',
-                               '{"batchcomplete":"","query":{"pages":{"129":{"pageid":129,"ns":0,'
-                               . '"title":"Q1","contentmodel":"wikibase-item","pagelanguage":"en",'
-                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
-                               . '"touched":"2016-06-23T05:11:21Z","lastrevid":350004448,"length":58001}}}}'
-                       ],
-                       'Q404 redirects to Q395' => [
-                               'Q395',
-                               'Q404',
-                               '{"batchcomplete":"","query":{"redirects":[{"from":"Q404","to":"Q395"}],"pages"'
-                               . ':{"601":{"pageid":601,"ns":0,"title":"Q395","contentmodel":"wikibase-item",'
-                               . '"pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
-                               . '"touched":"2016-06-23T08:00:20Z","lastrevid":350021914,"length":60108}}}}'
-                       ],
-                       'D converted to Д (Latin to Cyrillic) (taken from kkwiki)' => [
-                               'Д',
-                               'D',
-                               '{"batchcomplete":"","query":{"converted":[{"from":"D","to":"\u0414"}],'
-                               . '"pages":{"510541":{"pageid":510541,"ns":0,"title":"\u0414",'
-                               . '"contentmodel":"wikitext","pagelanguage":"kk","pagelanguagehtmlcode":"kk",'
-                               . '"pagelanguagedir":"ltr","touched":"2015-11-22T09:16:18Z",'
-                               . '"lastrevid":2373618,"length":3501}}}}'
-                       ],
-                       'there is no Q0' => [
-                               false,
-                               'Q0',
-                               '{"batchcomplete":"","query":{"pages":{"-1":{"ns":0,"title":"Q0",'
-                               . '"missing":"","contentmodel":"wikibase-item","pagelanguage":"en",'
-                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr"}}}}'
-                       ],
-                       'invalid title' => [
-                               false,
-                               '{{',
-                               '{"batchcomplete":"","query":{"pages":{"-1":{"title":"{{",'
-                               . '"invalidreason":"The requested page title contains invalid '
-                               . 'characters: \"{\".","invalid":""}}}}'
-                       ],
-                       'error on get' => [ false, 'ABC', false ]
-               ];
-       }
-
-}
-
-/**
- * @private
- * @see Http
- */
-class MediaWikiPageNameNormalizerTestMockHttp extends Http {
-
-       /**
-        * @var mixed
-        */
-       public static $response;
-
-       public static function get( $url, array $options = [], $caller = __METHOD__ ) {
-               PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
-               PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
-
-               return self::$response;
-       }
-}
index 5a978f9..bb72315 100644 (file)
@@ -1138,7 +1138,8 @@ class UserTest extends MediaWikiTestCase {
                $this->db->delete( 'actor', [ 'actor_user' => $id ], __METHOD__ );
                User::purge( $domain, $id );
                // Because WANObjectCache->delete() stupidly doesn't delete from the process cache.
-               ObjectCache::getMainWANInstance()->clearProcessCache();
+
+               MediaWikiServices::getInstance()->getMainWANObjectCache()->clearProcessCache();
 
                $user = User::newFromId( $id );
                $this->assertFalse( $user->getActorId() > 0, 'No Actor ID by default if none in database' );
diff --git a/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php
deleted file mode 100644 (file)
index a1a3fd7..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-/**
- * @covers ZipDirectoryReader
- * NOTE: this test is more like an integration test than a unit test
- */
-class ZipDirectoryReaderTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       protected $zipDir;
-       protected $entries;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->zipDir = __DIR__ . '/../../data/zip';
-       }
-
-       function zipCallback( $entry ) {
-               $this->entries[] = $entry;
-       }
-
-       function readZipAssertError( $file, $error, $assertMessage ) {
-               $this->entries = [];
-               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
-               $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
-       }
-
-       function readZipAssertSuccess( $file, $assertMessage ) {
-               $this->entries = [];
-               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
-               $this->assertTrue( $status->isOK(), $assertMessage );
-       }
-
-       public function testEmpty() {
-               $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
-       }
-
-       public function testMultiDisk0() {
-               $this->readZipAssertError( 'split.zip', 'zip-unsupported',
-                       'Split zip error' );
-       }
-
-       public function testNoSignature() {
-               $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
-                       'No signature should give "wrong format" error' );
-       }
-
-       public function testSimple() {
-               $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
-               $this->assertEquals( $this->entries, [ [
-                       'name' => 'Class.class',
-                       'mtime' => '20010115000000',
-                       'size' => 1,
-               ] ] );
-       }
-
-       public function testBadCentralEntrySignature() {
-               $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
-                       'Bad central entry error' );
-       }
-
-       public function testTrailingBytes() {
-               // Due to T40432 this is now zip-wrong-format instead of zip-bad
-               $this->readZipAssertError( 'trail.zip', 'zip-wrong-format',
-                       'Trailing bytes error' );
-       }
-
-       public function testWrongCDStart() {
-               $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
-                       'Wrong CD start disk error' );
-       }
-
-       public function testCentralDirectoryGap() {
-               $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
-                       'CD gap error' );
-       }
-
-       public function testCentralDirectoryTruncated() {
-               $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
-                       'CD truncated error (should hit unpack() overrun)' );
-       }
-
-       public function testLooksLikeZip64() {
-               $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
-                       'A file which looks like ZIP64 but isn\'t, should give error' );
-       }
-}
diff --git a/tests/phpunit/languages/LanguageCodeTest.php b/tests/phpunit/languages/LanguageCodeTest.php
deleted file mode 100644 (file)
index d8251bc..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-<?php
-
-/**
- * @covers LanguageCode
- * @group Language
- *
- * @author Thiemo Kreuz
- */
-class LanguageCodeTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testConstructor() {
-               $instance = new LanguageCode();
-
-               $this->assertInstanceOf( LanguageCode::class, $instance );
-       }
-
-       public function testGetDeprecatedCodeMapping() {
-               $map = LanguageCode::getDeprecatedCodeMapping();
-
-               $this->assertInternalType( 'array', $map );
-               $this->assertContainsOnly( 'string', array_keys( $map ) );
-               $this->assertArrayNotHasKey( '', $map );
-               $this->assertContainsOnly( 'string', $map );
-               $this->assertNotContains( '', $map );
-
-               // Codes special to MediaWiki should never appear in a map of "deprecated" codes
-               $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
-               $this->assertNotContains( 'qqq', $map, 'documentation' );
-               $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
-               $this->assertNotContains( 'qqx', $map, 'debug code' );
-
-               // Valid language codes that are currently not "deprecated"
-               $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
-               $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
-               $this->assertArrayNotHasKey( 'simple', $map );
-       }
-
-       public function testReplaceDeprecatedCodes() {
-               $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'als' ) );
-               $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'gsw' ) );
-               $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) );
-       }
-
-       /**
-        * test @see LanguageCode::bcp47().
-        * Please note the BCP 47 explicitly state that language codes are case
-        * insensitive, there are some exceptions to the rule :)
-        * This test is used to verify our formatting against all lower and
-        * all upper cases language code.
-        *
-        * @see https://tools.ietf.org/html/bcp47
-        * @dataProvider provideLanguageCodes()
-        */
-       public function testBcp47( $code, $expected ) {
-               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
-                       "Applying BCP 47 standard to '$code'"
-               );
-
-               $code = strtolower( $code );
-               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
-                       "Applying BCP 47 standard to lower case '$code'"
-               );
-
-               $code = strtoupper( $code );
-               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
-                       "Applying BCP 47 standard to upper case '$code'"
-               );
-       }
-
-       /**
-        * Array format is ($code, $expected)
-        */
-       public static function provideLanguageCodes() {
-               return [
-                       // Extracted from BCP 47 (list not exhaustive)
-                       # 2.1.1
-                       [ 'en-ca-x-ca', 'en-CA-x-ca' ],
-                       [ 'sgn-be-fr', 'sgn-BE-FR' ],
-                       [ 'az-latn-x-latn', 'az-Latn-x-latn' ],
-                       # 2.2
-                       [ 'sr-Latn-RS', 'sr-Latn-RS' ],
-                       [ 'az-arab-ir', 'az-Arab-IR' ],
-
-                       # 2.2.5
-                       [ 'sl-nedis', 'sl-nedis' ],
-                       [ 'de-ch-1996', 'de-CH-1996' ],
-
-                       # 2.2.6
-                       [
-                               'en-latn-gb-boont-r-extended-sequence-x-private',
-                               'en-Latn-GB-boont-r-extended-sequence-x-private'
-                       ],
-
-                       // Examples from BCP 47 Appendix A
-                       # Simple language subtag:
-                       [ 'DE', 'de' ],
-                       [ 'fR', 'fr' ],
-                       [ 'ja', 'ja' ],
-
-                       # Language subtag plus script subtag:
-                       [ 'zh-hans', 'zh-Hans' ],
-                       [ 'sr-cyrl', 'sr-Cyrl' ],
-                       [ 'sr-latn', 'sr-Latn' ],
-
-                       # Extended language subtags and their primary language subtag
-                       # counterparts:
-                       [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ],
-                       [ 'cmn-hans-cn', 'cmn-Hans-CN' ],
-                       [ 'zh-yue-hk', 'zh-yue-HK' ],
-                       [ 'yue-hk', 'yue-HK' ],
-
-                       # Language-Script-Region:
-                       [ 'zh-hans-cn', 'zh-Hans-CN' ],
-                       [ 'sr-latn-RS', 'sr-Latn-RS' ],
-
-                       # Language-Variant:
-                       [ 'sl-rozaj', 'sl-rozaj' ],
-                       [ 'sl-rozaj-biske', 'sl-rozaj-biske' ],
-                       [ 'sl-nedis', 'sl-nedis' ],
-
-                       # Language-Region-Variant:
-                       [ 'de-ch-1901', 'de-CH-1901' ],
-                       [ 'sl-it-nedis', 'sl-IT-nedis' ],
-
-                       # Language-Script-Region-Variant:
-                       [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ],
-
-                       # Language-Region:
-                       [ 'de-de', 'de-DE' ],
-                       [ 'en-us', 'en-US' ],
-                       [ 'es-419', 'es-419' ],
-
-                       # Private use subtags:
-                       [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ],
-                       [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ],
-                       /**
-                        * Previous test does not reflect the BCP 47 which states:
-                        *  az-Arab-x-AZE-derbend
-                        * AZE being private, it should be lower case, hence the test above
-                        * should probably be:
-                        * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ],
-                        */
-
-                       # Private use registry values:
-                       [ 'x-whatever', 'x-whatever' ],
-                       [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ],
-                       [ 'de-qaaa', 'de-Qaaa' ],
-                       [ 'sr-latn-qm', 'sr-Latn-QM' ],
-                       [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ],
-
-                       # Tags that use extensions
-                       [ 'en-us-u-islamcal', 'en-US-u-islamcal' ],
-                       [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ],
-                       [ 'en-a-myext-b-another', 'en-a-myext-b-another' ],
-
-                       # Invalid:
-                       // de-419-DE
-                       // a-DE
-                       // ar-a-aaa-b-bbb-a-ccc
-
-                       # Non-standard and deprecated language codes used by MediaWiki
-                       [ 'als', 'gsw' ],
-                       [ 'bat-smg', 'sgs' ],
-                       [ 'be-x-old', 'be-tarask' ],
-                       [ 'fiu-vro', 'vro' ],
-                       [ 'roa-rup', 'rup' ],
-                       [ 'zh-classical', 'lzh' ],
-                       [ 'zh-min-nan', 'nan' ],
-                       [ 'zh-yue', 'yue' ],
-                       [ 'cbk-zam', 'cbk' ],
-                       [ 'de-formal', 'de-x-formal' ],
-                       [ 'eml', 'egl' ],
-                       [ 'en-rtl', 'en-x-rtl' ],
-                       [ 'es-formal', 'es-x-formal' ],
-                       [ 'hu-formal', 'hu-x-formal' ],
-                       [ 'kk-Arab', 'kk-Arab' ],
-                       [ 'kk-Cyrl', 'kk-Cyrl' ],
-                       [ 'kk-Latn', 'kk-Latn' ],
-                       [ 'map-bms', 'jv-x-bms' ],
-                       [ 'mo', 'ro-Cyrl-MD' ],
-                       [ 'nrm', 'nrf' ],
-                       [ 'nl-informal', 'nl-x-informal' ],
-                       [ 'roa-tara', 'nap-x-tara' ],
-                       [ 'simple', 'en-simple' ],
-                       [ 'sr-ec', 'sr-Cyrl' ],
-                       [ 'sr-el', 'sr-Latn' ],
-                       [ 'zh-cn', 'zh-Hans-CN' ],
-                       [ 'zh-sg', 'zh-Hans-SG' ],
-                       [ 'zh-my', 'zh-Hans-MY' ],
-                       [ 'zh-tw', 'zh-Hant-TW' ],
-                       [ 'zh-hk', 'zh-Hant-HK' ],
-                       [ 'zh-mo', 'zh-Hant-MO' ],
-                       [ 'zh-hans', 'zh-Hans' ],
-                       [ 'zh-hant', 'zh-Hant' ],
-               ];
-       }
-
-}
index a762884..4f9664f 100644 (file)
@@ -45,53 +45,43 @@ class ResourcesTest extends MediaWikiTestCase {
        }
 
        /**
-        * Verify that nothing depends on "startup".
+        * Verify that all modules specified as dependencies of other modules actually
+        * exist and are not illegal.
         *
-        * Depending on it is unsupported as it cannot be loaded by the client.
-        *
-        * @todo Modules can dynamically choose dependencies based on context. This method does not
-        * test such dependencies. The same goes for testMissingDependencies() and
-        * testUnsatisfiableDependencies().
+        * @todo Modules can dynamically choose dependencies based on context. This method
+        * does not find all such variations. The same applies to testUnsatisfiableDependencies().
         */
-       public function testIllegalDependencies() {
+       public function testValidDependencies() {
                $data = self::getAllModules();
-
-               $illegalDeps = [];
-               foreach ( $data['modules'] as $moduleName => $module ) {
-                       if ( $module instanceof ResourceLoaderStartUpModule ) {
-                               $illegalDeps[] = $moduleName;
-                       }
-               }
-
-               /** @var ResourceLoaderModule $module */
-               foreach ( $data['modules'] as $moduleName => $module ) {
-                       foreach ( $illegalDeps as $illegalDep ) {
-                               $this->assertNotContains(
-                                       $illegalDep,
-                                       $module->getDependencies( $data['context'] ),
-                                       "Module '$moduleName' must not depend on '$illegalDep'"
-                               );
-                       }
-               }
-       }
-
-       /**
-        * Verify that all modules specified as dependencies of other modules actually exist.
-        */
-       public function testMissingDependencies() {
-               $data = self::getAllModules();
-               $validDeps = array_keys( $data['modules'] );
+               $knownDeps = array_keys( $data['modules'] );
+               $illegalDeps = [ 'startup' ];
+
+               // Avoid an assert for each module to keep the test fast.
+               // Instead, perform a single assertion against everything at once.
+               // When all is good, actual/expected are both empty arrays.
+               // When we find issues, add the violations to 'actual' and add an empty
+               // key to 'expected'. These keys in expected are because the PHPUnit diff
+               // (as of 6.5) only goes one level deep.
+               $actualUnknown = [];
+               $expectedUnknown = [];
+               $actualIllegal = [];
+               $expectedIllegal = [];
 
                /** @var ResourceLoaderModule $module */
                foreach ( $data['modules'] as $moduleName => $module ) {
                        foreach ( $module->getDependencies( $data['context'] ) as $dep ) {
-                               $this->assertContains(
-                                       $dep,
-                                       $validDeps,
-                                       "The module '$dep' required by '$moduleName' must exist"
-                               );
+                               if ( !in_array( $dep, $knownDeps, true ) ) {
+                                       $actualUnknown[$moduleName][] = $dep;
+                                       $expectedUnknown[$moduleName] = [];
+                               }
+                               if ( in_array( $dep, $illegalDeps, true ) ) {
+                                       $actualIllegal[$moduleName][] = $dep;
+                                       $expectedIllegal[$moduleName] = [];
+                               }
                        }
                }
+               $this->assertEquals( $expectedUnknown, $actualUnknown, 'Dependencies that do not exist' );
+               $this->assertEquals( $expectedIllegal, $actualIllegal, 'Dependencies that are not legal' );
        }
 
        /**
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfAppendQueryTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfAppendQueryTest.php
new file mode 100644 (file)
index 0000000..11d4475
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfAppendQuery
+ */
+class WfAppendQueryTest extends MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideAppendQuery
+        */
+       public function testAppendQuery( $url, $query, $expected, $message = null ) {
+               $this->assertEquals( $expected, wfAppendQuery( $url, $query ), $message );
+       }
+
+       public static function provideAppendQuery() {
+               return [
+                       [
+                               'http://www.example.org/index.php',
+                               '',
+                               'http://www.example.org/index.php',
+                               'No query'
+                       ],
+                       [
+                               'http://www.example.org/index.php',
+                               [ 'foo' => 'bar' ],
+                               'http://www.example.org/index.php?foo=bar',
+                               'Set query array'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foz=baz',
+                               'foo=bar',
+                               'http://www.example.org/index.php?foz=baz&foo=bar',
+                               'Set query string'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               '',
+                               'http://www.example.org/index.php?foo=bar',
+                               'Empty string with query'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               [ 'baz' => 'quux' ],
+                               'http://www.example.org/index.php?foo=bar&baz=quux',
+                               'Add query array'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               'baz=quux',
+                               'http://www.example.org/index.php?foo=bar&baz=quux',
+                               'Add query string'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               [ 'baz' => 'quux', 'foo' => 'baz' ],
+                               'http://www.example.org/index.php?foo=bar&baz=quux&foo=baz',
+                               'Modify query array'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               'baz=quux&foo=baz',
+                               'http://www.example.org/index.php?foo=bar&baz=quux&foo=baz',
+                               'Modify query string'
+                       ],
+                       [
+                               'http://www.example.org/index.php#baz',
+                               'foo=bar',
+                               'http://www.example.org/index.php?foo=bar#baz',
+                               'URL with fragment'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar#baz',
+                               'quux=blah',
+                               'http://www.example.org/index.php?foo=bar&quux=blah#baz',
+                               'URL with query string and fragment'
+                       ]
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfArrayPlus2dTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfArrayPlus2dTest.php
new file mode 100644 (file)
index 0000000..9c3e56c
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfArrayPlus2d
+ */
+class WfArrayPlus2dTest extends MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideArrays
+        */
+       public function testWfArrayPlus2d( $baseArray, $newValues, $expected, $testName ) {
+               $this->assertEquals(
+                       $expected,
+                       wfArrayPlus2d( $baseArray, $newValues ),
+                       $testName
+               );
+       }
+
+       /**
+        * Provider for testing wfArrayPlus2d
+        *
+        * @return array
+        */
+       public static function provideArrays() {
+               return [
+                       // target array, new values array, expected result
+                       [
+                               [ 0 => '1dArray' ],
+                               [ 1 => '1dArray' ],
+                               [ 0 => '1dArray', 1 => '1dArray' ],
+                               "Test simple union of two arrays with different keys",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => '2dArray' ],
+                               ],
+                               [
+                                       0 => [ 1 => '2dArray' ],
+                               ],
+                               [
+                                       0 => [ 0 => '2dArray', 1 => '2dArray' ],
+                               ],
+                               "Test union of 2d arrays with different keys in the value array",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => '2dArray' ],
+                               ],
+                               [
+                                       0 => [ 0 => '1dArray' ],
+                               ],
+                               [
+                                       0 => [ 0 => '2dArray' ],
+                               ],
+                               "Test union of 2d arrays with same keys in the value array",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 1 => '2dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               "Test union of 3d array with different keys",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 1 => [ 0 => '2dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ], 1 => [ 0 => '2dArray' ] ],
+                               ],
+                               "Test union of 3d array with different keys in the value array",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 0 => '2dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               "Test union of 3d array with same keys in the value array",
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfAssembleUrlTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfAssembleUrlTest.php
new file mode 100644 (file)
index 0000000..f1f1986
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfAssembleUrl
+ */
+class WfAssembleUrlTest extends MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideURLParts
+        */
+       public function testWfAssembleUrl( $parts, $output ) {
+               $partsDump = print_r( $parts, true );
+               $this->assertEquals(
+                       $output,
+                       wfAssembleUrl( $parts ),
+                       "Testing $partsDump assembles to $output"
+               );
+       }
+
+       /**
+        * Provider of URL parts for testing wfAssembleUrl()
+        *
+        * @return array
+        */
+       public static function provideURLParts() {
+               $schemes = [
+                       '' => [],
+                       '//' => [
+                               'delimiter' => '//',
+                       ],
+                       'http://' => [
+                               'scheme' => 'http',
+                               'delimiter' => '://',
+                       ],
+               ];
+
+               $hosts = [
+                       '' => [],
+                       'example.com' => [
+                               'host' => 'example.com',
+                       ],
+                       'example.com:123' => [
+                               'host' => 'example.com',
+                               'port' => 123,
+                       ],
+                       'id@example.com' => [
+                               'user' => 'id',
+                               'host' => 'example.com',
+                       ],
+                       'id@example.com:123' => [
+                               'user' => 'id',
+                               'host' => 'example.com',
+                               'port' => 123,
+                       ],
+                       'id:key@example.com' => [
+                               'user' => 'id',
+                               'pass' => 'key',
+                               'host' => 'example.com',
+                       ],
+                       'id:key@example.com:123' => [
+                               'user' => 'id',
+                               'pass' => 'key',
+                               'host' => 'example.com',
+                               'port' => 123,
+                       ],
+               ];
+
+               $cases = [];
+               foreach ( $schemes as $scheme => $schemeParts ) {
+                       foreach ( $hosts as $host => $hostParts ) {
+                               foreach ( [ '', '/path' ] as $path ) {
+                                       foreach ( [ '', 'query' ] as $query ) {
+                                               foreach ( [ '', 'fragment' ] as $fragment ) {
+                                                       $parts = array_merge(
+                                                               $schemeParts,
+                                                               $hostParts
+                                                       );
+                                                       $url = $scheme .
+                                                               $host .
+                                                               $path;
+
+                                                       if ( $path ) {
+                                                               $parts['path'] = $path;
+                                                       }
+                                                       if ( $query ) {
+                                                               $parts['query'] = $query;
+                                                               $url .= '?' . $query;
+                                                       }
+                                                       if ( $fragment ) {
+                                                               $parts['fragment'] = $fragment;
+                                                               $url .= '#' . $fragment;
+                                                       }
+
+                                                       $cases[] = [
+                                                               $parts,
+                                                               $url,
+                                                       ];
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               $complexURL = 'http://id:key@example.org:321' .
+                       '/over/there?name=ferret&foo=bar#nose';
+               $cases[] = [
+                       wfParseUrl( $complexURL ),
+                       $complexURL,
+               ];
+
+               return $cases;
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfBaseNameTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfBaseNameTest.php
new file mode 100644 (file)
index 0000000..4d55fb7
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfBaseName
+ */
+class WfBaseNameTest extends MediaWikiUnitTestCase {
+       /**
+        * @dataProvider providePaths
+        */
+       public function testBaseName( $fullpath, $basename ) {
+               $this->assertEquals( $basename, wfBaseName( $fullpath ),
+                       "wfBaseName('$fullpath') => '$basename'" );
+       }
+
+       public static function providePaths() {
+               return [
+                       [ '', '' ],
+                       [ '/', '' ],
+                       [ '\\', '' ],
+                       [ '//', '' ],
+                       [ '\\\\', '' ],
+                       [ 'a', 'a' ],
+                       [ 'aaaa', 'aaaa' ],
+                       [ '/a', 'a' ],
+                       [ '\\a', 'a' ],
+                       [ '/aaaa', 'aaaa' ],
+                       [ '\\aaaa', 'aaaa' ],
+                       [ '/aaaa/', 'aaaa' ],
+                       [ '\\aaaa\\', 'aaaa' ],
+                       [ '\\aaaa\\', 'aaaa' ],
+                       [
+                               '/mnt/upload3/wikipedia/en/thumb/8/8b/'
+                                       . 'Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg',
+                               '93px-Zork_Grand_Inquisitor_box_cover.jpg'
+                       ],
+                       [ 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE', 'VIEWER.EXE' ],
+                       [ 'Östergötland_coat_of_arms.png', 'Östergötland_coat_of_arms.png' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfEscapeShellArgTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfEscapeShellArgTest.php
new file mode 100644 (file)
index 0000000..d86b397
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfEscapeShellArg
+ */
+class WfEscapeShellArgTest extends MediaWikiUnitTestCase {
+       public function testSingleInput() {
+               if ( wfIsWindows() ) {
+                       $expected = '"blah"';
+               } else {
+                       $expected = "'blah'";
+               }
+
+               $actual = wfEscapeShellArg( 'blah' );
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public function testMultipleArgs() {
+               if ( wfIsWindows() ) {
+                       $expected = '"foo" "bar" "baz"';
+               } else {
+                       $expected = "'foo' 'bar' 'baz'";
+               }
+
+               $actual = wfEscapeShellArg( 'foo', 'bar', 'baz' );
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public function testMultipleArgsAsArray() {
+               if ( wfIsWindows() ) {
+                       $expected = '"foo" "bar" "baz"';
+               } else {
+                       $expected = "'foo' 'bar' 'baz'";
+               }
+
+               $actual = wfEscapeShellArg( [ 'foo', 'bar', 'baz' ] );
+
+               $this->assertEquals( $expected, $actual );
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfGetCallerTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfGetCallerTest.php
new file mode 100644 (file)
index 0000000..ae397d5
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfGetCaller
+ */
+class WfGetCallerTest extends MediaWikiUnitTestCase {
+       public function testZero() {
+               $this->assertEquals( 'WfGetCallerTest->testZero', wfGetCaller( 1 ) );
+       }
+
+       function callerOne() {
+               return wfGetCaller();
+       }
+
+       public function testOne() {
+               $this->assertEquals( 'WfGetCallerTest->testOne', self::callerOne() );
+       }
+
+       static function intermediateFunction( $level = 2, $n = 0 ) {
+               if ( $n > 0 ) {
+                       return self::intermediateFunction( $level, $n - 1 );
+               }
+
+               return wfGetCaller( $level );
+       }
+
+       public function testTwo() {
+               $this->assertEquals( 'WfGetCallerTest->testTwo', self::intermediateFunction() );
+       }
+
+       public function testN() {
+               $this->assertEquals( 'WfGetCallerTest->testN', self::intermediateFunction( 2, 0 ) );
+               $this->assertEquals(
+                       'WfGetCallerTest::intermediateFunction',
+                       self::intermediateFunction( 1, 0 )
+               );
+
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $this->assertEquals(
+                               'WfGetCallerTest::intermediateFunction',
+                               self::intermediateFunction( $i + 1, $i )
+                       );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php
new file mode 100644 (file)
index 0000000..199fa21
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfRemoveDotSegments
+ */
+class WfRemoveDotSegmentsTest extends MediaWikiUnitTestCase {
+       /**
+        * @dataProvider providePaths
+        */
+       public function testWfRemoveDotSegments( $inputPath, $outputPath ) {
+               $this->assertEquals(
+                       $outputPath,
+                       wfRemoveDotSegments( $inputPath ),
+                       "Testing $inputPath expands to $outputPath"
+               );
+       }
+
+       /**
+        * Provider of URL paths for testing wfRemoveDotSegments()
+        *
+        * @return array
+        */
+       public static function providePaths() {
+               return [
+                       [ '/a/b/c/./../../g', '/a/g' ],
+                       [ 'mid/content=5/../6', 'mid/6' ],
+                       [ '/a//../b', '/a/b' ],
+                       [ '/.../a', '/.../a' ],
+                       [ '.../a', '.../a' ],
+                       [ '', '' ],
+                       [ '/', '/' ],
+                       [ '//', '//' ],
+                       [ '.', '' ],
+                       [ '..', '' ],
+                       [ '...', '...' ],
+                       [ '/.', '/' ],
+                       [ '/..', '/' ],
+                       [ './', '' ],
+                       [ '../', '' ],
+                       [ './a', 'a' ],
+                       [ '../a', 'a' ],
+                       [ '../../a', 'a' ],
+                       [ '.././a', 'a' ],
+                       [ './../a', 'a' ],
+                       [ '././a', 'a' ],
+                       [ '../../', '' ],
+                       [ '.././', '' ],
+                       [ './../', '' ],
+                       [ '././', '' ],
+                       [ '../..', '' ],
+                       [ '../.', '' ],
+                       [ './..', '' ],
+                       [ './.', '' ],
+                       [ '/../../a', '/a' ],
+                       [ '/.././a', '/a' ],
+                       [ '/./../a', '/a' ],
+                       [ '/././a', '/a' ],
+                       [ '/../../', '/' ],
+                       [ '/.././', '/' ],
+                       [ '/./../', '/' ],
+                       [ '/././', '/' ],
+                       [ '/../..', '/' ],
+                       [ '/../.', '/' ],
+                       [ '/./..', '/' ],
+                       [ '/./.', '/' ],
+                       [ 'b/../../a', '/a' ],
+                       [ 'b/.././a', '/a' ],
+                       [ 'b/./../a', '/a' ],
+                       [ 'b/././a', 'b/a' ],
+                       [ 'b/../../', '/' ],
+                       [ 'b/.././', '/' ],
+                       [ 'b/./../', '/' ],
+                       [ 'b/././', 'b/' ],
+                       [ 'b/../..', '/' ],
+                       [ 'b/../.', '/' ],
+                       [ 'b/./..', '/' ],
+                       [ 'b/./.', 'b/' ],
+                       [ '/b/../../a', '/a' ],
+                       [ '/b/.././a', '/a' ],
+                       [ '/b/./../a', '/a' ],
+                       [ '/b/././a', '/b/a' ],
+                       [ '/b/../../', '/' ],
+                       [ '/b/.././', '/' ],
+                       [ '/b/./../', '/' ],
+                       [ '/b/././', '/b/' ],
+                       [ '/b/../..', '/' ],
+                       [ '/b/../.', '/' ],
+                       [ '/b/./..', '/' ],
+                       [ '/b/./.', '/b/' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfShorthandToIntegerTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfShorthandToIntegerTest.php
new file mode 100644 (file)
index 0000000..c2d1f4a
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfShorthandToInteger
+ */
+class WfShorthandToIntegerTest extends MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideABunchOfShorthands
+        */
+       public function testWfShorthandToInteger( $input, $output, $description ) {
+               $this->assertEquals(
+                       wfShorthandToInteger( $input ),
+                       $output,
+                       $description
+               );
+       }
+
+       public static function provideABunchOfShorthands() {
+               return [
+                       [ '', -1, 'Empty string' ],
+                       [ '     ', -1, 'String of spaces' ],
+                       [ '1G', 1024 * 1024 * 1024, 'One gig uppercased' ],
+                       [ '1g', 1024 * 1024 * 1024, 'One gig lowercased' ],
+                       [ '1M', 1024 * 1024, 'One meg uppercased' ],
+                       [ '1m', 1024 * 1024, 'One meg lowercased' ],
+                       [ '1K', 1024, 'One kb uppercased' ],
+                       [ '1k', 1024, 'One kb lowercased' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfStringToBoolTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfStringToBoolTest.php
new file mode 100644 (file)
index 0000000..b99d695
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfStringToBool
+ */
+class WfStringToBoolTest extends MediaWikiUnitTestCase {
+
+       public function getTestCases() {
+               return [
+                       [ 'true', true ],
+                       [ 'on', true ],
+                       [ 'yes', true ],
+                       [ 'TRUE', true ],
+                       [ 'YeS', true ],
+                       [ 'On', true ],
+                       [ '1', true ],
+                       [ '+1', true ],
+                       [ '01', true ],
+                       [ '-001', true ],
+                       [ '  1', true ],
+                       [ '-1  ', true ],
+                       [ '', false ],
+                       [ '0', false ],
+                       [ 'false', false ],
+                       [ 'NO', false ],
+                       [ 'NOT', false ],
+                       [ 'never', false ],
+                       [ '!&', false ],
+                       [ '-0', false ],
+                       [ '+0', false ],
+                       [ 'forget about it', false ],
+                       [ ' on', false ],
+                       [ 'true ', false ],
+               ];
+       }
+
+       /**
+        * @dataProvider getTestCases
+        * @param string $str
+        * @param bool $bool
+        */
+       public function testStr2Bool( $str, $bool ) {
+               if ( $bool ) {
+                       $this->assertTrue( wfStringToBool( $str ) );
+               } else {
+                       $this->assertFalse( wfStringToBool( $str ) );
+               }
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfTimestampTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfTimestampTest.php
new file mode 100644 (file)
index 0000000..94347bd
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfTimestamp
+ */
+class WfTimestampTest extends MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideNormalTimestamps
+        */
+       public function testNormalTimestamps( $input, $format, $output, $desc ) {
+               $this->assertEquals( $output, wfTimestamp( $format, $input ), $desc );
+       }
+
+       public static function provideNormalTimestamps() {
+               $t = gmmktime( 12, 34, 56, 1, 15, 2001 );
+
+               return [
+                       // TS_UNIX
+                       [ $t, TS_MW, '20010115123456', 'TS_UNIX to TS_MW' ],
+                       [ -30281104, TS_MW, '19690115123456', 'Negative TS_UNIX to TS_MW' ],
+                       [ $t, TS_UNIX, 979562096, 'TS_UNIX to TS_UNIX' ],
+                       [ $t, TS_DB, '2001-01-15 12:34:56', 'TS_UNIX to TS_DB' ],
+                       [ $t + 0.01, TS_MW, '20010115123456', 'TS_UNIX float to TS_MW' ],
+
+                       [ $t, TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_ISO_8601_BASIC to TS_DB' ],
+
+                       // TS_MW
+                       [ '20010115123456', TS_MW, '20010115123456', 'TS_MW to TS_MW' ],
+                       [ '20010115123456', TS_UNIX, 979562096, 'TS_MW to TS_UNIX' ],
+                       [ '20010115123456', TS_DB, '2001-01-15 12:34:56', 'TS_MW to TS_DB' ],
+                       [ '20010115123456', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_MW to TS_ISO_8601_BASIC' ],
+
+                       // TS_DB
+                       [ '2001-01-15 12:34:56', TS_MW, '20010115123456', 'TS_DB to TS_MW' ],
+                       [ '2001-01-15 12:34:56', TS_UNIX, 979562096, 'TS_DB to TS_UNIX' ],
+                       [ '2001-01-15 12:34:56', TS_DB, '2001-01-15 12:34:56', 'TS_DB to TS_DB' ],
+                       [
+                               '2001-01-15 12:34:56',
+                               TS_ISO_8601_BASIC,
+                               '20010115T123456Z',
+                               'TS_DB to TS_ISO_8601_BASIC'
+                       ],
+
+                       # rfc2822 section 3.3
+                       [ '20010115123456', TS_RFC2822, 'Mon, 15 Jan 2001 12:34:56 GMT', 'TS_MW to TS_RFC2822' ],
+                       [ 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ],
+                       [
+                               ' Mon, 15 Jan 2001 12:34:56 GMT',
+                               TS_MW,
+                               '20010115123456',
+                               'TS_RFC2822 with leading space to TS_MW'
+                       ],
+                       [
+                               '15 Jan 2001 12:34:56 GMT',
+                               TS_MW,
+                               '20010115123456',
+                               'TS_RFC2822 without optional day-of-week to TS_MW'
+                       ],
+
+                       # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space
+                       # obs-FWS = 1*WSP *(CRLF 1*WSP) ; Section 4.2
+                       [ 'Mon, 15         Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ],
+
+                       # WSP = SP / HTAB ; rfc2234
+                       [
+                               "Mon, 15 Jan\x092001 12:34:56 GMT",
+                               TS_MW,
+                               '20010115123456',
+                               'TS_RFC2822 with HTAB to TS_MW'
+                       ],
+                       [
+                               "Mon, 15 Jan\x09 \x09  2001 12:34:56 GMT",
+                               TS_MW,
+                               '20010115123456',
+                               'TS_RFC2822 with HTAB and SP to TS_MW'
+                       ],
+                       [
+                               'Sun, 6 Nov 94 08:49:37 GMT',
+                               TS_MW,
+                               '19941106084937',
+                               'TS_RFC2822 with obsolete year to TS_MW'
+                       ],
+               ];
+       }
+
+       /**
+        * This test checks wfTimestamp() with values outside.
+        * It needs PHP 64 bits or PHP > 5.1.
+        * See r74778 and T27451
+        * @dataProvider provideOldTimestamps
+        */
+       public function testOldTimestamps( $input, $outputType, $output, $message ) {
+               $timestamp = wfTimestamp( $outputType, $input );
+               if ( substr( $output, 0, 1 ) === '/' ) {
+                       // T66946: Day of the week calculations for very old
+                       // timestamps varies from system to system.
+                       $this->assertRegExp( $output, $timestamp, $message );
+               } else {
+                       $this->assertEquals( $output, $timestamp, $message );
+               }
+       }
+
+       public static function provideOldTimestamps() {
+               return [
+                       [
+                               '19011213204554',
+                               TS_RFC2822,
+                               'Fri, 13 Dec 1901 20:45:54 GMT',
+                               'Earliest time according to PHP documentation'
+                       ],
+                       [ '20380119031407', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:07 GMT', 'Latest 32 bit time' ],
+                       [ '19011213204552', TS_UNIX, '-2147483648', 'Earliest 32 bit unix time' ],
+                       [ '20380119031407', TS_UNIX, '2147483647', 'Latest 32 bit unix time' ],
+                       [ '19011213204552', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:52 GMT', 'Earliest 32 bit time' ],
+                       [
+                               '19011213204551',
+                               TS_RFC2822,
+                               'Fri, 13 Dec 1901 20:45:51 GMT', 'Earliest 32 bit time - 1'
+                       ],
+                       [ '20380119031408', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:08 GMT', 'Latest 32 bit time + 1' ],
+                       [ '19011212000000', TS_MW, '19011212000000', 'Convert to itself r74778#c10645' ],
+                       [ '19011213204551', TS_UNIX, '-2147483649', 'Earliest 32 bit unix time - 1' ],
+                       [ '20380119031408', TS_UNIX, '2147483648', 'Latest 32 bit unix time + 1' ],
+                       [ '-2147483649', TS_MW, '19011213204551', '1901 negative unix time to MediaWiki' ],
+                       [ '-5331871504', TS_MW, '18010115123456', '1801 negative unix time to MediaWiki' ],
+                       [
+                               '0117-08-09 12:34:56',
+                               TS_RFC2822,
+                               '/, 09 Aug 0117 12:34:56 GMT$/',
+                               'Death of Roman Emperor [[Trajan]]'
+                       ],
+
+                       /* @todo FIXME: 00 to 101 years are taken as being in [1970-2069] */
+                       [ '-58979923200', TS_RFC2822, '/, 01 Jan 0101 00:00:00 GMT$/', '1/1/101' ],
+                       [ '-62135596800', TS_RFC2822, 'Mon, 01 Jan 0001 00:00:00 GMT', 'Year 1' ],
+
+                       /* It is not clear if we should generate a year 0 or not
+                        * We are completely off RFC2822 requirement of year being
+                        * 1900 or later.
+                        */
+                       [
+                               '-62142076800',
+                               TS_RFC2822,
+                               'Wed, 18 Oct 0000 00:00:00 GMT',
+                               'ISO 8601:2004 [[year 0]], also called [[1 BC]]'
+                       ],
+               ];
+       }
+
+       /**
+        * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
+        * @dataProvider provideHttpDates
+        */
+       public function testHttpDate( $input, $output, $desc ) {
+               $this->assertEquals( $output, wfTimestamp( TS_MW, $input ), $desc );
+       }
+
+       public static function provideHttpDates() {
+               return [
+                       [ 'Sun, 06 Nov 1994 08:49:37 GMT', '19941106084937', 'RFC 822 date' ],
+                       [ 'Sunday, 06-Nov-94 08:49:37 GMT', '19941106084937', 'RFC 850 date' ],
+                       [ 'Sun Nov  6 08:49:37 1994', '19941106084937', "ANSI C's asctime() format" ],
+                       // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171
+                       [
+                               'Mon, 22 Nov 2010 14:12:42 GMT; length=52626',
+                               '20101122141242',
+                               'Netscape extension to HTTP/1.0'
+                       ],
+               ];
+       }
+
+       /**
+        * There are a number of assumptions in our codebase where wfTimestamp()
+        * should give the current date but it is not given a 0 there. See r71751 CR
+        */
+       public function testTimestampParameter() {
+               $now = wfTimestamp( TS_UNIX );
+               // We check that wfTimestamp doesn't return false (error) and use a LessThan assert
+               // for the cases where the test is run in a second boundary.
+
+               $zero = wfTimestamp( TS_UNIX, 0 );
+               $this->assertNotEquals( false, $zero );
+               $this->assertLessThan( 5, $zero - $now );
+
+               $empty = wfTimestamp( TS_UNIX, '' );
+               $this->assertNotEquals( false, $empty );
+               $this->assertLessThan( 5, $empty - $now );
+
+               $null = wfTimestamp( TS_UNIX, null );
+               $this->assertNotEquals( false, $null );
+               $this->assertLessThan( 5, $null - $now );
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfUrlencodeTest.php
new file mode 100644 (file)
index 0000000..2fc038c
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * The function only need a string parameter and might react to IIS7.0
+ *
+ * @group GlobalFunctions
+ * @covers ::wfUrlencode
+ */
+class WfUrlencodeTest extends MediaWikiUnitTestCase {
+       # ### TESTS ##############################################################
+
+       /**
+        * @dataProvider provideURLS
+        */
+       public function testEncodingUrlWith( $input, $expected ) {
+               $this->verifyEncodingFor( 'Apache', $input, $expected );
+       }
+
+       /**
+        * @dataProvider provideURLS
+        */
+       public function testEncodingUrlWithMicrosoftIis7( $input, $expected ) {
+               $this->verifyEncodingFor( 'Microsoft-IIS/7', $input, $expected );
+       }
+
+       # ### HELPERS #############################################################
+
+       /**
+        * Internal helper that actually run the test.
+        * Called by the public methods testEncodingUrlWith...()
+        */
+       private function verifyEncodingFor( $server, $input, $expectations ) {
+               $expected = $this->extractExpect( $server, $expectations );
+
+               // save up global
+               $old = $_SERVER['SERVER_SOFTWARE'] ?? null;
+               $_SERVER['SERVER_SOFTWARE'] = $server;
+               wfUrlencode( null );
+
+               // do the requested test
+               $this->assertEquals(
+                       $expected,
+                       wfUrlencode( $input ),
+                       "Encoding '$input' for server '$server' should be '$expected'"
+               );
+
+               // restore global
+               if ( $old === null ) {
+                       unset( $_SERVER['SERVER_SOFTWARE'] );
+               } else {
+                       $_SERVER['SERVER_SOFTWARE'] = $old;
+               }
+               wfUrlencode( null );
+       }
+
+       /**
+        * Interprets the provider array. Return expected value depending
+        * the HTTP server name.
+        */
+       private function extractExpect( $server, $expectations ) {
+               if ( is_string( $expectations ) ) {
+                       return $expectations;
+               } elseif ( is_array( $expectations ) ) {
+                       if ( !array_key_exists( $server, $expectations ) ) {
+                               throw new MWException( __METHOD__ . " expectation does not have any "
+                                       . "value for server name $server. Check the provider array.\n" );
+                       } else {
+                               return $expectations[$server];
+                       }
+               } else {
+                       throw new MWException( __METHOD__ . " given invalid expectation for "
+                               . "'$server'. Should be a string or an array [ <http server name> => <string> ].\n" );
+               }
+       }
+
+       # ### PROVIDERS ###########################################################
+
+       /**
+        * Format is either:
+        *   [ 'input', 'expected' ];
+        * Or:
+        *   [ 'input',
+        *       [ 'Apache', 'expected' ],
+        *       [ 'Microsoft-IIS/7', 'expected' ],
+        *   ],
+        * If you want to add other HTTP server name, you will have to add a new
+        * testing method much like the testEncodingUrlWith() method above.
+        */
+       public static function provideURLS() {
+               return [
+                       # ## RFC 1738 chars
+                       // + is not safe
+                       [ '+', '%2B' ],
+                       // & and = not safe in queries
+                       [ '&', '%26' ],
+                       [ '=', '%3D' ],
+
+                       [ ':', [
+                               'Apache' => ':',
+                               'Microsoft-IIS/7' => '%3A',
+                       ] ],
+
+                       // remaining chars do not need encoding
+                       [
+                               ';@$-_.!*',
+                               ';@$-_.!*',
+                       ],
+
+                       # ## Other tests
+                       // slash remain unchanged. %2F seems to break things
+                       [ '/', '/' ],
+                       // T105265
+                       [ '~', '~' ],
+
+                       // Other 'funnies' chars
+                       [ '[]', '%5B%5D' ],
+                       [ '<>', '%3C%3E' ],
+
+                       // Apostrophe is encoded
+                       [ '\'', '%27' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/PathRouterTest.php b/tests/phpunit/unit/includes/PathRouterTest.php
new file mode 100644 (file)
index 0000000..fb23706
--- /dev/null
@@ -0,0 +1,325 @@
+<?php
+
+/**
+ * Tests for the PathRouter parsing.
+ *
+ * @covers PathRouter
+ */
+class PathRouterTest extends MediaWikiUnitTestCase {
+
+       /**
+        * @var PathRouter
+        */
+       protected $basicRouter;
+
+       protected function setUp() {
+               parent::setUp();
+               $router = new PathRouter;
+               $router->add( "/wiki/$1" );
+               $this->basicRouter = $router;
+       }
+
+       public static function provideParse() {
+               $tests = [
+                       // Basic path parsing
+                       'Basic path parsing' => [
+                               "/wiki/$1",
+                               "/wiki/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       //
+                       'Loose path auto-$1: /$1' => [
+                               "/",
+                               "/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       'Loose path auto-$1: /wiki' => [
+                               "/wiki",
+                               "/wiki/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       'Loose path auto-$1: /wiki/' => [
+                               "/wiki/",
+                               "/wiki/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       // Ensure that path is based on specificity, not order
+                       'Order, /$1 added first' => [
+                               [ "/$1", "/a/$1", "/b/$1" ],
+                               "/a/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       'Order, /$1 added last' => [
+                               [ "/b/$1", "/a/$1", "/$1" ],
+                               "/a/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       // Handling of key based arrays with a url parameter
+                       'Key based array' => [
+                               [ [
+                                       'path' => [ 'edit' => "/edit/$1" ],
+                                       'params' => [ 'action' => '$key' ],
+                               ] ],
+                               "/edit/Foo",
+                               [ 'title' => "Foo", 'action' => 'edit' ]
+                       ],
+                       // Additional parameter
+                       'Basic $2' => [
+                               [ [
+                                       'path' => '/$2/$1',
+                                       'params' => [ 'test' => '$2' ]
+                               ] ],
+                               "/asdf/Foo",
+                               [ 'title' => "Foo", 'test' => 'asdf' ]
+                       ],
+               ];
+               // Shared patterns for restricted value parameter tests
+               $restrictedPatterns = [
+                       [
+                               'path' => '/$2/$1',
+                               'params' => [ 'test' => '$2' ],
+                               'options' => [ '$2' => [ 'a', 'b' ] ]
+                       ],
+                       [
+                               'path' => '/$2/$1',
+                               'params' => [ 'test2' => '$2' ],
+                               'options' => [ '$2' => 'c' ]
+                       ],
+                       '/$1'
+               ];
+               $tests += [
+                       // Restricted value parameter tests
+                       'Restricted 1' => [
+                               $restrictedPatterns,
+                               "/asdf/Foo",
+                               [ 'title' => "asdf/Foo" ]
+                       ],
+                       'Restricted 2' => [
+                               $restrictedPatterns,
+                               "/a/Foo",
+                               [ 'title' => "Foo", 'test' => 'a' ]
+                       ],
+                       'Restricted 3' => [
+                               $restrictedPatterns,
+                               "/c/Foo",
+                               [ 'title' => "Foo", 'test2' => 'c' ]
+                       ],
+
+                       // Callback test
+                       'Callback' => [
+                               [ [
+                                       'path' => "/$1",
+                                       'params' => [ 'a' => 'b', 'data:foo' => 'bar' ],
+                                       'options' => [ 'callback' => [ __CLASS__, 'callbackForTest' ] ]
+                               ] ],
+                               '/Foo',
+                               [
+                                       'title' => "Foo",
+                                       'x' => 'Foo',
+                                       'a' => 'b',
+                                       'foo' => 'bar'
+                               ]
+                       ],
+
+                       // Test to ensure that matches are not made if a parameter expects nonexistent input
+                       'Fail' => [
+                               [ [
+                                       'path' => "/wiki/$1",
+                                       'params' => [ 'title' => "$1$2" ],
+                               ] ],
+                               "/wiki/A",
+                               []
+                       ],
+
+                       // Make sure the router handles titles like Special:Recentchanges correctly
+                       'Special title' => [
+                               "/wiki/$1",
+                               "/wiki/Special:Recentchanges",
+                               [ 'title' => "Special:Recentchanges" ]
+                       ],
+
+                       // Make sure the router decodes urlencoding properly
+                       'URL encoding' => [
+                               "/wiki/$1",
+                               "/wiki/Title_With%20Space",
+                               [ 'title' => "Title_With Space" ]
+                       ],
+
+                       // Double slash and dot expansion
+                       'Double slash in prefix' => [
+                               '/wiki/$1',
+                               '//wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+                       'Double slash at start of $1' => [
+                               '/wiki/$1',
+                               '/wiki//Foo',
+                               [ 'title' => '/Foo' ]
+                       ],
+                       'Double slash in middle of $1' => [
+                               '/wiki/$1',
+                               '/wiki/.hack//SIGN',
+                               [ 'title' => '.hack//SIGN' ]
+                       ],
+                       'Dots removed 1' => [
+                               '/wiki/$1',
+                               '/x/../wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+                       'Dots removed 2' => [
+                               '/wiki/$1',
+                               '/./wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+                       'Dots retained 1' => [
+                               '/wiki/$1',
+                               '/wiki/../wiki/Foo',
+                               [ 'title' => '../wiki/Foo' ]
+                       ],
+                       'Dots retained 2' => [
+                               '/wiki/$1',
+                               '/wiki/./Foo',
+                               [ 'title' => './Foo' ]
+                       ],
+                       'Triple slash' => [
+                               '/wiki/$1',
+                               '///wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+                       // '..' only traverses one slash, see e.g. RFC 3986
+                       'Dots traversing double slash 1' => [
+                               '/wiki/$1',
+                               '/a//b/../../wiki/Foo',
+                               []
+                       ],
+                       'Dots traversing double slash 2' => [
+                               '/wiki/$1',
+                               '/a//b/../../../wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+               ];
+
+               // Make sure the router doesn't break on special characters like $ used in regexp replacements
+               foreach ( [ "$", "$1", "\\", "\\$1" ] as $char ) {
+                       $tests["Regexp character $char"] = [
+                               "/wiki/$1",
+                               "/wiki/$char",
+                               [ 'title' => "$char" ]
+                       ];
+               }
+
+               $tests += [
+                       // Make sure the router handles characters like +&() properly
+                       "Special characters" => [
+                               "/wiki/$1",
+                               "/wiki/Plus+And&Dollar\\Stuff();[]{}*",
+                               [ 'title' => "Plus+And&Dollar\\Stuff();[]{}*" ],
+                       ],
+
+                       // Make sure the router handles unicode characters correctly
+                       "Unicode 1" => [
+                               "/wiki/$1",
+                               "/wiki/Spécial:Modifications_récentes" ,
+                               [ 'title' => "Spécial:Modifications_récentes" ],
+                       ],
+
+                       "Unicode 2" => [
+                               "/wiki/$1",
+                               "/wiki/Sp%C3%A9cial:Modifications_r%C3%A9centes",
+                               [ 'title' => "Spécial:Modifications_récentes" ],
+                       ]
+               ];
+
+               // Ensure the router doesn't choke on long paths.
+               $lorem = "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_" .
+                       "tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_" .
+                        "nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._" .
+                        "Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_" .
+                        "eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_" .
+                        "in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum.";
+
+               $tests += [
+                       "Long path" => [
+                               "/wiki/$1",
+                               "/wiki/$lorem",
+                               [ 'title' => $lorem ]
+                       ],
+
+                       // Ensure that the php passed site of parameter values are not urldecoded
+                       "Pattern urlencoding" => [
+                               [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => '%20:$1' ] ] ],
+                               "/wiki/Foo",
+                               [ 'title' => '%20:Foo' ]
+                       ],
+
+                       // Ensure that raw parameter values do not have any variable replacements or urldecoding
+                       "Raw param value" => [
+                               [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => [ 'value' => 'bar%20$1' ] ] ] ],
+                               "/wiki/Foo",
+                               [ 'title' => 'bar%20$1' ]
+                       ]
+               ];
+
+               return $tests;
+       }
+
+       /**
+        * Test path parsing
+        * @dataProvider provideParse
+        */
+       public function testParse( $patterns, $path, $expected ) {
+               $patterns = (array)$patterns;
+
+               $router = new PathRouter;
+               foreach ( $patterns as $pattern ) {
+                       if ( is_array( $pattern ) ) {
+                               $router->add( $pattern['path'], $pattern['params'] ?? [],
+                                       $pattern['options'] ?? [] );
+                       } else {
+                               $router->add( $pattern );
+                       }
+               }
+               $matches = $router->parse( $path );
+               $this->assertEquals( $matches, $expected );
+       }
+
+       public static function callbackForTest( &$matches, $data ) {
+               $matches['x'] = $data['$1'];
+               $matches['foo'] = $data['foo'];
+       }
+
+       public static function provideWeight() {
+               return [
+                       [ '/Foo', [ 'title' => 'Foo' ] ],
+                       [ '/Bar', [ 'ping' => 'pong' ] ],
+                       [ '/Baz', [ 'marco' => 'polo' ] ],
+                       [ '/asdf-foo', [ 'title' => 'qwerty-foo' ] ],
+                       [ '/qwerty-bar', [ 'title' => 'asdf-bar' ] ],
+                       [ '/a/Foo', [ 'title' => 'Foo' ] ],
+                       [ '/asdf/Foo', [ 'title' => 'Foo' ] ],
+                       [ '/qwerty/Foo', [ 'title' => 'Foo', 'qwerty' => 'qwerty' ] ],
+                       [ '/baz/Foo', [ 'title' => 'Foo', 'unrestricted' => 'baz' ] ],
+                       [ '/y/Foo', [ 'title' => 'Foo', 'restricted-to-y' => 'y' ] ],
+               ];
+       }
+
+       /**
+        * Test to ensure weight of paths is handled correctly
+        * @dataProvider provideWeight
+        */
+       public function testWeight( $path, $expected ) {
+               $router = new PathRouter;
+               $router->addStrict( "/Bar", [ 'ping' => 'pong' ] );
+               $router->add( "/asdf-$1", [ 'title' => 'qwerty-$1' ] );
+               $router->add( "/$1" );
+               $router->add( "/qwerty-$1", [ 'title' => 'asdf-$1' ] );
+               $router->addStrict( "/Baz", [ 'marco' => 'polo' ] );
+               $router->add( "/a/$1" );
+               $router->add( "/asdf/$1" );
+               $router->add( "/$2/$1", [ 'unrestricted' => '$2' ] );
+               $router->add( [ 'qwerty' => "/qwerty/$1" ], [ 'qwerty' => '$key' ] );
+               $router->add( "/$2/$1", [ 'restricted-to-y' => '$2' ], [ '$2' => 'y' ] );
+
+               $this->assertEquals( $router->parse( $path ), $expected );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Rest/ResponseFactoryTest.php b/tests/phpunit/unit/includes/Rest/ResponseFactoryTest.php
new file mode 100644 (file)
index 0000000..04d54de
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use ArrayIterator;
+use MediaWiki\Rest\HttpException;
+use MediaWiki\Rest\ResponseFactory;
+use MediaWikiUnitTestCase;
+
+/** @covers \MediaWiki\Rest\ResponseFactory */
+class ResponseFactoryTest extends MediaWikiUnitTestCase {
+       public static function provideEncodeJson() {
+               return [
+                       [ (object)[], '{}' ],
+                       [ '/', '"/"' ],
+                       [ '£', '"£"' ],
+                       [ [], '[]' ],
+               ];
+       }
+
+       /** @dataProvider provideEncodeJson */
+       public function testEncodeJson( $input, $expected ) {
+               $rf = new ResponseFactory;
+               $this->assertSame( $expected, $rf->encodeJson( $input ) );
+       }
+
+       public function testCreateJson() {
+               $rf = new ResponseFactory;
+               $response = $rf->createJson( [] );
+               $response->getBody()->rewind();
+               $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) );
+               $this->assertSame( '[]', $response->getBody()->getContents() );
+               // Make sure getSize() is functional, since testCreateNoContent() depends on it
+               $this->assertSame( 2, $response->getBody()->getSize() );
+       }
+
+       public function testCreateNoContent() {
+               $rf = new ResponseFactory;
+               $response = $rf->createNoContent();
+               $this->assertSame( [], $response->getHeader( 'Content-Type' ) );
+               $this->assertSame( 0, $response->getBody()->getSize() );
+               $this->assertSame( 204, $response->getStatusCode() );
+       }
+
+       public function testCreatePermanentRedirect() {
+               $rf = new ResponseFactory;
+               $response = $rf->createPermanentRedirect( 'http://www.example.com/' );
+               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
+               $this->assertSame( 301, $response->getStatusCode() );
+       }
+
+       public function testCreateLegacyTemporaryRedirect() {
+               $rf = new ResponseFactory;
+               $response = $rf->createLegacyTemporaryRedirect( 'http://www.example.com/' );
+               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
+               $this->assertSame( 302, $response->getStatusCode() );
+       }
+
+       public function testCreateTemporaryRedirect() {
+               $rf = new ResponseFactory;
+               $response = $rf->createTemporaryRedirect( 'http://www.example.com/' );
+               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
+               $this->assertSame( 307, $response->getStatusCode() );
+       }
+
+       public function testCreateSeeOther() {
+               $rf = new ResponseFactory;
+               $response = $rf->createSeeOther( 'http://www.example.com/' );
+               $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
+               $this->assertSame( 303, $response->getStatusCode() );
+       }
+
+       public function testCreateNotModified() {
+               $rf = new ResponseFactory;
+               $response = $rf->createNotModified();
+               $this->assertSame( 0, $response->getBody()->getSize() );
+               $this->assertSame( 304, $response->getStatusCode() );
+       }
+
+       /** @expectedException \InvalidArgumentException */
+       public function testCreateHttpErrorInvalid() {
+               $rf = new ResponseFactory;
+               $rf->createHttpError( 200 );
+       }
+
+       public function testCreateHttpError() {
+               $rf = new ResponseFactory;
+               $response = $rf->createHttpError( 415, [ 'message' => '...' ] );
+               $this->assertSame( 415, $response->getStatusCode() );
+               $body = $response->getBody();
+               $body->rewind();
+               $data = json_decode( $body->getContents(), true );
+               $this->assertSame( 415, $data['httpCode'] );
+               $this->assertSame( '...', $data['message'] );
+       }
+
+       public function testCreateFromExceptionUnlogged() {
+               $rf = new ResponseFactory;
+               $response = $rf->createFromException( new HttpException( 'hello', 415 ) );
+               $this->assertSame( 415, $response->getStatusCode() );
+               $body = $response->getBody();
+               $body->rewind();
+               $data = json_decode( $body->getContents(), true );
+               $this->assertSame( 415, $data['httpCode'] );
+               $this->assertSame( 'hello', $data['message'] );
+       }
+
+       public function testCreateFromExceptionLogged() {
+               $rf = new ResponseFactory;
+               $response = $rf->createFromException( new \Exception( "hello", 415 ) );
+               $this->assertSame( 500, $response->getStatusCode() );
+               $body = $response->getBody();
+               $body->rewind();
+               $data = json_decode( $body->getContents(), true );
+               $this->assertSame( 500, $data['httpCode'] );
+               $this->assertSame( 'Error: exception of type Exception', $data['message'] );
+       }
+
+       public static function provideCreateFromReturnValue() {
+               return [
+                       [ 'hello', '{"value":"hello"}' ],
+                       [ true, '{"value":true}' ],
+                       [ [ 'x' => 'y' ], '{"x":"y"}' ],
+                       [ [ 'x', 'y' ], '["x","y"]' ],
+                       [ [ 'a', 'x' => 'y' ], '{"0":"a","x":"y"}' ],
+                       [ (object)[ 'a', 'x' => 'y' ], '{"0":"a","x":"y"}' ],
+                       [ [], '[]' ],
+                       [ (object)[], '{}' ],
+               ];
+       }
+
+       /** @dataProvider provideCreateFromReturnValue */
+       public function testCreateFromReturnValue( $input, $expected ) {
+               $rf = new ResponseFactory;
+               $response = $rf->createFromReturnValue( $input );
+               $body = $response->getBody();
+               $body->rewind();
+               $this->assertSame( $expected, $body->getContents() );
+       }
+
+       /** @expectedException \InvalidArgumentException */
+       public function testCreateFromReturnValueInvalid() {
+               $rf = new ResponseFactory;
+               $rf->createFromReturnValue( new ArrayIterator );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..9dff2cc
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\MainSlotRoleHandler;
+use MediaWikiUnitTestCase;
+use PHPUnit\Framework\MockObject\MockObject;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\MainSlotRoleHandler
+ */
+class MainSlotRoleHandlerTest extends MediaWikiUnitTestCase {
+
+       private function makeTitleObject( $ns ) {
+               /** @var Title|MockObject $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $title->method( 'getNamespace' )
+                       ->willReturn( $ns );
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new MainSlotRoleHandler( [] );
+               $this->assertSame( 'main', $handler->getRole() );
+               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
+        */
+       public function testFetDefaultModel() {
+               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
+
+               // For the main handler, the namespace determins the default model
+               $titleMain = $this->makeTitleObject( NS_MAIN );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
+
+               $title100 = $this->makeTitleObject( 100 );
+               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               // For the main handler, (nearly) all models are allowed
+               $title = $this->makeTitleObject( NS_MAIN );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               $this->assertTrue( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/Revision/SlotRecordTest.php b/tests/phpunit/unit/includes/Revision/SlotRecordTest.php
new file mode 100644 (file)
index 0000000..aab430a
--- /dev/null
@@ -0,0 +1,416 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
+use MediaWikiUnitTestCase;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRecord
+ */
+class SlotRecordTest extends MediaWikiUnitTestCase {
+
+       private function makeRow( $data = [] ) {
+               $data = $data + [
+                       'slot_id' => 1234,
+                       'slot_content_id' => 33,
+                       'content_size' => '5',
+                       'content_sha1' => 'someHash',
+                       'content_address' => 'tt:456',
+                       'model_name' => CONTENT_MODEL_WIKITEXT,
+                       'format_name' => CONTENT_FORMAT_WIKITEXT,
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '1',
+                       'role_name' => 'myRole',
+               ];
+               return (object)$data;
+       }
+
+       public function testCompleteConstruction() {
+               $row = $this->makeRow();
+               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasContentId() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertTrue( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 5, $record->getSize() );
+               $this->assertSame( 'someHash', $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 1, $record->getOrigin() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( 33, $record->getContentId() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testConstructionDeferred() {
+               $row = $this->makeRow( [
+                       'content_size' => null, // to be computed
+                       'content_sha1' => null, // to be computed
+                       'format_name' => function () {
+                               return CONTENT_FORMAT_WIKITEXT;
+                       },
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '2',
+                       'slot_content_id' => function () {
+                               return null;
+                       },
+               ] );
+
+               $content = function () {
+                       return new WikitextContent( 'A' );
+               };
+
+               $record = new SlotRecord( $row, $content );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotEmpty( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testNewUnsaved() {
+               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
+
+               $this->assertFalse( $record->hasAddress() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->hasRevision() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertFalse( $record->hasOrigin() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotEmpty( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function provideInvalidConstruction() {
+               yield 'both null' => [ null, null ];
+               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
+               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
+               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
+               yield 'null content' => [ (object)[], null ];
+       }
+
+       /**
+        * @dataProvider provideInvalidConstruction
+        */
+       public function testInvalidConstruction( $row, $content ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new SlotRecord( $row, $content );
+       }
+
+       public function testGetContentId_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getContentId();
+       }
+
+       public function testGetAddress_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getAddress();
+       }
+
+       public function provideIncomplete() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               yield 'unsaved' => [ $unsaved ];
+
+               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $inherited = SlotRecord::newInherited( $parent );
+               yield 'inherited' => [ $inherited ];
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetRevision_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getRevision();
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetOrigin_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getOrigin();
+       }
+
+       public function provideHashStability() {
+               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
+               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
+       }
+
+       /**
+        * @dataProvider provideHashStability
+        */
+       public function testHashStability( $text, $hash ) {
+               // Changing the output of the hash function will break things horribly!
+
+               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
+
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
+               $this->assertSame( $hash, $record->getSha1() );
+       }
+
+       public function testHashComputed() {
+               $row = $this->makeRow();
+               $row->content_sha1 = '';
+
+               $rec = new SlotRecord( $row, new WikitextContent( 'A' ) );
+               $this->assertNotEmpty( $rec->getSha1() );
+       }
+
+       public function testNewWithSuppressedContent() {
+               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $output = SlotRecord::newWithSuppressedContent( $input );
+
+               $this->setExpectedException( SuppressedDataException::class );
+               $output->getContent();
+       }
+
+       public function testNewInherited() {
+               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
+               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, before saving revision meta-data.
+               $inherited = SlotRecord::newInherited( $parent );
+
+               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
+               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
+               $this->assertSame( $parent->getContent(), $inherited->getContent() );
+               $this->assertTrue( $inherited->isInherited() );
+               $this->assertTrue( $inherited->hasOrigin() );
+               $this->assertFalse( $inherited->hasRevision() );
+
+               // make sure we didn't mess with the internal state of $parent
+               $this->assertFalse( $parent->isInherited() );
+               $this->assertSame( 7, $parent->getRevision() );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved(
+                       10,
+                       $inherited->getContentId(),
+                       $inherited->getAddress(),
+                       $inherited
+               );
+               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
+               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
+               $this->assertSame( $parent->getContent(), $saved->getContent() );
+               $this->assertTrue( $saved->isInherited() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertSame( 10, $saved->getRevision() );
+
+               // make sure we didn't mess with the internal state of $parent or $inherited
+               $this->assertSame( 7, $parent->getRevision() );
+               $this->assertFalse( $inherited->hasRevision() );
+       }
+
+       public function testNewSaved() {
+               // This would happen while doing an edit, before saving revision meta-data.
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
+               $this->assertFalse( $saved->isInherited() );
+               $this->assertTrue( $saved->hasOrigin() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertTrue( $saved->hasAddress() );
+               $this->assertTrue( $saved->hasContentId() );
+               $this->assertSame( 'theNewAddress', $saved->getAddress() );
+               $this->assertSame( 20, $saved->getContentId() );
+               $this->assertSame( 'A', $saved->getContent()->getText() );
+               $this->assertSame( 10, $saved->getRevision() );
+               $this->assertSame( 10, $saved->getOrigin() );
+
+               // make sure we didn't mess with the internal state of $unsaved
+               $this->assertFalse( $unsaved->hasAddress() );
+               $this->assertFalse( $unsaved->hasContentId() );
+               $this->assertFalse( $unsaved->hasRevision() );
+       }
+
+       public function provideNewSaved_LogicException() {
+               $freshRow = $this->makeRow( [
+                       'content_id' => 10,
+                       'content_address' => 'address:1',
+                       'slot_origin' => 1,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
+               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
+               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
+               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
+
+               $inheritedRow = $this->makeRow( [
+                       'content_id' => null,
+                       'content_address' => null,
+                       'slot_origin' => 0,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
+               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_LogicException
+        */
+       public function testNewSaved_LogicException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( LogicException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideNewSaved_InvalidArgumentException() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
+               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
+               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_InvalidArgumentException
+        */
+       public function testNewSaved_InvalidArgumentException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideHasSameContent() {
+               $fail = function () {
+                       self::fail( 'There should be no need to actually load the content.' );
+               };
+
+               $a100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a1b = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100null = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => null,
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a2 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $b100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'B',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a200a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 200,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100x1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-x',
+                                       'content_address' => 'xxx:x1',
+                               ]
+                       ),
+                       $fail
+               );
+
+               yield 'same instance' => [ $a100a1, $a100a1, true ];
+               yield 'no address' => [ $a100a1, $a100null, true ];
+               yield 'same address' => [ $a100a1, $a100a1b, true ];
+               yield 'different address' => [ $a100a1, $a100a2, true ];
+               yield 'different model' => [ $a100a1, $b100a1, false ];
+               yield 'different size' => [ $a100a1, $a200a1, false ];
+               yield 'different hash' => [ $a100a1, $a100x1, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        */
+       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
+               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
+               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/TitleArrayFromResultTest.php b/tests/phpunit/unit/includes/TitleArrayFromResultTest.php
new file mode 100644 (file)
index 0000000..10b7d36
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @author Addshore
+ * @covers TitleArrayFromResult
+ */
+class TitleArrayFromResultTest extends MediaWikiUnitTestCase {
+
+       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
+               $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class )
+                       ->disableOriginalConstructor();
+
+               $resultWrapper = $resultWrapper->getMock();
+               $resultWrapper->expects( $this->atLeastOnce() )
+                       ->method( 'current' )
+                       ->will( $this->returnValue( $row ) );
+               $resultWrapper->expects( $this->any() )
+                       ->method( 'numRows' )
+                       ->will( $this->returnValue( $numRows ) );
+
+               return $resultWrapper;
+       }
+
+       private function getRowWithTitle( $namespace = 3, $title = 'foo' ) {
+               $row = new stdClass();
+               $row->page_namespace = $namespace;
+               $row->page_title = $title;
+               return $row;
+       }
+
+       /**
+        * @covers TitleArrayFromResult::__construct
+        */
+       public function testConstructionWithFalseRow() {
+               $row = false;
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = new TitleArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertEquals( $row, $object->current );
+       }
+
+       /**
+        * @covers TitleArrayFromResult::__construct
+        */
+       public function testConstructionWithRow() {
+               $namespace = 0;
+               $title = 'foo';
+               $row = $this->getRowWithTitle( $namespace, $title );
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = new TitleArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertInstanceOf( Title::class, $object->current );
+               $this->assertEquals( $namespace, $object->current->mNamespace );
+               $this->assertEquals( $title, $object->current->mTextform );
+       }
+
+       public static function provideNumberOfRows() {
+               return [
+                       [ 0 ],
+                       [ 1 ],
+                       [ 122 ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNumberOfRows
+        * @covers TitleArrayFromResult::count
+        */
+       public function testCountWithVaryingValues( $numRows ) {
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper(
+                       $this->getRowWithTitle(),
+                       $numRows
+               ) );
+               $this->assertEquals( $numRows, $object->count() );
+       }
+
+       /**
+        * @covers TitleArrayFromResult::current
+        */
+       public function testCurrentAfterConstruction() {
+               $namespace = 0;
+               $title = 'foo';
+               $row = $this->getRowWithTitle( $namespace, $title );
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $row ) );
+               $this->assertInstanceOf( Title::class, $object->current() );
+               $this->assertEquals( $namespace, $object->current->mNamespace );
+               $this->assertEquals( $title, $object->current->mTextform );
+       }
+
+       public function provideTestValid() {
+               return [
+                       [ $this->getRowWithTitle(), true ],
+                       [ false, false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestValid
+        * @covers TitleArrayFromResult::valid
+        */
+       public function testValid( $input, $expected ) {
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $input ) );
+               $this->assertEquals( $expected, $object->valid() );
+       }
+
+       // @todo unit test for key()
+       // @todo unit test for next()
+       // @todo unit test for rewind()
+}
diff --git a/tests/phpunit/unit/includes/WikiReferenceTest.php b/tests/phpunit/unit/includes/WikiReferenceTest.php
new file mode 100644 (file)
index 0000000..a4aae86
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * @covers WikiReference
+ */
+class WikiReferenceTest extends MediaWikiUnitTestCase {
+
+       public function provideGetDisplayName() {
+               return [
+                       'http' => [ 'foo.bar', 'http://foo.bar' ],
+                       'https' => [ 'foo.bar', 'http://foo.bar' ],
+
+                       // apparently, this is the expected behavior
+                       'invalid' => [ 'purple kittens', 'purple kittens' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetDisplayName
+        */
+       public function testGetDisplayName( $expected, $canonicalServer ) {
+               $reference = new WikiReference( $canonicalServer, '/wiki/$1' );
+               $this->assertEquals( $expected, $reference->getDisplayName() );
+       }
+
+       public function testGetCanonicalServer() {
+               $reference = new WikiReference( 'https://acme.com', '/wiki/$1', '//acme.com' );
+               $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() );
+       }
+
+       public function provideGetCanonicalUrl() {
+               return [
+                       'no fragment' => [
+                               'https://acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               null
+                       ],
+                       'empty fragment' => [
+                               'https://acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               ''
+                       ],
+                       'fragment' => [
+                               'https://acme.com/wiki/Foo#Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar'
+                       ],
+                       'double fragment' => [
+                               'https://acme.com/wiki/Foo#Bar%23Xus',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar#Xus'
+                       ],
+                       'escaped fragment' => [
+                               'https://acme.com/wiki/Foo%23Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo#Bar',
+                               null
+                       ],
+                       'empty path' => [
+                               'https://acme.com/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/$1',
+                               'Foo',
+                               null
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetCanonicalUrl
+        */
+       public function testGetCanonicalUrl(
+               $expected, $canonicalServer, $server, $path, $page, $fragmentId
+       ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) );
+       }
+
+       /**
+        * @dataProvider provideGetCanonicalUrl
+        * @note getUrl is an alias for getCanonicalUrl
+        */
+       public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) );
+       }
+
+       public function provideGetFullUrl() {
+               return [
+                       'no fragment' => [
+                               '//acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               null
+                       ],
+                       'empty fragment' => [
+                               '//acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               ''
+                       ],
+                       'fragment' => [
+                               '//acme.com/wiki/Foo#Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar'
+                       ],
+                       'double fragment' => [
+                               '//acme.com/wiki/Foo#Bar%23Xus',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar#Xus'
+                       ],
+                       'escaped fragment' => [
+                               '//acme.com/wiki/Foo%23Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo#Bar',
+                               null
+                       ],
+                       'empty path' => [
+                               '//acme.com/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/$1',
+                               'Foo',
+                               null
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetFullUrl
+        */
+       public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/CeeFormatterTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/CeeFormatterTest.php
new file mode 100644 (file)
index 0000000..946a1a2
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+namespace MediaWiki\Logger\Monolog;
+
+/**
+ * Flay per https://phabricator.wikimedia.org/T218688.
+ *
+ * @group Broken
+ * @covers \MediaWiki\Logger\Monolog\CeeFormatter
+ */
+class CeeFormatterTest extends \MediaWikiUnitTestCase {
+       public function testV1() {
+               $ls_formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
+               $cee_formatter = new CeeFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
+               $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ];
+               $this->assertSame(
+                       $cee_formatter->format( $record ),
+                       "@cee: " . $ls_formatter->format( $record ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php b/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php
new file mode 100644 (file)
index 0000000..1a8b585
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @covers DifferenceEngineSlotDiffRenderer
+ */
+class DifferenceEngineSlotDiffRendererTest extends \MediaWikiUnitTestCase {
+
+       public function testGetDiff() {
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $oldContent = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
+               $newContent = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
+
+               $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
+               $this->assertEquals( 'xxx|yyy', $diff );
+
+               $diff = $slotDiffRenderer->getDiff( null, $newContent );
+               $this->assertEquals( '|yyy', $diff );
+
+               $diff = $slotDiffRenderer->getDiff( $oldContent, null );
+               $this->assertEquals( 'xxx|', $diff );
+       }
+
+       public function testAddModules() {
+               $output = $this->getMockBuilder( OutputPage::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'addModules' ] )
+                       ->getMock();
+               $output->expects( $this->once() )
+                       ->method( 'addModules' )
+                       ->with( 'foo' );
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $slotDiffRenderer->addModules( $output );
+       }
+
+       public function testGetExtraCacheKeys() {
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $extraCacheKeys = $slotDiffRenderer->getExtraCacheKeys();
+               $this->assertSame( [ 'foo' ], $extraCacheKeys );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php b/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php
new file mode 100644 (file)
index 0000000..f778115
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+use Wikimedia\Assert\ParameterTypeException;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers SlotDiffRenderer
+ */
+class SlotDiffRendererTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @dataProvider provideNormalizeContents
+        */
+       public function testNormalizeContents(
+               $oldContent, $newContent, $allowedClasses,
+               $expectedOldContent, $expectedNewContent, $expectedExceptionClass
+       ) {
+               $slotDiffRenderer = $this->getMockBuilder( SlotDiffRenderer::class )
+                       ->getMock();
+               try {
+                       // __call needs help deciding which parameter to take by reference
+                       call_user_func_array( [ TestingAccessWrapper::newFromObject( $slotDiffRenderer ),
+                               'normalizeContents' ], [ &$oldContent, &$newContent, $allowedClasses ] );
+                       $this->assertEquals( $expectedOldContent, $oldContent );
+                       $this->assertEquals( $expectedNewContent, $newContent );
+               } catch ( Exception $e ) {
+                       if ( !$expectedExceptionClass ) {
+                               throw $e;
+                       }
+                       $this->assertInstanceOf( $expectedExceptionClass, $e );
+               }
+       }
+
+       public function provideNormalizeContents() {
+               return [
+                       'both null' => [ null, null, null, null, null, InvalidArgumentException::class ],
+                       'left null' => [
+                               null, new WikitextContent( 'abc' ), null,
+                               new WikitextContent( '' ), new WikitextContent( 'abc' ), null,
+                       ],
+                       'right null' => [
+                               new WikitextContent( 'def' ), null, null,
+                               new WikitextContent( 'def' ), new WikitextContent( '' ), null,
+                       ],
+                       'type filter' => [
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
+                       ],
+                       'type filter (subclass)' => [
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), TextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
+                       ],
+                       'type filter (null)' => [
+                               new WikitextContent( 'abc' ), null, TextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( '' ), null,
+                       ],
+                       'type filter failure (left)' => [
+                               new TextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
+                               null, null, ParameterTypeException::class,
+                       ],
+                       'type filter failure (right)' => [
+                               new WikitextContent( 'abc' ), new TextContent( 'def' ), WikitextContent::class,
+                               null, null, ParameterTypeException::class,
+                       ],
+                       'type filter (array syntax)' => [
+                               new WikitextContent( 'abc' ), new JsonContent( 'def' ),
+                               [ JsonContent::class, WikitextContent::class ],
+                               new WikitextContent( 'abc' ), new JsonContent( 'def' ), null,
+                       ],
+                       'type filter failure (array syntax)' => [
+                               new WikitextContent( 'abc' ), new CssContent( 'def' ),
+                               [ JsonContent::class, WikitextContent::class ],
+                               null, null, ParameterTypeException::class,
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php
new file mode 100644 (file)
index 0000000..6084601
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+class FileBackendDBRepoWrapperTest extends MediaWikiUnitTestCase {
+       protected $backendName = 'foo-backend';
+       protected $repoName = 'pureTestRepo';
+
+       /**
+        * @dataProvider getBackendPathsProvider
+        * @covers FileBackendDBRepoWrapper::getBackendPaths
+        */
+       public function testGetBackendPaths(
+               $mocks,
+               $latest,
+               $dbReadsExpected,
+               $dbReturnValue,
+               $originalPath,
+               $expectedBackendPath,
+               $message ) {
+               list( $dbMock, $backendMock, $wrapperMock ) = $mocks;
+
+               $dbMock->expects( $dbReadsExpected )
+                       ->method( 'selectField' )
+                       ->will( $this->returnValue( $dbReturnValue ) );
+
+               $newPaths = $wrapperMock->getBackendPaths( [ $originalPath ], $latest );
+
+               $this->assertEquals(
+                       $expectedBackendPath,
+                       $newPaths[0],
+                       $message );
+       }
+
+       public function getBackendPathsProvider() {
+               $prefix = 'mwstore://' . $this->backendName . '/' . $this->repoName;
+               $mocksForCaching = $this->getMocks();
+
+               return [
+                       [
+                               $mocksForCaching,
+                               false,
+                               $this->once(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'Public path translated correctly',
+                       ],
+                       [
+                               $mocksForCaching,
+                               false,
+                               $this->never(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'LRU cache leveraged',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->once(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'Latest obtained',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->never(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-deleted/f/o/foobar.jpg',
+                               $prefix . '-original/f/o/o/foobar',
+                               'Deleted path translated correctly',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->once(),
+                               null,
+                               $prefix . '-public/b/a/baz.jpg',
+                               $prefix . '-public/b/a/baz.jpg',
+                               'Path left untouched if no sha1 can be found',
+                       ],
+               ];
+       }
+
+       /**
+        * @covers FileBackendDBRepoWrapper::getFileContentsMulti
+        */
+       public function testGetFileContentsMulti() {
+               list( $dbMock, $backendMock, $wrapperMock ) = $this->getMocks();
+
+               $sha1Path = 'mwstore://' . $this->backendName . '/' . $this->repoName
+                       . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9';
+               $filenamePath = 'mwstore://' . $this->backendName . '/' . $this->repoName
+                       . '-public/f/o/foobar.jpg';
+
+               $dbMock->expects( $this->once() )
+                       ->method( 'selectField' )
+                       ->will( $this->returnValue( '96246614d75ba1703bdfd5d7660bb57407aaf5d9' ) );
+
+               $backendMock->expects( $this->once() )
+                       ->method( 'getFileContentsMulti' )
+                       ->will( $this->returnValue( [ $sha1Path => 'foo' ] ) );
+
+               $result = $wrapperMock->getFileContentsMulti( [ 'srcs' => [ $filenamePath ] ] );
+
+               $this->assertEquals(
+                       [ $filenamePath => 'foo' ],
+                       $result,
+                       'File contents paths translated properly'
+               );
+       }
+
+       protected function getMocks() {
+               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
+                       ->disableOriginalClone()
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $backendMock = $this->getMockBuilder( FSFileBackend::class )
+                       ->setConstructorArgs( [ [
+                                       'name' => $this->backendName,
+                                       'wikiId' => wfWikiID()
+                               ] ] )
+                       ->getMock();
+
+               $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class )
+                       ->setMethods( [ 'getDB' ] )
+                       ->setConstructorArgs( [ [
+                                       'backend' => $backendMock,
+                                       'repoName' => $this->repoName,
+                                       'dbHandleFactory' => null
+                               ] ] )
+                       ->getMock();
+
+               $wrapperMock->expects( $this->any() )->method( 'getDB' )->will( $this->returnValue( $dbMock ) );
+
+               return [ $dbMock, $backendMock, $wrapperMock ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/filerepo/file/ForeignDBFileTest.php b/tests/phpunit/unit/includes/filerepo/file/ForeignDBFileTest.php
new file mode 100644 (file)
index 0000000..32624aa
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+/** @covers ForeignDBFile */
+class ForeignDBFileTest extends \MediaWikiUnitTestCase {
+
+       public function testShouldConstructCorrectInstanceFromTitle() {
+               $title = Title::makeTitle( NS_FILE, 'Awesome_file' );
+               $repoMock = $this->createMock( LocalRepo::class );
+
+               $file = ForeignDBFile::newFromTitle( $title, $repoMock );
+
+               $this->assertInstanceOf( ForeignDBFile::class, $file );
+       }
+}
diff --git a/tests/phpunit/unit/includes/htmlform/HTMLCheckMatrixTest.php b/tests/phpunit/unit/includes/htmlform/HTMLCheckMatrixTest.php
new file mode 100644 (file)
index 0000000..659d48d
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @covers HTMLCheckMatrix
+ */
+class HTMLCheckMatrixTest extends MediaWikiUnitTestCase {
+       private static $defaultOptions = [
+               'rows' => [ 'r1', 'r2' ],
+               'columns' => [ 'c1', 'c2' ],
+               'fieldname' => 'test',
+       ];
+
+       public function testPlainInstantiation() {
+               try {
+                       new HTMLCheckMatrix( [] );
+               } catch ( MWException $e ) {
+                       $this->assertInstanceOf( HTMLFormFieldRequiredOptionsException::class, $e );
+                       return;
+               }
+
+               $this->fail( 'Expected MWException indicating missing parameters but none was thrown.' );
+       }
+
+       public function testInstantiationWithMinimumRequiredParameters() {
+               new HTMLCheckMatrix( self::$defaultOptions );
+               $this->assertTrue( true ); // form instantiation must throw exception on failure
+       }
+
+       public function testValidateCallsUserDefinedValidationCallback() {
+               $called = false;
+               $field = new HTMLCheckMatrix( self::$defaultOptions + [
+                       'validation-callback' => function () use ( &$called ) {
+                               $called = true;
+
+                               return false;
+                       },
+               ] );
+               $this->assertEquals( false, $this->validate( $field, [] ) );
+               $this->assertTrue( $called );
+       }
+
+       public function testValidateRequiresArrayInput() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions );
+               $this->assertEquals( false, $this->validate( $field, null ) );
+               $this->assertEquals( false, $this->validate( $field, true ) );
+               $this->assertEquals( false, $this->validate( $field, 'abc' ) );
+               $this->assertEquals( false, $this->validate( $field, new stdClass ) );
+               $this->assertEquals( true, $this->validate( $field, [] ) );
+       }
+
+       public function testValidateAllowsOnlyKnownTags() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions );
+               $this->assertInstanceOf( Message::class, $this->validate( $field, [ 'foo' ] ) );
+       }
+
+       public function testValidateAcceptsPartialTagList() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions );
+               $this->assertTrue( $this->validate( $field, [] ) );
+               $this->assertTrue( $this->validate( $field, [ 'c1-r1' ] ) );
+               $this->assertTrue( $this->validate( $field, [ 'c1-r1', 'c1-r2', 'c2-r1', 'c2-r2' ] ) );
+       }
+
+       /**
+        * This form object actually has no visibility into what happens later on, but essentially
+        * if the data submitted by the user passes validate the following is run:
+        * foreach ( $field->filterDataForSubmit( $data ) as $k => $v ) {
+        *     $user->setOption( $k, $v );
+        * }
+        */
+       public function testValuesForcedOnRemainOn() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions + [
+                               'force-options-on' => [ 'c2-r1' ],
+                       ] );
+               $expected = [
+                       'c1-r1' => false,
+                       'c1-r2' => false,
+                       'c2-r1' => true,
+                       'c2-r2' => false,
+               ];
+               $this->assertEquals( $expected, $field->filterDataForSubmit( [] ) );
+       }
+
+       public function testValuesForcedOffRemainOff() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions + [
+                               'force-options-off' => [ 'c1-r2', 'c2-r2' ],
+                       ] );
+               $expected = [
+                       'c1-r1' => true,
+                       'c1-r2' => false,
+                       'c2-r1' => true,
+                       'c2-r2' => false,
+               ];
+               // array_keys on the result simulates submitting all fields checked
+               $this->assertEquals( $expected, $field->filterDataForSubmit( array_keys( $expected ) ) );
+       }
+
+       protected function validate( HTMLFormField $field, $submitted ) {
+               return $field->validate(
+                       $submitted,
+                       [ self::$defaultOptions['fieldname'] => $submitted ]
+               );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/http/HttpUnitTest.php b/tests/phpunit/unit/includes/http/HttpUnitTest.php
new file mode 100644 (file)
index 0000000..af73f34
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @covers Http
+ * @group Http
+ * @group small
+ */
+class HttpUnitTest extends MediaWikiUnitTestCase {
+
+       /**
+        * Test Http::isValidURI()
+        * T29854 : Http::isValidURI is too lax
+        * @dataProvider provideURI
+        * @covers Http::isValidURI
+        */
+       public function testIsValidUri( $expect, $URI, $message = '' ) {
+               $this->assertEquals(
+                       $expect,
+                       (bool)Http::isValidURI( $URI ),
+                       $message
+               );
+       }
+
+       /**
+        * Feeds URI to test a long regular expression in Http::isValidURI
+        */
+       public static function provideURI() {
+               /** Format: 'boolean expectation', 'URI to test', 'Optional message' */
+               return [
+                       [ false, '¿non sens before!! http://a', 'Allow anything before URI' ],
+
+                       # (http|https) - only two schemes allowed
+                       [ true, 'http://www.example.org/' ],
+                       [ true, 'https://www.example.org/' ],
+                       [ true, 'http://www.example.org', 'URI without directory' ],
+                       [ true, 'http://a', 'Short name' ],
+                       [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star'
+                       [ false, '\\host\directory', 'CIFS share' ],
+                       [ false, 'gopher://host/dir', 'Reject gopher scheme' ],
+                       [ false, 'telnet://host', 'Reject telnet scheme' ],
+
+                       # :\/\/ - double slashes
+                       [ false, 'http//example.org', 'Reject missing colon in protocol' ],
+                       [ false, 'http:/example.org', 'Reject missing slash in protocol' ],
+                       [ false, 'http:example.org', 'Must have two slashes' ],
+                       # Following fail since hostname can be made of anything
+                       [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ],
+
+                       # (\w+:{0,1}\w*@)? - optional user:pass
+                       [ true, 'http://user@host', 'Username provided' ],
+                       [ true, 'http://user:@host', 'Username provided, no password' ],
+                       [ true, 'http://user:pass@host', 'Username and password provided' ],
+
+                       # (\S+) - host part is made of anything not whitespaces
+                       // commented these out in order to remove @group Broken
+                       // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them
+                       // [ false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ],
+                       // [ false, 'http://exam:ple.org/', 'hostname can not use colons!' ],
+
+                       # (:[0-9]+)? - port number
+                       [ true, 'http://example.org:80/' ],
+                       [ true, 'https://example.org:80/' ],
+                       [ true, 'http://example.org:443/' ],
+                       [ true, 'https://example.org:443/' ],
+
+                       # Part after the hostname is / or / with something else
+                       [ true, 'http://example/#' ],
+                       [ true, 'http://example/!' ],
+                       [ true, 'http://example/:' ],
+                       [ true, 'http://example/.' ],
+                       [ true, 'http://example/?' ],
+                       [ true, 'http://example/+' ],
+                       [ true, 'http://example/=' ],
+                       [ true, 'http://example/&' ],
+                       [ true, 'http://example/%' ],
+                       [ true, 'http://example/@' ],
+                       [ true, 'http://example/-' ],
+                       [ true, 'http://example//' ],
+                       [ true, 'http://example/&' ],
+
+                       # Fragment
+                       [ true, 'http://exam#ple.org', ], # This one is valid, really!
+                       [ true, 'http://example.org:80#anchor' ],
+                       [ true, 'http://example.org/?id#anchor' ],
+                       [ true, 'http://example.org/?#anchor' ],
+
+                       [ false, 'http://a ¿non !!sens after', 'Allow anything after URI' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/json/FormatJsonTest.php b/tests/phpunit/unit/includes/json/FormatJsonTest.php
new file mode 100644 (file)
index 0000000..94c7d91
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @covers FormatJson
+ */
+class FormatJsonTest extends MediaWikiUnitTestCase {
+
+       /**
+        * Test data for testParseTryFixing.
+        *
+        * Some PHP interpreters use json-c rather than the JSON.org canonical
+        * parser to avoid being encumbered by the "shall be used for Good, not
+        * Evil" clause of the JSON.org parser's license. By default, json-c
+        * parses in a non-strict mode which allows trailing commas for array and
+        * object delarations among other things, so our JSON_ERROR_SYNTAX rescue
+        * block is not always triggered. It however isn't lenient in exactly the
+        * same ways as our TRY_FIXING mode, so the assertions in this test are
+        * a bit more complicated than they ideally would be:
+        *
+        * Optional third argument: true if json-c parses the value without
+        * intervention, false otherwise. Defaults to true.
+        *
+        * Optional fourth argument: expected cannonical JSON serialization of
+        * json-c parsed result. Defaults to the second argument's value.
+        */
+       public static function provideParseTryFixing() {
+               return [
+                       [ "[,]", '[]', false ],
+                       [ "[ , ]", '[]', false ],
+                       [ "[ , }", false ],
+                       [ '[1],', false, true, '[1]' ],
+                       [ "[1,]", '[1]' ],
+                       [ "[1\n,]", '[1]' ],
+                       [ "[1,\n]", '[1]' ],
+                       [ "[1,]\n", '[1]' ],
+                       [ "[1\n,\n]\n", '[1]' ],
+                       [ '["a,",]', '["a,"]' ],
+                       [ "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ],
+                       // I wish we could parse this, but would need quote parsing
+                       [ '[[1,],[2,],[3,]]', false, true, '[[1],[2],[3]]' ],
+                       [ '[1,,]', false, false, '[1]' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideParseTryFixing
+        * @param string $value
+        * @param string|bool $expected Expected result with strict parser
+        * @param bool $jsoncParses Will json-c parse this value without TRY_FIXING?
+        * @param string|bool $expectedJsonc Expected result with lenient parser
+        * if different from the strict expectation
+        */
+       public function testParseTryFixing(
+               $value, $expected,
+               $jsoncParses = true, $expectedJsonc = null
+       ) {
+               // PHP5 results are always expected to have isGood() === false
+               $expectedGoodStatus = false;
+
+               // Check to see if json parser allows trailing commas
+               if ( json_decode( '[1,]' ) !== null ) {
+                       // Use json-c specific expected result if provided
+                       $expected = ( $expectedJsonc === null ) ? $expected : $expectedJsonc;
+                       // If json-c parses the value natively, expect isGood() === true
+                       $expectedGoodStatus = $jsoncParses;
+               }
+
+               $st = FormatJson::parse( $value, FormatJson::TRY_FIXING );
+               $this->assertInstanceOf( Status::class, $st );
+               if ( $expected === false ) {
+                       $this->assertFalse( $st->isOK(), 'Expected isOK() == false' );
+               } else {
+                       $this->assertSame( $expectedGoodStatus, $st->isGood(),
+                               'Expected isGood() == ' . ( $expectedGoodStatus ? 'true' : 'false' )
+                       );
+                       $this->assertTrue( $st->isOK(), 'Expected isOK == true' );
+                       $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK );
+                       $this->assertEquals( $expected, $val );
+               }
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/language/LanguageCodeTest.php b/tests/phpunit/unit/includes/language/LanguageCodeTest.php
new file mode 100644 (file)
index 0000000..f3a7ae4
--- /dev/null
@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * @covers LanguageCode
+ * @group Language
+ *
+ * @author Thiemo Kreuz
+ */
+class LanguageCodeTest extends MediaWikiUnitTestCase {
+
+       public function testConstructor() {
+               $instance = new LanguageCode();
+
+               $this->assertInstanceOf( LanguageCode::class, $instance );
+       }
+
+       public function testGetDeprecatedCodeMapping() {
+               $map = LanguageCode::getDeprecatedCodeMapping();
+
+               $this->assertInternalType( 'array', $map );
+               $this->assertContainsOnly( 'string', array_keys( $map ) );
+               $this->assertArrayNotHasKey( '', $map );
+               $this->assertContainsOnly( 'string', $map );
+               $this->assertNotContains( '', $map );
+
+               // Codes special to MediaWiki should never appear in a map of "deprecated" codes
+               $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
+               $this->assertNotContains( 'qqq', $map, 'documentation' );
+               $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
+               $this->assertNotContains( 'qqx', $map, 'debug code' );
+
+               // Valid language codes that are currently not "deprecated"
+               $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
+               $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
+               $this->assertArrayNotHasKey( 'simple', $map );
+       }
+
+       public function testReplaceDeprecatedCodes() {
+               $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'als' ) );
+               $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'gsw' ) );
+               $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) );
+       }
+
+       /**
+        * test @see LanguageCode::bcp47().
+        * Please note the BCP 47 explicitly state that language codes are case
+        * insensitive, there are some exceptions to the rule :)
+        * This test is used to verify our formatting against all lower and
+        * all upper cases language code.
+        *
+        * @see https://tools.ietf.org/html/bcp47
+        * @dataProvider provideLanguageCodes()
+        */
+       public function testBcp47( $code, $expected ) {
+               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+                       "Applying BCP 47 standard to '$code'"
+               );
+
+               $code = strtolower( $code );
+               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+                       "Applying BCP 47 standard to lower case '$code'"
+               );
+
+               $code = strtoupper( $code );
+               $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+                       "Applying BCP 47 standard to upper case '$code'"
+               );
+       }
+
+       /**
+        * Array format is ($code, $expected)
+        */
+       public static function provideLanguageCodes() {
+               return [
+                       // Extracted from BCP 47 (list not exhaustive)
+                       # 2.1.1
+                       [ 'en-ca-x-ca', 'en-CA-x-ca' ],
+                       [ 'sgn-be-fr', 'sgn-BE-FR' ],
+                       [ 'az-latn-x-latn', 'az-Latn-x-latn' ],
+                       # 2.2
+                       [ 'sr-Latn-RS', 'sr-Latn-RS' ],
+                       [ 'az-arab-ir', 'az-Arab-IR' ],
+
+                       # 2.2.5
+                       [ 'sl-nedis', 'sl-nedis' ],
+                       [ 'de-ch-1996', 'de-CH-1996' ],
+
+                       # 2.2.6
+                       [
+                               'en-latn-gb-boont-r-extended-sequence-x-private',
+                               'en-Latn-GB-boont-r-extended-sequence-x-private'
+                       ],
+
+                       // Examples from BCP 47 Appendix A
+                       # Simple language subtag:
+                       [ 'DE', 'de' ],
+                       [ 'fR', 'fr' ],
+                       [ 'ja', 'ja' ],
+
+                       # Language subtag plus script subtag:
+                       [ 'zh-hans', 'zh-Hans' ],
+                       [ 'sr-cyrl', 'sr-Cyrl' ],
+                       [ 'sr-latn', 'sr-Latn' ],
+
+                       # Extended language subtags and their primary language subtag
+                       # counterparts:
+                       [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ],
+                       [ 'cmn-hans-cn', 'cmn-Hans-CN' ],
+                       [ 'zh-yue-hk', 'zh-yue-HK' ],
+                       [ 'yue-hk', 'yue-HK' ],
+
+                       # Language-Script-Region:
+                       [ 'zh-hans-cn', 'zh-Hans-CN' ],
+                       [ 'sr-latn-RS', 'sr-Latn-RS' ],
+
+                       # Language-Variant:
+                       [ 'sl-rozaj', 'sl-rozaj' ],
+                       [ 'sl-rozaj-biske', 'sl-rozaj-biske' ],
+                       [ 'sl-nedis', 'sl-nedis' ],
+
+                       # Language-Region-Variant:
+                       [ 'de-ch-1901', 'de-CH-1901' ],
+                       [ 'sl-it-nedis', 'sl-IT-nedis' ],
+
+                       # Language-Script-Region-Variant:
+                       [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ],
+
+                       # Language-Region:
+                       [ 'de-de', 'de-DE' ],
+                       [ 'en-us', 'en-US' ],
+                       [ 'es-419', 'es-419' ],
+
+                       # Private use subtags:
+                       [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ],
+                       [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ],
+                       /**
+                        * Previous test does not reflect the BCP 47 which states:
+                        *  az-Arab-x-AZE-derbend
+                        * AZE being private, it should be lower case, hence the test above
+                        * should probably be:
+                        * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ],
+                        */
+
+                       # Private use registry values:
+                       [ 'x-whatever', 'x-whatever' ],
+                       [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ],
+                       [ 'de-qaaa', 'de-Qaaa' ],
+                       [ 'sr-latn-qm', 'sr-Latn-QM' ],
+                       [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ],
+
+                       # Tags that use extensions
+                       [ 'en-us-u-islamcal', 'en-US-u-islamcal' ],
+                       [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ],
+                       [ 'en-a-myext-b-another', 'en-a-myext-b-another' ],
+
+                       # Invalid:
+                       // de-419-DE
+                       // a-DE
+                       // ar-a-aaa-b-bbb-a-ccc
+
+                       # Non-standard and deprecated language codes used by MediaWiki
+                       [ 'als', 'gsw' ],
+                       [ 'bat-smg', 'sgs' ],
+                       [ 'be-x-old', 'be-tarask' ],
+                       [ 'fiu-vro', 'vro' ],
+                       [ 'roa-rup', 'rup' ],
+                       [ 'zh-classical', 'lzh' ],
+                       [ 'zh-min-nan', 'nan' ],
+                       [ 'zh-yue', 'yue' ],
+                       [ 'cbk-zam', 'cbk' ],
+                       [ 'de-formal', 'de-x-formal' ],
+                       [ 'eml', 'egl' ],
+                       [ 'en-rtl', 'en-x-rtl' ],
+                       [ 'es-formal', 'es-x-formal' ],
+                       [ 'hu-formal', 'hu-x-formal' ],
+                       [ 'kk-Arab', 'kk-Arab' ],
+                       [ 'kk-Cyrl', 'kk-Cyrl' ],
+                       [ 'kk-Latn', 'kk-Latn' ],
+                       [ 'map-bms', 'jv-x-bms' ],
+                       [ 'mo', 'ro-Cyrl-MD' ],
+                       [ 'nrm', 'nrf' ],
+                       [ 'nl-informal', 'nl-x-informal' ],
+                       [ 'roa-tara', 'nap-x-tara' ],
+                       [ 'simple', 'en-simple' ],
+                       [ 'sr-ec', 'sr-Cyrl' ],
+                       [ 'sr-el', 'sr-Latn' ],
+                       [ 'zh-cn', 'zh-Hans-CN' ],
+                       [ 'zh-sg', 'zh-Hans-SG' ],
+                       [ 'zh-my', 'zh-Hans-MY' ],
+                       [ 'zh-tw', 'zh-Hant-TW' ],
+                       [ 'zh-hk', 'zh-Hant-HK' ],
+                       [ 'zh-mo', 'zh-Hant-MO' ],
+                       [ 'zh-hans', 'zh-Hans' ],
+                       [ 'zh-hant', 'zh-Hant' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/language/SpecialPageAliasTest.php b/tests/phpunit/unit/includes/language/SpecialPageAliasTest.php
new file mode 100644 (file)
index 0000000..cce9d0e
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * Verifies that special page aliases are valid, with no slashes.
+ *
+ * @group Language
+ * @group SpecialPageAliases
+ * @group SystemTest
+ * @group medium
+ * @todo This should be a structure test
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class SpecialPageAliasTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @coversNothing
+        * @dataProvider validSpecialPageAliasesProvider
+        */
+       public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
+               foreach ( $specialPageAliases as $specialPage => $aliases ) {
+                       foreach ( $aliases as $alias ) {
+                               $msg = "$specialPage alias '$alias' in $code is valid with no slashes";
+                               $this->assertRegExp( '/^[^\/]*$/', $msg );
+                       }
+               }
+       }
+
+       public function validSpecialPageAliasesProvider() {
+               $codes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
+
+               $data = [];
+
+               foreach ( $codes as $code ) {
+                       $specialPageAliases = $this->getSpecialPageAliases( $code );
+
+                       if ( $specialPageAliases !== [] ) {
+                               $data[] = [ $code, $specialPageAliases ];
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * @param string $code
+        *
+        * @return array
+        */
+       protected function getSpecialPageAliases( $code ) {
+               $file = Language::getMessagesFileName( $code );
+
+               if ( is_readable( $file ) ) {
+                       include $file;
+
+                       if ( isset( $specialPageAliases ) && $specialPageAliases !== null ) {
+                               return $specialPageAliases;
+                       }
+               }
+
+               return [];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php
new file mode 100644 (file)
index 0000000..365c140
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+/**
+ * @todo Could use a test of extended XMP segments. Hard to find programs that
+ * create example files, and creating my own in vim propbably wouldn't
+ * serve as a very good "test". (Adobe photoshop probably creates such files
+ * but it costs money). The implementation of it currently in MediaWiki is based
+ * solely on reading the standard, without any real world test files.
+ *
+ * @group Media
+ * @covers JpegMetadataExtractor
+ */
+class JpegMetadataExtractorTest extends MediaWikiUnitTestCase {
+
+       protected $filePath;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->filePath = __DIR__ . '/../../../data/media/';
+       }
+
+       /**
+        * We also use this test to test padding bytes don't
+        * screw stuff up
+        *
+        * @param string $file Filename
+        *
+        * @dataProvider provideUtf8Comment
+        */
+       public function testUtf8Comment( $file ) {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
+               $this->assertEquals( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] );
+       }
+
+       public static function provideUtf8Comment() {
+               return [
+                       [ 'jpeg-comment-utf.jpg' ],
+                       [ 'jpeg-padding-even.jpg' ],
+                       [ 'jpeg-padding-odd.jpg' ],
+               ];
+       }
+
+       /** The file is iso-8859-1, but it should get auto converted */
+       public function testIso88591Comment() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
+               $this->assertEquals( [ 'ISO-8859-1 JPEG Comment - ¼' ], $res['COM'] );
+       }
+
+       /** Comment values that are non-textual (random binary junk) should not be shown.
+        * The example test file has a comment with a 0x5 byte in it which is a control character
+        * and considered binary junk for our purposes.
+        */
+       public function testBinaryCommentStripped() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
+               $this->assertEmpty( $res['COM'] );
+       }
+
+       /* Very rarely a file can have multiple comments.
+        *   Order of comments is based on order inside the file.
+        */
+       public function testMultipleComment() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
+               $this->assertEquals( [ 'foo', 'bar' ], $res['COM'] );
+       }
+
+       public function testXMPExtraction() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+               $this->assertEquals( $expected, $res['XMP'] );
+       }
+
+       public function testPSIRExtraction() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $expected = '50686f746f73686f7020332e30003842494d04040000000'
+                       . '000181c02190004746573741c02190003666f6f1c020000020004';
+               $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
+       }
+
+       public function testXMPExtractionAltAppId() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
+               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+               $this->assertEquals( $expected, $res['XMP'] );
+       }
+
+       public function testIPTCHashComparisionNoHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-no-hash', $res );
+       }
+
+       public function testIPTCHashComparisionBadHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-bad-hash', $res );
+       }
+
+       public function testIPTCHashComparisionGoodHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-good-hash', $res );
+       }
+
+       public function testExifByteOrder() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
+               $expected = 'BE';
+               $this->assertEquals( $expected, $res['byteOrder'] );
+       }
+
+       public function testInfiniteRead() {
+               // test file truncated right after a segment, which previously
+               // caused an infinite loop looking for the next segment byte.
+               // Should get past infinite loop and throw in wfUnpack()
+               $this->setExpectedException( 'MWException' );
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' );
+       }
+
+       public function testInfiniteRead2() {
+               // test file truncated after a segment's marker and size, which
+               // would cause a seek past end of file. Seek past end of file
+               // doesn't actually fail, but prevents further reading and was
+               // devolving into the previous case (testInfiniteRead).
+               $this->setExpectedException( 'MWException' );
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/page/ArticleTest.php b/tests/phpunit/unit/includes/page/ArticleTest.php
new file mode 100644 (file)
index 0000000..9995793
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+class ArticleTest extends MediaWikiUnitTestCase {
+
+       /**
+        * @var Title
+        */
+       private $title;
+       /**
+        * @var Article
+        */
+       private $article;
+
+       /** creates a title object and its article object */
+       protected function setUp() {
+               parent::setUp();
+               $this->title = Title::makeTitle( NS_MAIN, 'SomePage' );
+               $this->article = new Article( $this->title );
+       }
+
+       /** cleanup title object and its article object */
+       protected function tearDown() {
+               parent::tearDown();
+               $this->title = null;
+               $this->article = null;
+       }
+
+       /**
+        * @covers Article::__get
+        */
+       public function testImplementsGetMagic() {
+               $this->assertEquals( false, $this->article->mLatest, "Article __get magic" );
+       }
+
+       /**
+        * @depends testImplementsGetMagic
+        * @covers Article::__set
+        */
+       public function testImplementsSetMagic() {
+               $this->article->mLatest = 2;
+               $this->assertEquals( 2, $this->article->mLatest, "Article __set magic" );
+       }
+
+       /**
+        * @covers Article::__get
+        * @covers Article::__set
+        */
+       public function testGetOrSetOnNewProperty() {
+               $this->article->ext_someNewProperty = 12;
+               $this->assertEquals( 12, $this->article->ext_someNewProperty,
+                       "Article get/set magic on new field" );
+
+               $this->article->ext_someNewProperty = -8;
+               $this->assertEquals( -8, $this->article->ext_someNewProperty,
+                       "Article get/set magic on update to new field" );
+       }
+}
diff --git a/tests/phpunit/unit/includes/session/SessionUnitTest.php b/tests/phpunit/unit/includes/session/SessionUnitTest.php
new file mode 100644 (file)
index 0000000..b6e1d3a
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LogLevel;
+use MediaWikiUnitTestCase;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\Session
+ */
+class SessionUnitTest extends MediaWikiUnitTestCase {
+
+       public function testConstructor() {
+               $backend = TestUtils::getDummySessionBackend();
+               TestingAccessWrapper::newFromObject( $backend )->requests = [ -1 => 'dummy' ];
+               TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
+
+               $session = new Session( $backend, 42, new \TestLogger );
+               $priv = TestingAccessWrapper::newFromObject( $session );
+               $this->assertSame( $backend, $priv->backend );
+               $this->assertSame( 42, $priv->index );
+
+               $request = new \FauxRequest();
+               $priv2 = TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
+               $this->assertSame( $backend, $priv2->backend );
+               $this->assertNotSame( $priv->index, $priv2->index );
+               $this->assertSame( $request, $priv2->getRequest() );
+       }
+
+       /**
+        * @dataProvider provideMethods
+        * @param string $m Method to test
+        * @param array $args Arguments to pass to the method
+        * @param bool $index Whether the backend method gets passed the index
+        * @param bool $ret Whether the method returns a value
+        */
+       public function testMethods( $m, $args, $index, $ret ) {
+               $mock = $this->getMockBuilder( DummySessionBackend::class )
+                       ->setMethods( [ $m, 'deregisterSession' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'deregisterSession' )
+                       ->with( $this->identicalTo( 42 ) );
+
+               $tmp = $mock->expects( $this->once() )->method( $m );
+               $expectArgs = [];
+               if ( $index ) {
+                       $expectArgs[] = $this->identicalTo( 42 );
+               }
+               foreach ( $args as $arg ) {
+                       $expectArgs[] = $this->identicalTo( $arg );
+               }
+               $tmp = call_user_func_array( [ $tmp, 'with' ], $expectArgs );
+
+               $retval = new \stdClass;
+               $tmp->will( $this->returnValue( $retval ) );
+
+               $session = TestUtils::getDummySession( $mock, 42 );
+
+               if ( $ret ) {
+                       $this->assertSame( $retval, call_user_func_array( [ $session, $m ], $args ) );
+               } else {
+                       $this->assertNull( call_user_func_array( [ $session, $m ], $args ) );
+               }
+
+               // Trigger Session destructor
+               $session = null;
+       }
+
+       public static function provideMethods() {
+               return [
+                       [ 'getId', [], false, true ],
+                       [ 'getSessionId', [], false, true ],
+                       [ 'resetId', [], false, true ],
+                       [ 'getProvider', [], false, true ],
+                       [ 'isPersistent', [], false, true ],
+                       [ 'persist', [], false, false ],
+                       [ 'unpersist', [], false, false ],
+                       [ 'shouldRememberUser', [], false, true ],
+                       [ 'setRememberUser', [ true ], false, false ],
+                       [ 'getRequest', [], true, true ],
+                       [ 'getUser', [], false, true ],
+                       [ 'getAllowedUserRights', [], false, true ],
+                       [ 'canSetUser', [], false, true ],
+                       [ 'setUser', [ new \stdClass ], false, false ],
+                       [ 'suggestLoginUsername', [], true, true ],
+                       [ 'shouldForceHTTPS', [], false, true ],
+                       [ 'setForceHTTPS', [ true ], false, false ],
+                       [ 'getLoggedOutTimestamp', [], false, true ],
+                       [ 'setLoggedOutTimestamp', [ 123 ], false, false ],
+                       [ 'getProviderMetadata', [], false, true ],
+                       [ 'save', [], false, false ],
+                       [ 'delaySave', [], false, true ],
+                       [ 'renew', [], false, false ],
+               ];
+       }
+
+       public function testDataAccess() {
+               $session = TestUtils::getDummySession();
+               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
+
+               $this->assertEquals( 1, $session->get( 'foo' ) );
+               $this->assertEquals( 'zero', $session->get( 0 ) );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertEquals( null, $session->get( 'null' ) );
+               $this->assertEquals( 'default', $session->get( 'null', 'default' ) );
+               $this->assertFalse( $backend->dirty );
+
+               $session->set( 'foo', 55 );
+               $this->assertEquals( 55, $backend->data['foo'] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->set( 1, 'one' );
+               $this->assertEquals( 'one', $backend->data[1] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->set( 1, 'one' );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertTrue( $session->exists( 'foo' ) );
+               $this->assertTrue( $session->exists( 1 ) );
+               $this->assertFalse( $session->exists( 'null' ) );
+               $this->assertFalse( $session->exists( 100 ) );
+               $this->assertFalse( $backend->dirty );
+
+               $session->remove( 'foo' );
+               $this->assertArrayNotHasKey( 'foo', $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+               $session->remove( 1 );
+               $this->assertArrayNotHasKey( 1, $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->remove( 101 );
+               $this->assertFalse( $backend->dirty );
+
+               $backend->data = [ 'a', 'b', '?' => 'c' ];
+               $this->assertSame( 3, $session->count() );
+               $this->assertSame( 3, count( $session ) );
+               $this->assertFalse( $backend->dirty );
+
+               $data = [];
+               foreach ( $session as $key => $value ) {
+                       $data[$key] = $value;
+               }
+               $this->assertEquals( $backend->data, $data );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertEquals( $backend->data, iterator_to_array( $session ) );
+               $this->assertFalse( $backend->dirty );
+       }
+
+       public function testArrayAccess() {
+               $logger = new \TestLogger;
+               $session = TestUtils::getDummySession( null, -1, $logger );
+               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
+
+               $this->assertEquals( 1, $session['foo'] );
+               $this->assertEquals( 'zero', $session[0] );
+               $this->assertFalse( $backend->dirty );
+
+               $logger->setCollect( true );
+               $this->assertEquals( null, $session['null'] );
+               $logger->setCollect( false );
+               $this->assertFalse( $backend->dirty );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ]
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $session['foo'] = 55;
+               $this->assertEquals( 55, $backend->data['foo'] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session[1] = 'one';
+               $this->assertEquals( 'one', $backend->data[1] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session[1] = 'one';
+               $this->assertFalse( $backend->dirty );
+
+               $session['bar'] = [ 'baz' => [] ];
+               $session['bar']['baz']['quux'] = 2;
+               $this->assertEquals( [ 'baz' => [ 'quux' => 2 ] ], $backend->data['bar'] );
+
+               $logger->setCollect( true );
+               $session['bar2']['baz']['quux'] = 3;
+               $logger->setCollect( false );
+               $this->assertEquals( [ 'baz' => [ 'quux' => 3 ] ], $backend->data['bar2'] );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ]
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $backend->dirty = false;
+               $this->assertTrue( isset( $session['foo'] ) );
+               $this->assertTrue( isset( $session[1] ) );
+               $this->assertFalse( isset( $session['null'] ) );
+               $this->assertFalse( isset( $session['missing'] ) );
+               $this->assertFalse( isset( $session[100] ) );
+               $this->assertFalse( $backend->dirty );
+
+               unset( $session['foo'] );
+               $this->assertArrayNotHasKey( 'foo', $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+               unset( $session[1] );
+               $this->assertArrayNotHasKey( 1, $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               unset( $session[101] );
+               $this->assertFalse( $backend->dirty );
+       }
+
+       public function testTokens() {
+               $session = TestUtils::getDummySession();
+               $priv = TestingAccessWrapper::newFromObject( $session );
+               $backend = $priv->backend;
+
+               $token = TestingAccessWrapper::newFromObject( $session->getToken() );
+               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
+               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
+               $secret = $backend->data['wsTokenSecrets']['default'];
+               $this->assertSame( $secret, $token->secret );
+               $this->assertSame( '', $token->salt );
+               $this->assertTrue( $token->wasNew() );
+
+               $token = TestingAccessWrapper::newFromObject( $session->getToken( 'foo' ) );
+               $this->assertSame( $secret, $token->secret );
+               $this->assertSame( 'foo', $token->salt );
+               $this->assertFalse( $token->wasNew() );
+
+               $backend->data['wsTokenSecrets']['secret'] = 'sekret';
+               $token = TestingAccessWrapper::newFromObject(
+                       $session->getToken( [ 'bar', 'baz' ], 'secret' )
+               );
+               $this->assertSame( 'sekret', $token->secret );
+               $this->assertSame( 'bar|baz', $token->salt );
+               $this->assertFalse( $token->wasNew() );
+
+               $session->resetToken( 'secret' );
+               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
+               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
+               $this->assertArrayNotHasKey( 'secret', $backend->data['wsTokenSecrets'] );
+
+               $session->resetAllTokens();
+               $this->assertArrayNotHasKey( 'wsTokenSecrets', $backend->data );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/session/TokenTest.php b/tests/phpunit/unit/includes/session/TokenTest.php
new file mode 100644 (file)
index 0000000..5546603
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use MediaWikiUnitTestCase;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\Token
+ */
+class TokenTest extends MediaWikiUnitTestCase {
+
+       public function testBasics() {
+               $token = $this->getMockBuilder( Token::class )
+                       ->setMethods( [ 'toStringAtTimestamp' ] )
+                       ->setConstructorArgs( [ 'sekret', 'salty', true ] )
+                       ->getMock();
+               $token->expects( $this->any() )->method( 'toStringAtTimestamp' )
+                       ->will( $this->returnValue( 'faketoken+\\' ) );
+
+               $this->assertSame( 'faketoken+\\', $token->toString() );
+               $this->assertSame( 'faketoken+\\', (string)$token );
+               $this->assertTrue( $token->wasNew() );
+
+               $token = new Token( 'sekret', 'salty', false );
+               $this->assertFalse( $token->wasNew() );
+       }
+
+       public function testToStringAtTimestamp() {
+               $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) );
+
+               $this->assertSame(
+                       'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\',
+                       $token->toStringAtTimestamp( 1447362018 )
+               );
+               $this->assertSame(
+                       'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\',
+                       $token->toStringAtTimestamp( 1447362026 )
+               );
+       }
+
+       public function testGetTimestamp() {
+               $this->assertSame(
+                       1447362018, Token::getTimestamp( 'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\' )
+               );
+               $this->assertSame(
+                       1447362026, Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\' )
+               );
+               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) );
+               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be+\\' ) );
+
+               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9x76c224cfb400d43be5644fdea+\\' ) );
+       }
+
+       public function testMatch() {
+               $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) );
+
+               $test = $token->toStringAtTimestamp( time() - 10 );
+               $this->assertTrue( $token->match( $test ) );
+               $this->assertTrue( $token->match( $test, 12 ) );
+               $this->assertFalse( $token->match( $test, 8 ) );
+
+               $this->assertFalse( $token->match( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/shell/FirejailCommandTest.php b/tests/phpunit/unit/includes/shell/FirejailCommandTest.php
new file mode 100644 (file)
index 0000000..e3a3b86
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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.
+ *
+ */
+
+use MediaWiki\Shell\FirejailCommand;
+use MediaWiki\Shell\Shell;
+use Wikimedia\TestingAccessWrapper;
+
+class FirejailCommandTest extends MediaWikiUnitTestCase {
+
+       public function provideBuildFinalCommand() {
+               global $IP;
+               // phpcs:ignore Generic.Files.LineLength
+               $env = "'MW_INCLUDE_STDERR=;MW_CPU_LIMIT=180; MW_CGROUP='\'''\''; MW_MEM_LIMIT=307200; MW_FILE_SIZE_LIMIT=102400; MW_WALL_CLOCK_LIMIT=180; MW_USE_LOG_PIPE=yes'";
+               $limit = "/bin/bash '$IP/includes/shell/limit.sh'";
+               $profile = "--profile=$IP/includes/shell/firejail.profile";
+               $blacklist = '--blacklist=' . realpath( MW_CONFIG_FILE );
+               $default = "$blacklist --noroot --seccomp --private-dev";
+               return [
+                       [
+                               'No restrictions',
+                               'ls', 0, "$limit ''\''ls'\''' $env"
+                       ],
+                       [
+                               'default restriction',
+                               'ls', Shell::RESTRICT_DEFAULT,
+                               "$limit 'firejail --quiet $profile $default -- '\''ls'\''' $env"
+                       ],
+                       [
+                               'no network',
+                               'ls', Shell::NO_NETWORK,
+                               "$limit 'firejail --quiet $profile --net=none -- '\''ls'\''' $env"
+                       ],
+                       [
+                               'default restriction & no network',
+                               'ls', Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK,
+                               "$limit 'firejail --quiet $profile $default --net=none -- '\''ls'\''' $env"
+                       ],
+                       [
+                               'seccomp',
+                               'ls', Shell::SECCOMP,
+                               "$limit 'firejail --quiet $profile --seccomp -- '\''ls'\''' $env"
+                       ],
+                       [
+                               'seccomp & no execve',
+                               'ls', Shell::SECCOMP | Shell::NO_EXECVE,
+                               "$limit 'firejail --quiet $profile --shell=none --seccomp=execve -- '\''ls'\''' $env"
+                       ],
+               ];
+       }
+
+       /**
+        * @covers \MediaWiki\Shell\FirejailCommand::buildFinalCommand()
+        * @dataProvider provideBuildFinalCommand
+        */
+       public function testBuildFinalCommand( $desc, $params, $flags, $expected ) {
+               $command = new FirejailCommand( 'firejail' );
+               $command
+                       ->params( $params )
+                       ->restrict( $flags );
+               $wrapper = TestingAccessWrapper::newFromObject( $command );
+               $output = $wrapper->buildFinalCommand( $wrapper->command );
+               $this->assertEquals( $expected, $output[0], $desc );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php b/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php
new file mode 100644 (file)
index 0000000..d426306
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+
+use MediaWiki\Site\MediaWikiPageNameNormalizer;
+
+/**
+ * @covers MediaWiki\Site\MediaWikiPageNameNormalizer
+ *
+ * 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
+ *
+ * @since 1.27
+ *
+ * @group Site
+ * @group medium
+ *
+ * @author Marius Hoch
+ */
+class MediaWikiPageNameNormalizerTest extends MediaWikiUnitTestCase {
+
+       /**
+        * @dataProvider normalizePageTitleProvider
+        */
+       public function testNormalizePageTitle( $expected, $pageName, $getResponse ) {
+               MediaWikiPageNameNormalizerTestMockHttp::$response = $getResponse;
+
+               $normalizer = new MediaWikiPageNameNormalizer(
+                       new MediaWikiPageNameNormalizerTestMockHttp()
+               );
+
+               $this->assertSame(
+                       $expected,
+                       $normalizer->normalizePageName( $pageName, 'https://www.wikidata.org/w/api.php' )
+               );
+       }
+
+       public function normalizePageTitleProvider() {
+               // Response are taken from wikidata and kkwiki using the following API request
+               // api.php?action=query&prop=info&redirects=1&converttitles=1&format=json&titles=…
+               return [
+                       'universe (Q1)' => [
+                               'Q1',
+                               'Q1',
+                               '{"batchcomplete":"","query":{"pages":{"129":{"pageid":129,"ns":0,'
+                               . '"title":"Q1","contentmodel":"wikibase-item","pagelanguage":"en",'
+                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
+                               . '"touched":"2016-06-23T05:11:21Z","lastrevid":350004448,"length":58001}}}}'
+                       ],
+                       'Q404 redirects to Q395' => [
+                               'Q395',
+                               'Q404',
+                               '{"batchcomplete":"","query":{"redirects":[{"from":"Q404","to":"Q395"}],"pages"'
+                               . ':{"601":{"pageid":601,"ns":0,"title":"Q395","contentmodel":"wikibase-item",'
+                               . '"pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
+                               . '"touched":"2016-06-23T08:00:20Z","lastrevid":350021914,"length":60108}}}}'
+                       ],
+                       'D converted to Д (Latin to Cyrillic) (taken from kkwiki)' => [
+                               'Д',
+                               'D',
+                               '{"batchcomplete":"","query":{"converted":[{"from":"D","to":"\u0414"}],'
+                               . '"pages":{"510541":{"pageid":510541,"ns":0,"title":"\u0414",'
+                               . '"contentmodel":"wikitext","pagelanguage":"kk","pagelanguagehtmlcode":"kk",'
+                               . '"pagelanguagedir":"ltr","touched":"2015-11-22T09:16:18Z",'
+                               . '"lastrevid":2373618,"length":3501}}}}'
+                       ],
+                       'there is no Q0' => [
+                               false,
+                               'Q0',
+                               '{"batchcomplete":"","query":{"pages":{"-1":{"ns":0,"title":"Q0",'
+                               . '"missing":"","contentmodel":"wikibase-item","pagelanguage":"en",'
+                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr"}}}}'
+                       ],
+                       'invalid title' => [
+                               false,
+                               '{{',
+                               '{"batchcomplete":"","query":{"pages":{"-1":{"title":"{{",'
+                               . '"invalidreason":"The requested page title contains invalid '
+                               . 'characters: \"{\".","invalid":""}}}}'
+                       ],
+                       'error on get' => [ false, 'ABC', false ]
+               ];
+       }
+
+}
+
+/**
+ * @private
+ * @see Http
+ */
+class MediaWikiPageNameNormalizerTestMockHttp extends Http {
+
+       /**
+        * @var mixed
+        */
+       public static $response;
+
+       public static function get( $url, array $options = [], $caller = __METHOD__ ) {
+               PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
+               PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
+
+               return self::$response;
+       }
+}
diff --git a/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php
new file mode 100644 (file)
index 0000000..be7e224
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @covers ZipDirectoryReader
+ * NOTE: this test is more like an integration test than a unit test
+ */
+class ZipDirectoryReaderTest extends MediaWikiUnitTestCase {
+
+       protected $zipDir;
+       protected $entries;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->zipDir = __DIR__ . '/../../../data/zip';
+       }
+
+       function zipCallback( $entry ) {
+               $this->entries[] = $entry;
+       }
+
+       function readZipAssertError( $file, $error, $assertMessage ) {
+               $this->entries = [];
+               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
+               $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
+       }
+
+       function readZipAssertSuccess( $file, $assertMessage ) {
+               $this->entries = [];
+               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
+               $this->assertTrue( $status->isOK(), $assertMessage );
+       }
+
+       public function testEmpty() {
+               $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
+       }
+
+       public function testMultiDisk0() {
+               $this->readZipAssertError( 'split.zip', 'zip-unsupported',
+                       'Split zip error' );
+       }
+
+       public function testNoSignature() {
+               $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
+                       'No signature should give "wrong format" error' );
+       }
+
+       public function testSimple() {
+               $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
+               $this->assertEquals( $this->entries, [ [
+                       'name' => 'Class.class',
+                       'mtime' => '20010115000000',
+                       'size' => 1,
+               ] ] );
+       }
+
+       public function testBadCentralEntrySignature() {
+               $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
+                       'Bad central entry error' );
+       }
+
+       public function testTrailingBytes() {
+               // Due to T40432 this is now zip-wrong-format instead of zip-bad
+               $this->readZipAssertError( 'trail.zip', 'zip-wrong-format',
+                       'Trailing bytes error' );
+       }
+
+       public function testWrongCDStart() {
+               $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
+                       'Wrong CD start disk error' );
+       }
+
+       public function testCentralDirectoryGap() {
+               $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
+                       'CD gap error' );
+       }
+
+       public function testCentralDirectoryTruncated() {
+               $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
+                       'CD truncated error (should hit unpack() overrun)' );
+       }
+
+       public function testLooksLikeZip64() {
+               $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
+                       'A file which looks like ZIP64 but isn\'t, should give error' );
+       }
+}
diff --git a/tests/phpunit/unit/languages/SpecialPageAliasTest.php b/tests/phpunit/unit/languages/SpecialPageAliasTest.php
deleted file mode 100644 (file)
index cce9d0e..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-/**
- * Verifies that special page aliases are valid, with no slashes.
- *
- * @group Language
- * @group SpecialPageAliases
- * @group SystemTest
- * @group medium
- * @todo This should be a structure test
- *
- * @author Katie Filbert < aude.wiki@gmail.com >
- */
-class SpecialPageAliasTest extends \MediaWikiUnitTestCase {
-
-       /**
-        * @coversNothing
-        * @dataProvider validSpecialPageAliasesProvider
-        */
-       public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
-               foreach ( $specialPageAliases as $specialPage => $aliases ) {
-                       foreach ( $aliases as $alias ) {
-                               $msg = "$specialPage alias '$alias' in $code is valid with no slashes";
-                               $this->assertRegExp( '/^[^\/]*$/', $msg );
-                       }
-               }
-       }
-
-       public function validSpecialPageAliasesProvider() {
-               $codes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
-
-               $data = [];
-
-               foreach ( $codes as $code ) {
-                       $specialPageAliases = $this->getSpecialPageAliases( $code );
-
-                       if ( $specialPageAliases !== [] ) {
-                               $data[] = [ $code, $specialPageAliases ];
-                       }
-               }
-
-               return $data;
-       }
-
-       /**
-        * @param string $code
-        *
-        * @return array
-        */
-       protected function getSpecialPageAliases( $code ) {
-               $file = Language::getMessagesFileName( $code );
-
-               if ( is_readable( $file ) ) {
-                       include $file;
-
-                       if ( isset( $specialPageAliases ) && $specialPageAliases !== null ) {
-                               return $specialPageAliases;
-                       }
-               }
-
-               return [];
-       }
-
-}