Merge "Title: Title::getSubpage should not lose the interwiki prefix"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 13 Jul 2019 18:02:27 +0000 (18:02 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 13 Jul 2019 18:02:27 +0000 (18:02 +0000)
189 files changed:
RELEASE-NOTES-1.34
autoload.php
composer.json
docs/hooks.txt
docs/pageupdater.txt
includes/CategoryFinder.php
includes/DefaultSettings.php
includes/GlobalFunctions.php
includes/Html.php
includes/Linker.php
includes/MediaWiki.php
includes/Rest/BasicAccess/BasicAuthorizerBase.php [new file with mode: 0644]
includes/Rest/BasicAccess/BasicAuthorizerInterface.php [new file with mode: 0644]
includes/Rest/BasicAccess/BasicRequestAuthorizer.php [new file with mode: 0644]
includes/Rest/BasicAccess/MWBasicAuthorizer.php [new file with mode: 0644]
includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php [new file with mode: 0644]
includes/Rest/BasicAccess/StaticBasicAuthorizer.php [new file with mode: 0644]
includes/Rest/EntryPoint.php
includes/Rest/Handler.php
includes/Rest/Handler/HelloHandler.php
includes/Rest/Router.php
includes/Revision/RevisionStore.php
includes/ServiceWiring.php
includes/Setup.php
includes/Storage/DerivedPageDataUpdater.php
includes/Title.php
includes/actions/HistoryAction.php
includes/actions/pagers/HistoryPager.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/ApiQueryDeletedRevisions.php
includes/api/ApiQueryDeletedrevs.php
includes/block/BlockManager.php
includes/cache/LinkCache.php
includes/cache/localisation/LocalisationCache.php
includes/config/ConfigException.php
includes/db/DatabaseOracle.php
includes/deferred/DeferredUpdates.php
includes/deferred/LinksUpdate.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/vi.json
includes/jobqueue/JobQueueRedis.php
includes/jobqueue/jobs/CategoryMembershipChangeJob.php
includes/jobqueue/jobs/ClearUserWatchlistJob.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/libs/MapCacheLRU.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/filebackend/HTTPFileStreamer.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/rdbms/loadmonitor/LoadMonitor.php
includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php
includes/objectcache/ObjectCache.php
includes/objectcache/SqlBagOStuff.php
includes/page/WikiPage.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderImage.php
includes/session/PHPSessionHandler.php
includes/specialpage/RedirectSpecialPage.php
includes/specials/SpecialBrokenRedirects.php
includes/specials/SpecialChangeCredentials.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/user/User.php
includes/user/UserGroupMembership.php
includes/watcheditem/WatchedItemStore.php
languages/i18n/ban.json
languages/i18n/be-tarask.json
languages/i18n/ce.json
languages/i18n/de.json
languages/i18n/es.json
languages/i18n/exif/sr-ec.json
languages/i18n/hr.json
languages/i18n/hy.json
languages/i18n/id.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/mctest.php
maintenance/nukePage.php
maintenance/rebuildrecentchanges.php
maintenance/storage/orphanStats.php
maintenance/storage/recompressTracked.php
maintenance/syncFileBackend.php
maintenance/tables.sql
maintenance/update.php
maintenance/updateSearchIndex.php
maintenance/uppercaseTitlesForUnicodeTransition.php [new file with mode: 0644]
phpunit.xml.dist
resources/lib/foreign-resources.yaml
resources/lib/ooui/History.md
resources/lib/ooui/oojs-ui-apex.js
resources/lib/ooui/oojs-ui-core-apex.css
resources/lib/ooui/oojs-ui-core-wikimediaui.css
resources/lib/ooui/oojs-ui-core.js
resources/lib/ooui/oojs-ui-core.js.map.json
resources/lib/ooui/oojs-ui-toolbars-apex.css
resources/lib/ooui/oojs-ui-toolbars-wikimediaui.css
resources/lib/ooui/oojs-ui-toolbars.js
resources/lib/ooui/oojs-ui-widgets-apex.css
resources/lib/ooui/oojs-ui-widgets-wikimediaui.css
resources/lib/ooui/oojs-ui-widgets.js
resources/lib/ooui/oojs-ui-widgets.js.map.json
resources/lib/ooui/oojs-ui-wikimediaui.js
resources/lib/ooui/oojs-ui-windows-apex.css
resources/lib/ooui/oojs-ui-windows-wikimediaui.css
resources/lib/ooui/oojs-ui-windows.js
resources/lib/ooui/oojs-ui-windows.js.map.json
resources/src/mediawiki.legacy/shared.css
resources/src/mediawiki.less/mediawiki.ui/variables.less
resources/src/mediawiki.ui/components/forms.less
rest.php [new file with mode: 0644]
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/MediaWikiUnitTestCase.php
tests/phpunit/ResourceLoaderTestCase.php
tests/phpunit/bootstrap.php
tests/phpunit/includes/ContentSecurityPolicyTest.php
tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
tests/phpunit/includes/SampleTest.php
tests/phpunit/includes/block/BlockManagerTest.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/db/LoadBalancerTest.php
tests/phpunit/includes/filebackend/HTTPFileStreamerTest.php [new file with mode: 0644]
tests/phpunit/includes/jobqueue/jobs/RefreshLinksJobTest.php
tests/phpunit/includes/libs/MapCacheLRUTest.php
tests/phpunit/includes/libs/objectcache/BagOStuffTest.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/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/languages/LanguageCodeTest.php [deleted file]
tests/phpunit/unit/includes/Rest/EntryPointTest.php
tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php
tests/phpunit/unit/includes/Rest/RouterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Rest/testRoutes.json
tests/phpunit/unit/languages/LanguageCodeTest.php [new file with mode: 0644]

index 5e49fc7..be24b50 100644 (file)
@@ -74,7 +74,7 @@ For notes on 1.33.x and older releases, see HISTORY.
 
 ==== Changed external libraries ====
 * Updated Mustache from 1.0.0 to v3.0.1.
-* Updated OOUI from v0.31.3 to v0.33.1.
+* Updated OOUI from v0.31.3 to v0.33.2.
 * Updated composer/semver from 1.4.2 to 1.5.0.
 * Updated composer/spdx-licenses from 1.4.0 to 1.5.1 (dev-only).
 * Updated mediawiki/codesniffer from 25.0.0 to 26.0.0 (dev-only).
@@ -82,9 +82,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 +266,12 @@ 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.
 * …
 
 === Deprecations in 1.34 ===
@@ -342,6 +349,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 218c244..9f9f1a6 100644 (file)
@@ -1560,6 +1560,7 @@ $wgAutoloadLocalClasses = [
        'UploadStashWrongOwnerException' => __DIR__ . '/includes/upload/exception/UploadStashWrongOwnerException.php',
        'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/exception/UploadStashZeroLengthFileException.php',
        'UppercaseCollation' => __DIR__ . '/includes/collation/UppercaseCollation.php',
+       'UppercaseTitlesForUnicodeTransition' => __DIR__ . '/maintenance/uppercaseTitlesForUnicodeTransition.php',
        'User' => __DIR__ . '/includes/user/User.php',
        'UserArray' => __DIR__ . '/includes/user/UserArray.php',
        'UserArrayFromResult' => __DIR__ . '/includes/user/UserArrayFromResult.php',
index f7b72f5..307e310 100644 (file)
@@ -27,7 +27,7 @@
                "ext-xml": "*",
                "guzzlehttp/guzzle": "6.3.3",
                "liuggio/statsd-php-client": "1.0.18",
-               "oojs/oojs-ui": "0.33.1",
+               "oojs/oojs-ui": "0.33.2",
                "pear/mail": "1.4.1",
                "pear/mail_mime": "1.10.2",
                "pear/net_smtp": "1.8.1",
@@ -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 9b5a38b..0886f38 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
@@ -8300,6 +8324,13 @@ $wgCrossSiteAJAXdomains = [];
  */
 $wgCrossSiteAJAXdomainExceptions = [];
 
+/**
+ * Enable the experimental REST API.
+ *
+ * This will be removed once the REST API is stable and used by clients.
+ */
+$wgEnableRestAPI = false;
+
 /** @} */ # End AJAX and API }
 
 /************************************************************************//**
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 f3d492f..2e0011c 100644 (file)
@@ -1121,7 +1121,7 @@ class Linker {
                ) {
                        $userId = $rev->getUser( Revision::FOR_THIS_USER );
                        $userText = $rev->getUserText( Revision::FOR_THIS_USER );
-                       if ( $userId && $userText ) {
+                       if ( $userId || (string)$userText !== '' ) {
                                $link = self::userLink( $userId, $userText )
                                        . self::userToolLinks( $userId, $userText, false, 0, null,
                                                $useParentheses );
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() );
diff --git a/includes/Rest/BasicAccess/BasicAuthorizerBase.php b/includes/Rest/BasicAccess/BasicAuthorizerBase.php
new file mode 100644 (file)
index 0000000..7aefe25
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+namespace MediaWiki\Rest\BasicAccess;
+
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * An implementation of BasicAuthorizerInterface which creates a request-local
+ * object (a request authorizer) to do the actual authorization.
+ *
+ * @internal
+ */
+abstract class BasicAuthorizerBase implements BasicAuthorizerInterface {
+       public function authorize( RequestInterface $request, Handler $handler ) {
+               return $this->createRequestAuthorizer( $request, $handler )->authorize();
+       }
+
+       /**
+        * Create a BasicRequestAuthorizer to authorize the request.
+        *
+        * @param RequestInterface $request
+        * @param Handler $handler
+        * @return BasicRequestAuthorizer
+        */
+       abstract protected function createRequestAuthorizer( RequestInterface $request,
+               Handler $handler ) : BasicRequestAuthorizer;
+}
diff --git a/includes/Rest/BasicAccess/BasicAuthorizerInterface.php b/includes/Rest/BasicAccess/BasicAuthorizerInterface.php
new file mode 100644 (file)
index 0000000..64143d4
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+namespace MediaWiki\Rest\BasicAccess;
+
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * An interface used by Router to ensure that the client has "basic" access,
+ * i.e. the ability to read or write to the wiki.
+ *
+ * @internal
+ */
+interface BasicAuthorizerInterface {
+       /**
+        * Determine whether a request should be permitted, given the handler's
+        * needsReadAccess() and needsWriteAccess().
+        *
+        * If the request should be permitted, return null. If the request should
+        * be denied, return a string error identifier.
+        *
+        * @param RequestInterface $request
+        * @param Handler $handler
+        * @return string|null If the request is denied, the string error code. If
+        *   the request is allowed, null.
+        */
+       public function authorize( RequestInterface $request, Handler $handler );
+}
diff --git a/includes/Rest/BasicAccess/BasicRequestAuthorizer.php b/includes/Rest/BasicAccess/BasicRequestAuthorizer.php
new file mode 100644 (file)
index 0000000..2c97732
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+namespace MediaWiki\Rest\BasicAccess;
+
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * A request authorizer which checks needsReadAccess() and needsWriteAccess() in the
+ * handler and calls isReadAllowed() and/or isWriteAllowed() in the subclass
+ * accordingly.
+ *
+ * @internal
+ */
+abstract class BasicRequestAuthorizer {
+       protected $request;
+       protected $handler;
+
+       /**
+        * @param RequestInterface $request
+        * @param Handler $handler
+        */
+       public function __construct( RequestInterface $request, Handler $handler ) {
+               $this->request = $request;
+               $this->handler = $handler;
+       }
+
+       /**
+        * @see BasicAuthorizerInterface::authorize()
+        * @return string|null If the request is denied, the string error code. If
+        *   the request is allowed, null.
+        */
+       public function authorize() {
+               if ( $this->handler->needsReadAccess() && !$this->isReadAllowed() ) {
+                       return 'rest-read-denied';
+               }
+               if ( $this->handler->needsWriteAccess() && !$this->isWriteAllowed() ) {
+                       return 'rest-write-denied';
+               }
+               return null;
+       }
+
+       /**
+        * Check if the current user is allowed to read from the wiki
+        *
+        * @return bool
+        */
+       abstract protected function isReadAllowed();
+
+       /**
+        * Check if the current user is allowed to write to the wiki
+        *
+        * @return bool
+        */
+       abstract protected function isWriteAllowed();
+}
diff --git a/includes/Rest/BasicAccess/MWBasicAuthorizer.php b/includes/Rest/BasicAccess/MWBasicAuthorizer.php
new file mode 100644 (file)
index 0000000..43014f1
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+namespace MediaWiki\Rest\BasicAccess;
+
+use User;
+use MediaWiki\Permissions\PermissionManager;
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * A factory for MWBasicRequestAuthorizer which passes through a User object
+ *
+ * @internal
+ */
+class MWBasicAuthorizer extends BasicAuthorizerBase {
+       /** @var User */
+       private $user;
+
+       /** @var PermissionManager */
+       private $permissionManager;
+
+       public function __construct( User $user, PermissionManager $permissionManager ) {
+               $this->user = $user;
+               $this->permissionManager = $permissionManager;
+       }
+
+       protected function createRequestAuthorizer( RequestInterface $request,
+               Handler $handler
+       ): BasicRequestAuthorizer {
+               return new MWBasicRequestAuthorizer( $request, $handler, $this->user,
+                       $this->permissionManager );
+       }
+}
diff --git a/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php b/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php
new file mode 100644 (file)
index 0000000..8c459c6
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+namespace MediaWiki\Rest\BasicAccess;
+
+use User;
+use MediaWiki\Permissions\PermissionManager;
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * The concrete implementation of basic read/write restrictions in MediaWiki
+ *
+ * @internal
+ */
+class MWBasicRequestAuthorizer extends BasicRequestAuthorizer {
+       /** @var User */
+       private $user;
+
+       /** @var PermissionManager */
+       private $permissionManager;
+
+       public function __construct( RequestInterface $request, Handler $handler,
+               User $user, PermissionManager $permissionManager
+       ) {
+               parent::__construct( $request, $handler );
+               $this->user = $user;
+               $this->permissionManager = $permissionManager;
+       }
+
+       protected function isReadAllowed() {
+               return $this->permissionManager->isEveryoneAllowed( 'read' )
+                  || $this->isAllowed( 'read' );
+       }
+
+       protected function isWriteAllowed() {
+               return $this->isAllowed( 'writeapi' );
+       }
+
+       private function isAllowed( $action ) {
+               return $this->permissionManager->userHasRight( $this->user, $action );
+       }
+}
diff --git a/includes/Rest/BasicAccess/StaticBasicAuthorizer.php b/includes/Rest/BasicAccess/StaticBasicAuthorizer.php
new file mode 100644 (file)
index 0000000..c4dcda1
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace MediaWiki\Rest\BasicAccess;
+
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * An authorizer which returns a value from authorize() which is given in the constructor.
+ *
+ * @internal
+ */
+class StaticBasicAuthorizer implements BasicAuthorizerInterface {
+       private $value;
+
+       /**
+        * @see BasicAuthorizerInterface::authorize()
+        *
+        * @param string|null $value The value returned by authorize(). If the
+        *   request is denied, this is the string error code. If the request is
+        *   allowed, it is null.
+        */
+       public function __construct( $value = null ) {
+               $this->value = $value;
+       }
+
+       public function authorize( RequestInterface $request, Handler $handler ) {
+               return $this->value;
+       }
+}
index 795999a..a14c1a1 100644 (file)
@@ -4,6 +4,7 @@ namespace MediaWiki\Rest;
 
 use ExtensionRegistry;
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer;
 use RequestContext;
 use Title;
 use WebResponse;
@@ -31,17 +32,27 @@ class EntryPoint {
                $services = MediaWikiServices::getInstance();
                $conf = $services->getMainConfig();
 
+               if ( !$conf->get( 'EnableRestAPI' ) ) {
+                       wfHttpError( 403, 'Access Denied',
+                               'Set $wgEnableRestAPI to true to enable the experimental REST API' );
+                       return;
+               }
+
                $request = new RequestFromGlobals( [
                        'cookiePrefix' => $conf->get( 'CookiePrefix' )
                ] );
 
+               $authorizer = new MWBasicAuthorizer( RequestContext::getMain()->getUser(),
+                       $services->getPermissionManager() );
+
                global $IP;
                $router = new Router(
                        [ "$IP/includes/Rest/coreRoutes.json" ],
                        ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ),
                        $conf->get( 'RestPath' ),
                        $services->getLocalServerObjectCache(),
-                       new ResponseFactory
+                       new ResponseFactory,
+                       $authorizer
                );
 
                $entryPoint = new self(
index cee403f..c05d8e7 100644 (file)
@@ -95,6 +95,35 @@ abstract class Handler {
                return null;
        }
 
+       /**
+        * Indicates whether this route requires read rights.
+        *
+        * The handler should override this if it does not need to read from the
+        * wiki. This is uncommon, but may be useful for login and other account
+        * management APIs.
+        *
+        * @return bool
+        */
+       public function needsReadAccess() {
+               return true;
+       }
+
+       /**
+        * Indicates whether this route requires write access.
+        *
+        * The handler should override this if the route does not need to write to
+        * the database.
+        *
+        * This should return true for routes that may require synchronous database writes.
+        * Modules that do not need such writes should also not rely on master database access,
+        * since only read queries are needed and each master DB is a single point of failure.
+        *
+        * @return bool
+        */
+       public function needsWriteAccess() {
+               return true;
+       }
+
        /**
         * Execute the handler. This is called after parameter validation. The
         * return value can either be a Response or any type accepted by
index 6e119dd..34faee2 100644 (file)
@@ -12,4 +12,8 @@ class HelloHandler extends SimpleHandler {
        public function run( $name ) {
                return [ 'message' => "Hello, $name!" ];
        }
+
+       public function needsWriteAccess() {
+               return false;
+       }
 }
index 5ba3d08..14b4c9c 100644 (file)
@@ -4,6 +4,7 @@ namespace MediaWiki\Rest;
 
 use AppendIterator;
 use BagOStuff;
+use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
 use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
 use Wikimedia\ObjectFactory;
 
@@ -40,21 +41,27 @@ class Router {
        /** @var ResponseFactory */
        private $responseFactory;
 
+       /** @var BasicAuthorizerInterface */
+       private $basicAuth;
+
        /**
         * @param string[] $routeFiles List of names of JSON files containing routes
         * @param array $extraRoutes Extension route array
         * @param string $rootPath The base URL path
         * @param BagOStuff $cacheBag A cache in which to store the matcher trees
         * @param ResponseFactory $responseFactory
+        * @param BasicAuthorizerInterface $basicAuth
         */
        public function __construct( $routeFiles, $extraRoutes, $rootPath,
-               BagOStuff $cacheBag, ResponseFactory $responseFactory
+               BagOStuff $cacheBag, ResponseFactory $responseFactory,
+               BasicAuthorizerInterface $basicAuth
        ) {
                $this->routeFiles = $routeFiles;
                $this->extraRoutes = $extraRoutes;
                $this->rootPath = $rootPath;
                $this->cacheBag = $cacheBag;
                $this->responseFactory = $responseFactory;
+               $this->basicAuth = $basicAuth;
        }
 
        /**
@@ -189,7 +196,9 @@ class Router {
         * @return false|string
         */
        private function getRelativePath( $path ) {
-               if ( substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0 ) {
+               if ( strlen( $this->rootPath ) > strlen( $path ) ||
+                       substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0
+               ) {
                        return false;
                }
                return substr( $path, strlen( $this->rootPath ) );
@@ -254,6 +263,10 @@ class Router {
         * @return ResponseInterface
         */
        private function executeHandler( $handler ): ResponseInterface {
+               $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
+               if ( $authResult ) {
+                       return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
+               }
                $response = $handler->execute();
                if ( !( $response instanceof ResponseInterface ) ) {
                        $response = $this->responseFactory->createFromReturnValue( $response );
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 96baf14..7d2b3cb 100644 (file)
@@ -94,17 +94,11 @@ return [
                $config = $services->getMainConfig();
                $context = RequestContext::getMain();
                return new BlockManager(
+                       new ServiceOptions(
+                               BlockManager::$constructorOptions, $services->getMainConfig()
+                       ),
                        $context->getUser(),
-                       $context->getRequest(),
-                       $config->get( 'ApplyIpBlocksToXff' ),
-                       $config->get( 'CookieSetOnAutoblock' ),
-                       $config->get( 'CookieSetOnIpBlock' ),
-                       $config->get( 'DnsBlacklistUrls' ),
-                       $config->get( 'EnableDnsBlacklist' ),
-                       $config->get( 'ProxyList' ),
-                       $config->get( 'ProxyWhitelist' ),
-                       $config->get( 'SecretKey' ),
-                       $config->get( 'SoftBlockRanges' )
+                       $context->getRequest()
                );
        },
 
index 641f1f9..df53c99 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 );
@@ -754,7 +760,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+
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 079f2c8..28bec0b 100644 (file)
@@ -2299,34 +2299,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
@@ -2954,7 +2926,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();
                        }
                }
 
@@ -4295,7 +4267,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 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 beaad43..85ca648 100644 (file)
@@ -143,14 +143,8 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                        }
                }
 
+               // This means stricter restrictions
                if ( $this->fetchContent ) {
-                       $this->addTables( 'text' );
-                       $this->addJoinConds(
-                               [ 'text' => [ 'LEFT JOIN', [ 'ar_text_id=old_id' ] ] ]
-                       );
-                       $this->addFields( [ 'old_text', 'old_flags' ] );
-
-                       // This also means stricter restrictions
                        $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
                }
 
index b266ecf..bbb987f 100644 (file)
@@ -94,14 +94,8 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
                        }
                }
 
+               // This means stricter restrictions
                if ( $this->fetchContent ) {
-                       $this->addTables( 'text' );
-                       $this->addJoinConds(
-                               [ 'text' => [ 'LEFT JOIN', [ 'ar_text_id=old_id' ] ] ]
-                       );
-                       $this->addFields( [ 'old_text', 'old_flags' ] );
-
-                       // This also means stricter restrictions
                        $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
                }
 
index 91d86b9..4fbc157 100644 (file)
@@ -104,27 +104,12 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        $this->dieWithError( 'user and excludeuser cannot be used together', 'badparams' );
                }
 
-               $this->addTables( 'archive' );
-               $this->addFields( [ 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_deleted', 'ar_id' ] );
-
-               $this->addFieldsIf( 'ar_parent_id', $fld_parentid );
-               $this->addFieldsIf( 'ar_rev_id', $fld_revid );
-               if ( $fld_user || $fld_userid ) {
-                       $actorQuery = ActorMigration::newMigration()->getJoin( 'ar_user' );
-                       $this->addTables( $actorQuery['tables'] );
-                       $this->addFields( $actorQuery['fields'] );
-                       $this->addJoinConds( $actorQuery['joins'] );
-               }
-               $this->addFieldsIf( 'ar_minor_edit', $fld_minor );
-               $this->addFieldsIf( 'ar_len', $fld_len );
-               $this->addFieldsIf( 'ar_sha1', $fld_sha1 );
-
-               if ( $fld_comment || $fld_parsedcomment ) {
-                       $commentQuery = $commentStore->getJoin( 'ar_comment' );
-                       $this->addTables( $commentQuery['tables'] );
-                       $this->addFields( $commentQuery['fields'] );
-                       $this->addJoinConds( $commentQuery['joins'] );
-               }
+               $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+               $arQuery = $revisionStore->getArchiveQueryInfo();
+               $this->addTables( $arQuery['tables'] );
+               $this->addFields( $arQuery['fields'] );
+               $this->addJoinConds( $arQuery['joins'] );
+               $this->addFields( [ 'ar_title', 'ar_namespace' ] );
 
                if ( $fld_tags ) {
                        $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
@@ -144,14 +129,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        }
                }
 
+               // This means stricter restrictions
                if ( $fld_content ) {
-                       $this->addTables( 'text' );
-                       $this->addJoinConds(
-                               [ 'text' => [ 'LEFT JOIN', [ 'ar_text_id=old_id' ] ] ]
-                       );
-                       $this->addFields( [ 'ar_text_id', 'old_text', 'old_flags' ] );
-
-                       // This also means stricter restrictions
                        $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
                }
                // Check limits
@@ -365,7 +344,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                        $anyHidden = true;
                                }
                                if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) {
-                                       ApiResult::setContentValue( $rev, 'text', Revision::getRevisionText( $row ) );
+                                       ApiResult::setContentValue( $rev, 'text', Revision::getRevisionText( $row, 'ar_' ) );
                                }
                        }
 
index c58537e..68141a1 100644 (file)
@@ -23,6 +23,7 @@ namespace MediaWiki\Block;
 use DateTime;
 use DeferredUpdates;
 use IP;
+use MediaWiki\Config\ServiceOptions;
 use MediaWiki\User\UserIdentity;
 use MWCryptHash;
 use User;
@@ -44,70 +45,38 @@ class BlockManager {
        /** @var WebRequest */
        private $currentRequest;
 
-       /** @var bool */
-       private $applyIpBlocksToXff;
-
-       /** @var bool */
-       private $cookieSetOnAutoblock;
-
-       /** @var bool */
-       private $cookieSetOnIpBlock;
-
-       /** @var array */
-       private $dnsBlacklistUrls;
-
-       /** @var bool */
-       private $enableDnsBlacklist;
-
-       /** @var array */
-       private $proxyList;
-
-       /** @var array */
-       private $proxyWhitelist;
-
-       /** @var string|bool */
-       private $secretKey;
-
-       /** @var array */
-       private $softBlockRanges;
+       /**
+        * TODO Make this a const when HHVM support is dropped (T192166)
+        *
+        * @var array
+        * @since 1.34
+        * */
+       public static $constructorOptions = [
+               'ApplyIpBlocksToXff',
+               'CookieSetOnAutoblock',
+               'CookieSetOnIpBlock',
+               'DnsBlacklistUrls',
+               'EnableDnsBlacklist',
+               'ProxyList',
+               'ProxyWhitelist',
+               'SecretKey',
+               'SoftBlockRanges',
+       ];
 
        /**
+        * @param ServiceOptions $options
         * @param User $currentUser
         * @param WebRequest $currentRequest
-        * @param bool $applyIpBlocksToXff
-        * @param bool $cookieSetOnAutoblock
-        * @param bool $cookieSetOnIpBlock
-        * @param string[] $dnsBlacklistUrls
-        * @param bool $enableDnsBlacklist
-        * @param string[] $proxyList
-        * @param string[] $proxyWhitelist
-        * @param string $secretKey
-        * @param array $softBlockRanges
         */
        public function __construct(
+               ServiceOptions $options,
                User $currentUser,
-               WebRequest $currentRequest,
-               $applyIpBlocksToXff,
-               $cookieSetOnAutoblock,
-               $cookieSetOnIpBlock,
-               array $dnsBlacklistUrls,
-               $enableDnsBlacklist,
-               array $proxyList,
-               array $proxyWhitelist,
-               $secretKey,
-               $softBlockRanges
+               WebRequest $currentRequest
        ) {
+               $options->assertRequiredOptions( self::$constructorOptions );
+               $this->options = $options;
                $this->currentUser = $currentUser;
                $this->currentRequest = $currentRequest;
-               $this->applyIpBlocksToXff = $applyIpBlocksToXff;
-               $this->cookieSetOnAutoblock = $cookieSetOnAutoblock;
-               $this->cookieSetOnIpBlock = $cookieSetOnIpBlock;
-               $this->dnsBlacklistUrls = $dnsBlacklistUrls;
-               $this->enableDnsBlacklist = $enableDnsBlacklist;
-               $this->proxyList = $proxyList;
-               $this->proxyWhitelist = $proxyWhitelist;
-               $this->secretKey = $secretKey;
-               $this->softBlockRanges = $softBlockRanges;
        }
 
        /**
@@ -157,7 +126,7 @@ class BlockManager {
                }
 
                // Proxy blocking
-               if ( $ip !== null && !in_array( $ip, $this->proxyWhitelist ) ) {
+               if ( $ip !== null && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) ) {
                        // Local list
                        if ( $this->isLocallyBlockedProxy( $ip ) ) {
                                $blocks[] = new SystemBlock( [
@@ -177,9 +146,9 @@ class BlockManager {
                }
 
                // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
-               if ( $this->applyIpBlocksToXff
+               if ( $this->options->get( 'ApplyIpBlocksToXff' )
                        && $ip !== null
-                       && !in_array( $ip, $this->proxyWhitelist )
+                       && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) )
                ) {
                        $xff = $request->getHeader( 'X-Forwarded-For' );
                        $xff = array_map( 'trim', explode( ',', $xff ) );
@@ -192,7 +161,7 @@ class BlockManager {
                // Soft blocking
                if ( $ip !== null
                        && $isAnon
-                       && IP::isInRanges( $ip, $this->softBlockRanges )
+                       && IP::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) )
                ) {
                        $blocks[] = new SystemBlock( [
                                'address' => $ip,
@@ -253,7 +222,8 @@ class BlockManager {
        }
 
        /**
-        * Try to load a block from an ID given in a cookie value.
+        * Try to load a block from an ID given in a cookie value. If the block is invalid
+        * or doesn't exist, remove the cookie.
         *
         * @param UserIdentity $user
         * @param WebRequest $request
@@ -263,43 +233,45 @@ class BlockManager {
                UserIdentity $user,
                WebRequest $request
        ) {
-               $blockCookieVal = $request->getCookie( 'BlockID' );
-               $response = $request->response();
+               $blockCookieId = $this->getIdFromCookieValue( $request->getCookie( 'BlockID' ) );
 
-               // Make sure there's something to check. The cookie value must start with a number.
-               if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
-                       return false;
-               }
-               // Load the block from the ID in the cookie.
-               $blockCookieId = $this->getIdFromCookieValue( $blockCookieVal );
                if ( $blockCookieId !== null ) {
-                       // An ID was found in the cookie.
                        // TODO: remove dependency on DatabaseBlock
-                       $tmpBlock = DatabaseBlock::newFromID( $blockCookieId );
-                       if ( $tmpBlock instanceof DatabaseBlock ) {
-                               switch ( $tmpBlock->getType() ) {
-                                       case DatabaseBlock::TYPE_USER:
-                                               $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking();
-                                               $useBlockCookie = ( $this->cookieSetOnAutoblock === true );
-                                               break;
-                                       case DatabaseBlock::TYPE_IP:
-                                       case DatabaseBlock::TYPE_RANGE:
-                                               // If block is type IP or IP range, load only if user is not logged in (T152462)
-                                               $blockIsValid = !$tmpBlock->isExpired() && $user->getId() === 0;
-                                               $useBlockCookie = ( $this->cookieSetOnIpBlock === true );
-                                               break;
-                                       default:
-                                               $blockIsValid = false;
-                                               $useBlockCookie = false;
-                               }
+                       $block = DatabaseBlock::newFromID( $blockCookieId );
+                       if (
+                               $block instanceof DatabaseBlock &&
+                               $this->shouldApplyCookieBlock( $block, $user->isAnon() )
+                       ) {
+                               return $block;
+                       }
+                       $this->clearBlockCookie( $request->response() );
+               }
 
-                               if ( $blockIsValid && $useBlockCookie ) {
-                                       // Use the block.
-                                       return $tmpBlock;
-                               }
+               return false;
+       }
+
+       /**
+        * Check if the block loaded from the cookie should be applied.
+        *
+        * @param DatabaseBlock $block
+        * @param bool $isAnon The user is logged out
+        * @return bool The block sould be applied
+        */
+       private function shouldApplyCookieBlock( DatabaseBlock $block, $isAnon ) {
+               if ( !$block->isExpired() ) {
+                       switch ( $block->getType() ) {
+                               case DatabaseBlock::TYPE_IP:
+                               case DatabaseBlock::TYPE_RANGE:
+                                       // If block is type IP or IP range, load only
+                                       // if user is not logged in (T152462)
+                                       return $isAnon &&
+                                               $this->options->get( 'CookieSetOnIpBlock' );
+                               case DatabaseBlock::TYPE_USER:
+                                       return $block->isAutoblocking() &&
+                                               $this->options->get( 'CookieSetOnAutoblock' );
+                               default:
+                                       return false;
                        }
-                       // If the block is invalid or doesn't exist, remove the cookie.
-                       $this->clearBlockCookie( $response );
                }
                return false;
        }
@@ -311,20 +283,21 @@ class BlockManager {
         * @return bool
         */
        private function isLocallyBlockedProxy( $ip ) {
-               if ( !$this->proxyList ) {
+               $proxyList = $this->options->get( 'ProxyList' );
+               if ( !$proxyList ) {
                        return false;
                }
 
-               if ( !is_array( $this->proxyList ) ) {
+               if ( !is_array( $proxyList ) ) {
                        // Load values from the specified file
-                       $this->proxyList = array_map( 'trim', file( $this->proxyList ) );
+                       $proxyList = array_map( 'trim', file( $proxyList ) );
                }
 
                $resultProxyList = [];
                $deprecatedIPEntries = [];
 
                // backward compatibility: move all ip addresses in keys to values
-               foreach ( $this->proxyList as $key => $value ) {
+               foreach ( $proxyList as $key => $value ) {
                        $keyIsIP = IP::isIPAddress( $key );
                        $valueIsIP = IP::isIPAddress( $value );
                        if ( $keyIsIP && !$valueIsIP ) {
@@ -357,13 +330,13 @@ class BlockManager {
         * @return bool True if blacklisted.
         */
        public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
-               if ( !$this->enableDnsBlacklist ||
-                       ( $checkWhitelist && in_array( $ip, $this->proxyWhitelist ) )
+               if ( !$this->options->get( 'EnableDnsBlacklist' ) ||
+                       ( $checkWhitelist && in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) )
                ) {
                        return false;
                }
 
-               return $this->inDnsBlacklist( $ip, $this->dnsBlacklistUrls );
+               return $this->inDnsBlacklist( $ip, $this->options->get( 'DnsBlacklistUrls' ) );
        }
 
        /**
@@ -505,9 +478,11 @@ class BlockManager {
                        switch ( $block->getType() ) {
                                case DatabaseBlock::TYPE_IP:
                                case DatabaseBlock::TYPE_RANGE:
-                                       return $isAnon && $this->cookieSetOnIpBlock;
+                                       return $isAnon && $this->options->get( 'CookieSetOnIpBlock' );
                                case DatabaseBlock::TYPE_USER:
-                                       return !$isAnon && $this->cookieSetOnAutoblock && $block->isAutoblocking();
+                                       return !$isAnon &&
+                                               $this->options->get( 'CookieSetOnAutoblock' ) &&
+                                               $block->isAutoblocking();
                                default:
                                        return false;
                        }
@@ -536,15 +511,20 @@ class BlockManager {
         * @return int|null The block ID, or null if the HMAC is present and invalid.
         */
        public function getIdFromCookieValue( $cookieValue ) {
+               // The cookie value must start with a number
+               if ( !is_numeric( substr( $cookieValue, 0, 1 ) ) ) {
+                       return null;
+               }
+
                // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
                $bangPos = strpos( $cookieValue, '!' );
                $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
-               if ( !$this->secretKey ) {
+               if ( !$this->options->get( 'SecretKey' ) ) {
                        // If there's no secret key, just use the ID as given.
                        return $id;
                }
                $storedHmac = substr( $cookieValue, $bangPos + 1 );
-               $calculatedHmac = MWCryptHash::hmac( $id, $this->secretKey, false );
+               $calculatedHmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
                if ( $calculatedHmac === $storedHmac ) {
                        return $id;
                } else {
@@ -565,11 +545,11 @@ class BlockManager {
         */
        public function getCookieValue( DatabaseBlock $block ) {
                $id = $block->getId();
-               if ( !$this->secretKey ) {
+               if ( !$this->options->get( 'SecretKey' ) ) {
                        // If there's no secret key, don't append a HMAC.
                        return $id;
                }
-               $hmac = MWCryptHash::hmac( $id, $this->secretKey, false );
+               $hmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
                $cookieValue = $id . '!' . $hmac;
                return $cookieValue;
        }
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 3b3ba9d..b2d0fc6 100644 (file)
@@ -25,5 +25,5 @@
  *
  * @since 1.23
  */
-class ConfigException extends Exception {
+class ConfigException extends LogicException {
 }
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 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..52cab04 100644 (file)
@@ -49,6 +49,8 @@
        "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-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..36e1902 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": "copiando",
+       "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 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 2140043..b8a5ad2 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;
@@ -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..882ae64 100644 (file)
@@ -99,7 +99,7 @@ class CategoryMembershipChangeJob extends Job {
                }
 
                // 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..74b90ed 100644 (file)
@@ -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 89ecb0e..3179a2f 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,233 @@ 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 links updates by page ID so they see each others' changes
+               // 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.
                $dbw = $lbFactory->getMainLB()->getConnection( 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 +387,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 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 7ebbe8b..c47f6ee 100644 (file)
@@ -258,6 +258,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         */
        public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                if (
+                       is_int( $value ) || // avoid breaking incr()/decr()
                        ( $flags & self::WRITE_ALLOW_SEGMENTS ) != self::WRITE_ALLOW_SEGMENTS ||
                        is_infinite( $this->segmentationSize )
                ) {
@@ -294,6 +295,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
                        $segmentHashes[] = $hash;
                }
 
+               $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity
                $ok = $this->setMulti( $chunksByKey, $exptime, $flags );
                if ( $ok ) {
                        // Only when all segments are stored should the main key be changed
@@ -405,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
@@ -503,6 +505,16 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @since 1.28
         */
        public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+               return $this->doChangeTTL( $key, $exptime, $flags );
+       }
+
+       /**
+        * @param string $key
+        * @param int $exptime
+        * @param int $flags
+        * @return bool
+        */
+       protected function doChangeTTL( $key, $exptime, $flags ) {
                $expiry = $this->convertToExpiry( $exptime );
                $delete = ( $expiry != 0 && $expiry < $this->getCurrentTime() );
 
@@ -653,19 +665,18 @@ 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;
        }
 
@@ -673,7 +684,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * 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
+        * @return array Map of (key => value) for existing keys
         */
        public function getMulti( array $keys, $flags = 0 ) {
                $valuesBykey = $this->doGetMulti( $keys, $flags );
@@ -689,7 +700,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * 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
+        * @return array Map of (key => value) for existing keys
         */
        protected function doGetMulti( array $keys, $flags = 0 ) {
                $res = [];
@@ -718,12 +729,20 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
                if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
                        throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
                }
+               return $this->doSetMulti( $data, $exptime, $flags );
+       }
 
+       /**
+        * @param mixed[] $data Map of (key => value)
+        * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+        * @return bool Success
+        */
+       protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
                $res = true;
                foreach ( $data as $key => $value ) {
                        $res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
                }
-
                return $res;
        }
 
@@ -738,10 +757,41 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * @since 1.33
         */
        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 );
+       }
+
+       /**
+        * @param string[] $keys List of keys
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+        * @return bool Success
+        */
+       protected function doDeleteMulti( array $keys, $flags = 0 ) {
                $res = true;
                foreach ( $keys as $key ) {
                        $res = $this->doDelete( $key, $flags ) && $res;
                }
+               return $res;
+       }
+
+       /**
+        * Change the expiration of multiple keys that exist
+        *
+        * @see BagOStuff::changeTTL()
+        *
+        * @param string[] $keys List of keys
+        * @param int $exptime TTL or UNIX timestamp
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
+        * @return bool Success
+        * @since 1.34
+        */
+       public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+               $res = true;
+               foreach ( $keys as $key ) {
+                       $res = $this->doChangeTTL( $key, $exptime, $flags ) && $res;
+               }
 
                return $res;
        }
@@ -798,7 +848,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} );
                }
@@ -874,7 +924,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;
        }
 
@@ -883,9 +933,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 ] );
                }
        }
 
@@ -893,21 +941,26 @@ 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 ) );
        }
 
        /**
-        * Convert an optionally relative time to an absolute time
-        * @param int $exptime
+        * Convert an optionally relative timestamp to an absolute time
+        *
+        * The input value will be cast to an integer and interpreted as follows:
+        *   - zero: no expiry; return zero (e.g. TTL_INDEFINITE)
+        *   - negative: relative TTL; return UNIX timestamp offset by this value
+        *   - positive (< 10 years): relative TTL; return UNIX timestamp offset by this value
+        *   - positive (>= 10 years): absolute UNIX timestamp; return this value
+        *
+        * @param int $exptime Absolute TTL or 0 for indefinite
         * @return int
         */
-       protected function convertToExpiry( $exptime ) {
-               if ( $this->expiryIsRelative( $exptime ) ) {
-                       return (int)$this->getCurrentTime() + $exptime;
-               } else {
-                       return $exptime;
-               }
+       final protected function convertToExpiry( $exptime ) {
+               return $this->expiryIsRelative( $exptime )
+                       ? (int)$this->getCurrentTime() + $exptime
+                       : $exptime;
        }
 
        /**
@@ -917,16 +970,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 );
        }
 
        /**
@@ -935,7 +982,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 ) ) {
@@ -1018,7 +1065,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 43cebd3..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 setMulti( 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 deleteMulti( array $keys, $flags = 0 ) {
+       protected function doDeleteMulti( array $keys, $flags = 0 ) {
                $this->debug( 'deleteMulti(' . implode( ', ', $keys ) . ')' );
                foreach ( $keys as $key ) {
                        $this->validateKeyEncoding( $key );
@@ -284,7 +295,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $this->checkResult( false, $ok );
        }
 
-       public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+       protected function doChangeTTL( $key, $exptime, $flags ) {
                $this->debug( "touch($key)" );
                $result = $this->client->touch( $key, $exptime );
                return $this->checkResult( $key, $result );
index ea73cba..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 ) {
@@ -101,14 +104,14 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
                return ( $n !== false && $n !== null ) ? $n : false;
        }
 
-       public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+       protected function doChangeTTL( $key, $exptime, $flags ) {
                return $this->client->touch(
                        $this->validateKeyEncoding( $key ),
                        $this->fixExpiry( $exptime )
                );
        }
 
-       public function doGetMulti( array $keys, $flags = 0 ) {
+       protected function doGetMulti( array $keys, $flags = 0 ) {
                foreach ( $keys as $key ) {
                        $this->validateKeyEncoding( $key );
                }
index 4c6750f..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;
                        }
                }
@@ -254,6 +254,15 @@ class MultiWriteBagOStuff extends BagOStuff {
                );
        }
 
+       public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+               return $this->doWrite(
+                       $this->cacheIndexes,
+                       $this->usesAsyncWritesGivenFlags( $flags ),
+                       __FUNCTION__,
+                       func_get_args()
+               );
+       }
+
        public function incr( $key, $value = 1 ) {
                return $this->doWrite(
                        $this->cacheIndexes,
@@ -362,15 +371,27 @@ class MultiWriteBagOStuff extends BagOStuff {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
 
+       protected function doChangeTTL( $key, $exptime, $flags ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
        protected function doGetMulti( array $keys, $flags = 0 ) {
                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..833c045
--- /dev/null
@@ -0,0 +1,108 @@
+# 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}.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 743b9eb..dd859ad 100644 (file)
@@ -106,15 +106,15 @@ 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 ) );
@@ -146,7 +146,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 +185,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function setMulti( array $data, $expiry = 0, $flags = 0 ) {
+       protected function doSetMulti( array $data, $expiry = 0, $flags = 0 ) {
                $batches = [];
                $conns = [];
                foreach ( $data as $key => $value ) {
@@ -229,7 +229,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function deleteMulti( array $keys, $flags = 0 ) {
+       protected function doDeleteMulti( array $keys, $flags = 0 ) {
                $batches = [];
                $conns = [];
                foreach ( $keys as $key ) {
@@ -325,7 +325,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+       protected function doChangeTTL( $key, $exptime, $flags ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
index 8502ce2..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 ) {
@@ -134,6 +130,10 @@ class ReplicatedBagOStuff extends BagOStuff {
                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 );
        }
@@ -181,10 +181,22 @@ class ReplicatedBagOStuff extends BagOStuff {
                throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
        }
 
+       protected function doChangeTTL( $key, $exptime, $flags ) {
+               throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+       }
+
        protected function doGetMulti( array $keys, $flags = 0 ) {
                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..45caa78 100644 (file)
@@ -188,31 +188,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 +231,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
 
        /**
@@ -256,7 +262,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                $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->setLogger( $params['logger'] ?? new NullLogger() );
                $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
@@ -314,11 +320,12 @@ 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.
         *
@@ -339,7 +346,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,7 +360,7 @@ 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).
@@ -420,9 +428,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 +438,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;
@@ -520,8 +527,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 +538,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 +638,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 ) {
@@ -812,7 +831,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 +1235,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.