Merge "Fix a typo (folow -> follow)"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 14 Sep 2019 21:09:06 +0000 (21:09 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 14 Sep 2019 21:09:06 +0000 (21:09 +0000)
75 files changed:
.phan/config.php
CODE_OF_CONDUCT.md
RELEASE-NOTES-1.34
docs/hooks.txt
includes/OutputPage.php
includes/ProtectionForm.php
includes/ServiceWiring.php
includes/Status.php
includes/Storage/SqlBlobStore.php
includes/StreamFile.php
includes/actions/HistoryAction.php
includes/actions/RawAction.php
includes/api/ApiComparePages.php
includes/api/ApiEditPage.php
includes/api/ApiFormatXmlRsd.php
includes/api/ApiHelpParamValueMessage.php
includes/api/ApiImageRotate.php
includes/api/ApiQueryBacklinks.php
includes/api/ApiSetNotificationTimestamp.php
includes/block/BlockManager.php
includes/changes/CategoryMembershipChange.php
includes/changes/RCCacheEntryFactory.php
includes/clientpool/SquidPurgeClient.php
includes/collation/CustomUppercaseCollation.php
includes/debug/logger/ConsoleLogger.php
includes/deferred/CdnCacheUpdate.php
includes/deferred/LinksUpdate.php
includes/diff/DifferenceEngine.php
includes/export/DumpMultiWriter.php
includes/filerepo/FileRepo.php
includes/filerepo/file/ArchivedFile.php
includes/filerepo/file/LocalFileMoveBatch.php
includes/gallery/ImageGalleryBase.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLSelectAndOtherField.php
includes/http/MWCallbackStream.php
includes/import/ImportStreamSource.php
includes/import/ImportStringSource.php
includes/installer/InstallDocFormatter.php
includes/installer/LocalSettingsGenerator.php
includes/libs/MemoizedCallable.php
includes/libs/composer/ComposerInstalled.php
includes/libs/composer/ComposerJson.php
includes/libs/composer/ComposerLock.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/filebackend/fileophandle/SwiftFileOpHandle.php
includes/libs/lockmanager/QuorumLockManager.php
includes/libs/mime/XmlTypeCheck.php
includes/libs/objectcache/MultiWriteBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/logging/DeleteLogFormatter.php
includes/logging/LogPager.php
includes/media/DjVuHandler.php
includes/media/DjVuImage.php
includes/page/Article.php
includes/page/PageArchive.php
includes/page/WikiFilePage.php
includes/page/WikiPage.php
includes/resourceloader/ResourceLoaderImage.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialTags.php
includes/specials/forms/PreferencesFormOOUI.php
includes/title/MediaWikiTitleCodec.php
includes/user/User.php
includes/utils/AvroValidator.php
languages/data/Names.php
maintenance/Doxyfile
maintenance/findDeprecated.php
maintenance/mwdocgen.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/block/BlockManagerTest.php
tests/phpunit/includes/deferred/LinksUpdateTest.php
tests/phpunit/includes/filebackend/FileBackendTest.php
tests/phpunit/includes/filebackend/SwiftFileBackendTest.php

index fc775fe..b1ca2a7 100644 (file)
@@ -104,8 +104,6 @@ $cfg['suppress_issue_types'] = array_merge( $cfg['suppress_issue_types'], [
 
        // approximate error count: 45
        "PhanTypeMismatchArgument",
-       // approximate error count: 693
-       "PhanUndeclaredProperty",
 ] );
 
 // This helps a lot in discovering bad code, but unfortunately it will always fail for
index 498acf7..3d9f26a 100644 (file)
@@ -1 +1,4 @@
+Code of Conduct
+===============
+
 The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Special:MyLanguage/Code_of_Conduct).
index 07f52d0..2da3b55 100644 (file)
@@ -429,6 +429,7 @@ because of Phabricator reports.
   * Revision::selectTextFields()
   * Revision::selectPageFields()
   * Revision::selectUserFields()
+* User::setNewpassword(), deprecated in 1.27 has been removed.
 
 === Deprecations in 1.34 ===
 * The MWNamespace class is deprecated. Use NamespaceInfo.
@@ -564,6 +565,9 @@ because of Phabricator reports.
 * The GetBlockedStatus hook is deprecated. Use GetUserBlock instead, to add or
   remove a block.
 * $wgContentHandlerUseDB is deprecated and should always be true.
+* StreamFile::send404Message() and StreamFile::parseRange() are now deprecated.
+  Use HTTPFileStreamer::send404Message() and HTTPFileStreamer::parseRange()
+  respectively instead.
 
 === Other changes in 1.34 ===
 * …
index 43bfd8d..55ba06e 100644 (file)
@@ -2921,7 +2921,7 @@ result augmentors.
 Note that lists should be in the format name => object and the names in both
   lists should be distinct.
 
-'SecondaryDataUpdates': DEPRECATED! Use RevisionDataUpdates or override
+'SecondaryDataUpdates': DEPRECATED since 1.32! Use RevisionDataUpdates or override
 ContentHandler::getSecondaryDataUpdates instead.
 Allows modification of the list of DataUpdates to perform when page content is modified.
 $title: Title of the page that is being edited.
@@ -3969,7 +3969,7 @@ dumps. One, and only one hook should set this, and return false.
 &$opts: Options to use for the query
 &$join: Join conditions
 
-'WikiPageDeletionUpdates': DEPRECATED! Use PageDeletionDataUpdates or
+'WikiPageDeletionUpdates': DEPRECATED since 1.32! Use PageDeletionDataUpdates or
 override ContentHandler::getDeletionDataUpdates instead.
 Manipulates the list of DeferrableUpdates to be applied when a page is deleted.
 $page: the WikiPage
index 9dbbf13..79b343e 100644 (file)
@@ -1598,6 +1598,7 @@ class OutputPage extends ContextSource {
         * @param ParserOptions|null $options Either the ParserOption to use or null to only get the
         *   current ParserOption object. This parameter is deprecated since 1.31.
         * @return ParserOptions
+        * @suppress PhanUndeclaredProperty For isBogus
         */
        public function parserOptions( $options = null ) {
                if ( $options !== null ) {
index 8b5d995..a1be271 100644 (file)
@@ -58,6 +58,18 @@ class ProtectionForm {
        /** @var array Map of action to the expiry time of the existing protection */
        protected $mExistingExpiry = [];
 
+       /** @var Article */
+       protected $mArticle;
+
+       /** @var Title */
+       protected $mTitle;
+
+       /** @var bool */
+       protected $disabled;
+
+       /** @var array */
+       protected $disabledAttrib;
+
        /** @var IContextSource */
        private $mContext;
 
@@ -78,7 +90,7 @@ class ProtectionForm {
                if ( wfReadOnly() ) {
                        $this->mPermErrors[] = [ 'readonlytext', wfReadOnlyReason() ];
                }
-               $this->disabled = $this->mPermErrors != [];
+               $this->disabled = $this->mPermErrors !== [];
                $this->disabledAttrib = $this->disabled
                        ? [ 'disabled' => 'disabled' ]
                        : [];
index 689a98e..eb26776 100644 (file)
@@ -111,7 +111,8 @@ return [
                        new ServiceOptions(
                                BlockManager::$constructorOptions, $services->getMainConfig()
                        ),
-                       $services->getPermissionManager()
+                       $services->getPermissionManager(),
+                       LoggerFactory::getInstance( 'BlockManager' )
                );
        },
 
index 76b905e..932fd2a 100644 (file)
@@ -115,6 +115,7 @@ class Status extends StatusValue {
         * ]
         *
         * @return Status[]
+        * @suppress PhanUndeclaredProperty Status vs StatusValue
         */
        public function splitByErrorType() {
                list( $errorsOnlyStatus, $warningsOnlyStatus ) = parent::splitByErrorType();
index 8c011df..bcbc9e8 100644 (file)
@@ -103,10 +103,10 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
         * @param ExternalStoreAccess $extStoreAccess Access layer for external storage
         * @param WANObjectCache $cache A cache manager for caching blobs. This can be the local
         *        wiki's default instance even if $dbDomain refers to a different wiki, since
-        *        makeGlobalKey() is used to constructed a key that allows cached blobs from the
-        *        same database to be re-used between wikis. For example, enwiki and frwiki will
-        *        use the same cache keys for blobs from the wikidatawiki database, regardless of
-        *        the cache's default key space.
+        *        makeGlobalKey() is used to construct a key that allows cached blobs from the
+        *        same database to be re-used between wikis. For example, wiki A and wiki B will
+        *        use the same cache keys for blobs fetched from wiki C, regardless of the
+        *        wiki-specific default key space.
         * @param bool|string $dbDomain The ID of the target wiki database. Use false for the local wiki.
         */
        public function __construct(
@@ -449,16 +449,15 @@ class SqlBlobStore implements IDBAccessObject, BlobStore {
         * Get a cache key for a given Blob address.
         *
         * The cache key is constructed in a way that allows cached blobs from the same database
-        * to be re-used between wikis. For example, enwiki and frwiki will use the same cache keys
-        * for blobs from the wikidatawiki database.
+        * to be re-used between wikis. For example, wiki A and wiki B will use the same cache keys
+        * for blobs fetched from wiki C.
         *
         * @param string $blobAddress
         * @return string
         */
        private function getCacheKey( $blobAddress ) {
                return $this->cache->makeGlobalKey(
-                       'BlobStore',
-                       'address',
+                       'SqlBlobStore-blob',
                        $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ),
                        $blobAddress
                );
index 2afd651..c9b2c33 100644 (file)
@@ -68,8 +68,10 @@ class StreamFile {
         * @param string $fname Full name and path of the file to stream
         * @param int $flags Bitfield of STREAM_* constants
         * @since 1.24
+        * @deprecated since 1.34, use HTTPFileStreamer::send404Message() instead
         */
        public static function send404Message( $fname, $flags = 0 ) {
+               wfDeprecated( __METHOD__, '1.34' );
                HTTPFileStreamer::send404Message( $fname, $flags );
        }
 
@@ -80,8 +82,10 @@ class StreamFile {
         * @param int $size File size
         * @return array|string Returns error string on failure (start, end, length)
         * @since 1.24
+        * @deprecated since 1.34, use HTTPFileStreamer::parseRange() instead
         */
        public static function parseRange( $range, $size ) {
+               wfDeprecated( __METHOD__, '1.34' );
                return HTTPFileStreamer::parseRange( $range, $size );
        }
 
index db874f2..6a61045 100644 (file)
@@ -433,27 +433,30 @@ class HistoryAction extends FormlessAction {
         * @return FeedItem
         */
        function feedItem( $row ) {
-               $rev = new Revision( $row, 0, $this->getTitle() );
-
+               $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+               $rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() );
+               $prevRev = $revisionStore->getPreviousRevision( $rev );
+               $revComment = $rev->getComment() === null ? null : $rev->getComment()->text;
                $text = FeedUtils::formatDiffRow(
                        $this->getTitle(),
-                       $this->getTitle()->getPreviousRevisionID( $rev->getId() ),
+                       $prevRev ? $prevRev->getId() : false,
                        $rev->getId(),
                        $rev->getTimestamp(),
-                       $rev->getComment()
+                       $revComment
                );
-               if ( $rev->getComment() == '' ) {
+               $revUserText = $rev->getUser() ? $rev->getUser()->getName() : '';
+               if ( $revComment == '' ) {
                        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
                        $title = $this->msg( 'history-feed-item-nocomment',
-                               $rev->getUserText(),
+                               $revUserText,
                                $contLang->timeanddate( $rev->getTimestamp() ),
                                $contLang->date( $rev->getTimestamp() ),
                                $contLang->time( $rev->getTimestamp() )
                        )->inContentLanguage()->text();
                } else {
-                       $title = $rev->getUserText() .
+                       $title = $revUserText .
                                $this->msg( 'colon-separator' )->inContentLanguage()->text() .
-                               FeedItem::stripComment( $rev->getComment() );
+                               FeedItem::stripComment( $revComment );
                }
 
                return new FeedItem(
@@ -461,7 +464,7 @@ class HistoryAction extends FormlessAction {
                        $text,
                        $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
                        $rev->getTimestamp(),
-                       $rev->getUserText(),
+                       $revUserText,
                        $this->getTitle()->getTalkPage()->getFullURL()
                );
        }
index 8fd4e0a..0586e09 100644 (file)
@@ -238,23 +238,31 @@ class RawAction extends FormlessAction {
         */
        public function getOldId() {
                $oldid = $this->getRequest()->getInt( 'oldid' );
+               $rl = MediaWikiServices::getInstance()->getRevisionLookup();
                switch ( $this->getRequest()->getText( 'direction' ) ) {
                        case 'next':
                                # output next revision, or nothing if there isn't one
-                               $nextid = 0;
+                               $nextRev = null;
                                if ( $oldid ) {
-                                       $nextid = $this->getTitle()->getNextRevisionID( $oldid );
+                                       $oldRev = $rl->getRevisionById( $oldid );
+                                       if ( $oldRev ) {
+                                               $nextRev = $rl->getNextRevision( $oldRev );
+                                       }
                                }
-                               $oldid = $nextid ?: -1;
+                               $oldid = $nextRev ? $nextRev->getId() : -1;
                                break;
                        case 'prev':
                                # output previous revision, or nothing if there isn't one
+                               $prevRev = null;
                                if ( !$oldid ) {
                                        # get the current revision so we can get the penultimate one
                                        $oldid = $this->page->getLatest();
                                }
-                               $previd = $this->getTitle()->getPreviousRevisionID( $oldid );
-                               $oldid = $previd ?: -1;
+                               $oldRev = $rl->getRevisionById( $oldid );
+                               if ( $oldRev ) {
+                                       $prevRev = $rl->getPreviousRevision( $oldRev );
+                               }
+                               $oldid = $prevRev ? $prevRev->getId() : -1;
                                break;
                        case 'cur':
                                $oldid = 0;
index 05eb438..6e788d5 100644 (file)
@@ -26,6 +26,9 @@ use MediaWiki\Revision\RevisionArchiveRecord;
 use MediaWiki\Revision\RevisionStore;
 use MediaWiki\Revision\SlotRecord;
 
+/**
+ * @ingroup API
+ */
 class ApiComparePages extends ApiBase {
 
        /** @var RevisionStore */
@@ -249,6 +252,7 @@ class ApiComparePages extends ApiBase {
                        );
                        if ( $row ) {
                                $rev = $this->revisionStore->newRevisionFromArchiveRow( $row );
+                               // @phan-suppress-next-line PhanUndeclaredProperty
                                $rev->isArchive = true;
                        }
                }
@@ -617,6 +621,7 @@ class ApiComparePages extends ApiBase {
                                }
                        }
 
+                       // @phan-suppress-next-line PhanUndeclaredProperty
                        if ( !empty( $rev->isArchive ) ) {
                                $this->getMain()->setCacheMode( 'private' );
                                $vals["{$prefix}archive"] = true;
index fdf9cf1..06f6f60 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Storage\RevisionRecord;
 
 /**
@@ -239,11 +240,15 @@ class ApiEditPage extends ApiBase {
                        $params['text'] = $newContent->serialize( $contentFormat );
                        // If no summary was given and we only undid one rev,
                        // use an autosummary
-                       if ( is_null( $params['summary'] ) &&
-                               $titleObj->getNextRevisionID( $undoafterRev->getId() ) == $params['undo']
-                       ) {
-                               $params['summary'] = wfMessage( 'undo-summary' )
-                                       ->params( $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text();
+
+                       if ( is_null( $params['summary'] ) ) {
+                               $nextRev = MediaWikiServices::getInstance()->getRevisionLookup()
+                                       ->getNextRevision( $undoafterRev->getRevisionRecord() );
+                               if ( $nextRev && $nextRev->getId() == $params['undo'] ) {
+                                       $params['summary'] = wfMessage( 'undo-summary' )
+                                               ->params( $params['undo'], $undoRev->getUserText() )
+                                               ->inContentLanguage()->text();
+                               }
                        }
                }
 
index 6b892fa..3052b89 100644 (file)
@@ -21,6 +21,9 @@
  * @file
  */
 
+/**
+ * @ingroup API
+ */
 class ApiFormatXmlRsd extends ApiFormatXml {
        public function __construct( ApiMain $main, $format ) {
                parent::__construct( $main, $format );
index 7aebf90..0272dcd 100644 (file)
@@ -28,6 +28,7 @@
  * 'APIGetParamDescriptionMessages' hook simple.
  *
  * @since 1.25
+ * @ingroup API
  */
 class ApiHelpParamValueMessage extends Message {
 
index 1f8b012..8a0e8c9 100644 (file)
@@ -1,7 +1,4 @@
 <?php
-
-use MediaWiki\MediaWikiServices;
-
 /**
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,6 +18,11 @@ use MediaWiki\MediaWikiServices;
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @ingroup API
+ */
 class ApiImageRotate extends ApiBase {
        private $mPageSet = null;
 
index d21f111..a6b15e9 100644 (file)
@@ -41,6 +41,9 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
        private $redirect;
        private $bl_ns, $bl_from, $bl_from_ns, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS;
 
+       /** @var string */
+       private $helpUrl;
+
        /**
         * Maps ns and title to pageid
         *
index 7e9f56d..3f6f14c 100644 (file)
@@ -93,13 +93,14 @@ class ApiSetNotificationTimestamp extends ApiBase {
                        $titles = $pageSet->getGoodTitles();
                        $title = reset( $titles );
                        if ( $title ) {
-                               $revid = $title->getNextRevisionID( $params['newerthanrevid'], Title::READ_LATEST );
-                               if ( $revid ) {
-                                       $timestamp = $dbw->timestamp(
-                                               MediaWikiServices::getInstance()->getRevisionStore()->getTimestampFromId( $title, $revid )
-                                       );
-                               } else {
-                                       $timestamp = null;
+                               $timestamp = null;
+                               $rl = MediaWikiServices::getInstance()->getRevisionLookup();
+                               $currRev = $rl->getRevisionById( $params['newerthanrevid'], Title::READ_LATEST );
+                               if ( $currRev ) {
+                                       $nextRev = $rl->getNextRevision( $currRev, Title::READ_LATEST );
+                                       if ( $nextRev ) {
+                                               $timestamp = $dbw->timestamp( $nextRev->getTimestamp() );
+                                       }
                                }
                        }
                }
index 2e20529..4c2f27e 100644 (file)
@@ -29,6 +29,7 @@ use MediaWiki\Config\ServiceOptions;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\User\UserIdentity;
 use MWCryptHash;
+use Psr\Log\LoggerInterface;
 use User;
 use WebRequest;
 use WebResponse;
@@ -41,6 +42,12 @@ use Wikimedia\IPSet;
  * @since 1.34 Refactored from User and Block.
  */
 class BlockManager {
+       /** @var PermissionManager */
+       private $permissionManager;
+
+       /** @var ServiceOptions */
+       private $options;
+
        /**
         * TODO Make this a const when HHVM support is dropped (T192166)
         *
@@ -59,17 +66,23 @@ class BlockManager {
                'SoftBlockRanges',
        ];
 
+       /** @var LoggerInterface */
+       private $logger;
+
        /**
         * @param ServiceOptions $options
         * @param PermissionManager $permissionManager
+        * @param LoggerInterface $logger
         */
        public function __construct(
                ServiceOptions $options,
-               PermissionManager $permissionManager
+               PermissionManager $permissionManager,
+               LoggerInterface $logger
        ) {
                $options->assertRequiredOptions( self::$constructorOptions );
                $this->options = $options;
                $this->permissionManager = $permissionManager;
+               $this->logger = $logger;
        }
 
        /**
@@ -392,15 +405,14 @@ class BlockManager {
                                $ipList = $this->checkHost( $hostname );
 
                                if ( $ipList ) {
-                                       wfDebugLog(
-                                               'dnsblacklist',
+                                       $this->logger->info(
                                                "Hostname $hostname is {$ipList[0]}, it's a proxy says $basename!"
                                        );
                                        $found = true;
                                        break;
                                }
 
-                               wfDebugLog( 'dnsblacklist', "Requested $hostname, not found in $basename." );
+                               $this->logger->debug( "Requested $hostname, not found in $basename." );
                        }
                }
 
index 2ef9c9f..14e391d 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Storage\RevisionRecord;
 
 /**
@@ -273,11 +274,15 @@ class CategoryMembershipChange {
         * @return null|string
         */
        private function getPreviousRevisionTimestamp() {
-               $previousRev = Revision::newFromId(
-                               $this->pageTitle->getPreviousRevisionID( $this->pageTitle->getLatestRevID() )
-                       );
-
-               return $previousRev ? $previousRev->getTimestamp() : null;
+               $rl = MediaWikiServices::getInstance()->getRevisionLookup();
+               $latestRev = $rl->getRevisionByTitle( $this->pageTitle );
+               if ( $latestRev ) {
+                       $previousRev = $rl->getPreviousRevision( $latestRev );
+                       if ( $previousRev ) {
+                               return $previousRev->getTimestamp();
+                       }
+               }
+               return null;
        }
 
 }
index d448eae..e718f6b 100644 (file)
@@ -56,7 +56,6 @@ class RCCacheEntryFactory {
         */
        public function newFromRecentChange( RecentChange $baseRC, $watched ) {
                $user = $this->context->getUser();
-               $counter = $baseRC->counter;
 
                $cacheEntry = RCCacheEntry::newFromParent( $baseRC );
 
@@ -73,8 +72,8 @@ class RCCacheEntryFactory {
                // called too many times (50% of CPU time on RecentChanges!).
                $showDiffLinks = $this->showDiffLinks( $cacheEntry, $user );
 
-               $cacheEntry->difflink = $this->buildDiffLink( $cacheEntry, $showDiffLinks, $counter );
-               $cacheEntry->curlink = $this->buildCurLink( $cacheEntry, $showDiffLinks, $counter );
+               $cacheEntry->difflink = $this->buildDiffLink( $cacheEntry, $showDiffLinks );
+               $cacheEntry->curlink = $this->buildCurLink( $cacheEntry, $showDiffLinks );
                $cacheEntry->lastlink = $this->buildLastLink( $cacheEntry, $showDiffLinks );
 
                // Make user links
@@ -109,11 +108,11 @@ class RCCacheEntryFactory {
        }
 
        /**
-        * @param RecentChange $cacheEntry
+        * @param RCCacheEntry $cacheEntry
         *
         * @return string
         */
-       private function buildCLink( RecentChange $cacheEntry ) {
+       private function buildCLink( RCCacheEntry $cacheEntry ) {
                $type = $cacheEntry->mAttribs['rc_type'];
 
                // New unpatrolled pages
@@ -182,11 +181,10 @@ class RCCacheEntryFactory {
        /**
         * @param RecentChange $cacheEntry
         * @param bool $showDiffLinks
-        * @param int $counter
         *
         * @return string
         */
-       private function buildCurLink( RecentChange $cacheEntry, $showDiffLinks, $counter ) {
+       private function buildCurLink( RecentChange $cacheEntry, $showDiffLinks ) {
                $queryParams = $this->buildCurQueryParams( $cacheEntry );
                $curMessage = $this->getMessage( 'cur' );
                $logTypes = [ RC_LOG ];
@@ -217,11 +215,10 @@ class RCCacheEntryFactory {
        /**
         * @param RecentChange $cacheEntry
         * @param bool $showDiffLinks
-        * @param int $counter
         *
         * @return string
         */
-       private function buildDiffLink( RecentChange $cacheEntry, $showDiffLinks, $counter ) {
+       private function buildDiffLink( RecentChange $cacheEntry, $showDiffLinks ) {
                $queryParams = $this->buildDiffQueryParams( $cacheEntry );
                $diffMessage = $this->getMessage( 'diff' );
                $logTypes = [ RC_NEW, RC_LOG ];
index 6b5482c..dffe6e1 100644 (file)
@@ -191,11 +191,11 @@ class SquidPurgeClient {
        /**
         * Queue a purge operation
         *
-        * @param string $url
+        * @param string $url Fully expanded URL (with host and protocol)
         */
        public function queuePurge( $url ) {
                global $wgSquidPurgeUseHostHeader;
-               $url = CdnCacheUpdate::expand( str_replace( "\n", '', $url ) );
+               $url = str_replace( "\n", '', $url ); // sanity
                $request = [];
                if ( $wgSquidPurgeUseHostHeader ) {
                        $url = wfParseUrl( $url );
index 170d5c2..8f4f058 100644 (file)
@@ -45,6 +45,9 @@ class CustomUppercaseCollation extends NumericUppercaseCollation {
        /** @var array $puaSubset List of private use area codes */
        private $puaSubset;
 
+       /** @var array */
+       private $firstLetters;
+
        /**
         * @note This assumes $alphabet does not contain U+F3000-U+F3FFF
         *
index a48faf1..56fc0b3 100644 (file)
@@ -10,6 +10,9 @@ use Psr\Log\AbstractLogger;
  * goal.
  */
 class ConsoleLogger extends AbstractLogger {
+       /** @var string */
+       private $channel;
+
        /**
         * @param string $channel
         */
index 7044748..b983e97 100644 (file)
@@ -137,7 +137,7 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate {
                                foreach ( $chunks as $chunk ) {
                                        $client = new SquidPurgeClient( $server );
                                        foreach ( $chunk as $url ) {
-                                               $client->queuePurge( $url );
+                                               $client->queuePurge( self::expand( $url ) );
                                        }
                                        $pool->addClient( $client );
                                }
@@ -254,7 +254,7 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate {
         * @param string $url
         * @return string
         */
-       public static function expand( $url ) {
+       private static function expand( $url ) {
                return wfExpandUrl( $url, PROTO_INTERNAL );
        }
 
index c4e8341..2bfdc0e 100644 (file)
@@ -1197,4 +1197,14 @@ class LinksUpdate extends DataUpdate {
 
                return $this->db;
        }
+
+       /**
+        * Whether or not this LinksUpdate will also update pages which transclude the
+        * current page or otherwise depend on it.
+        *
+        * @return bool
+        */
+       public function isRecursive() {
+               return $this->mRecursive;
+       }
 }
index 8a5caa2..7e4e53e 100644 (file)
@@ -1713,14 +1713,29 @@ class DifferenceEngine extends ContextSource {
         *     false signifies that there is no previous/next revision ($old is the oldest/newest one).
         */
        public function mapDiffPrevNext( $old, $new ) {
+               $rl = MediaWikiServices::getInstance()->getRevisionLookup();
                if ( $new === 'prev' ) {
                        // Show diff between revision $old and the previous one. Get previous one from DB.
                        $newid = intval( $old );
-                       $oldid = $this->getTitle()->getPreviousRevisionID( $newid );
+                       $oldid = false;
+                       $newRev = $rl->getRevisionById( $newid );
+                       if ( $newRev ) {
+                               $oldRev = $rl->getPreviousRevision( $newRev );
+                               if ( $oldRev ) {
+                                       $oldid = $oldRev->getId();
+                               }
+                       }
                } elseif ( $new === 'next' ) {
                        // Show diff between revision $old and the next one. Get next one from DB.
                        $oldid = intval( $old );
-                       $newid = $this->getTitle()->getNextRevisionID( $oldid );
+                       $newid = false;
+                       $oldRev = $rl->getRevisionById( $oldid );
+                       if ( $oldRev ) {
+                               $newRev = $rl->getNextRevision( $oldRev );
+                               if ( $newRev ) {
+                                       $newid = $newRev->getId();
+                               }
+                       }
                } else {
                        $oldid = intval( $old );
                        $newid = intval( $new );
index 92118fe..2f5b3dc 100644 (file)
  * @ingroup Dump
  */
 class DumpMultiWriter {
+       /** @var array */
+       private $sinks;
+       /** @var int */
+       private $count;
 
        /**
         * @param array $sinks
index 42e78ff..ff8f056 100644 (file)
@@ -142,6 +142,12 @@ class FileRepo {
        /** @var WANObjectCache */
        protected $wanCache;
 
+       /**
+        * @var string
+        * @protected Use $this->getName(). Public for back-compat only
+        */
+       public $name;
+
        /**
         * @param array|null $info
         * @throws MWException
index 17fa146..b4fd176 100644 (file)
@@ -94,6 +94,9 @@ class ArchivedFile {
        /** @var Title */
        protected $title; # image title
 
+       /** @var bool */
+       private $exists;
+
        /**
         * @throws MWException
         * @param Title $title
index 21980b9..0cdc2d5 100644 (file)
@@ -46,6 +46,24 @@ class LocalFileMoveBatch {
        /** @var IDatabase */
        protected $db;
 
+       /** @var string */
+       protected $oldHash;
+
+       /** @var string */
+       protected $newHash;
+
+       /** @var string */
+       protected $oldName;
+
+       /** @var string */
+       protected $newName;
+
+       /** @var string */
+       protected $oldRel;
+
+       /** @var string */
+       protected $newRel;
+
        /**
         * @param File $file
         * @param Title $target
index c6d8ddf..4781a48 100644 (file)
@@ -88,6 +88,15 @@ abstract class ImageGalleryBase extends ContextSource {
        /** @var array */
        protected $mAttribs = [];
 
+       /** @var int */
+       protected $mPerRow;
+
+       /** @var int */
+       protected $mWidths;
+
+       /** @var int */
+       protected $mHeights;
+
        /** @var array */
        private static $modeMapping;
 
index 048abbb..b55b652 100644 (file)
@@ -657,6 +657,7 @@ abstract class HTMLFormField {
         * @param OOUI\Widget $inputField
         * @param array $config
         * @return OOUI\FieldLayout|OOUI\ActionFieldLayout
+        * @suppress PhanUndeclaredProperty Only some subclasses declare mClassWithButton
         */
        protected function getFieldLayoutOOUI( $inputField, $config ) {
                if ( isset( $this->mClassWithButton ) ) {
index 354432b..05ab0bb 100644 (file)
@@ -11,6 +11,9 @@
  * @todo FIXME: If made 'required', only the text field should be compulsory.
  */
 class HTMLSelectAndOtherField extends HTMLSelectField {
+       /** @var string[] */
+       private $mFlatOptions;
+
        public function __construct( $params ) {
                if ( array_key_exists( 'other', $params ) ) {
                        // Do nothing
index a4120a3..13ab9b7 100644 (file)
@@ -29,6 +29,7 @@ use GuzzleHttp\Psr7\StreamDecoratorTrait;
  *
  * @private for use by GuzzleHttpRequest only
  * @since 1.33
+ * @property StreamInterface $stream Defined in StreamDecoratorTrait via @property, not read by phan
  */
 class MWCallbackStream implements StreamInterface {
        use StreamDecoratorTrait;
index 2f8f5dd..81e414e 100644 (file)
@@ -30,6 +30,9 @@ use MediaWiki\MediaWikiServices;
  * @ingroup SpecialPage
  */
 class ImportStreamSource implements ImportSource {
+       /** @var resource */
+       private $mHandle;
+
        /**
         * @param resource $handle
         */
index fdd1f77..b75ea1a 100644 (file)
  * @ingroup SpecialPage
  */
 class ImportStringSource implements ImportSource {
+       /** @var string */
+       private $mString;
+
+       /** @var bool */
+       private $mRead;
+
        /**
         * @param string $string
         */
index 08cfd86..eb96e05 100644 (file)
@@ -21,6 +21,9 @@
  */
 
 class InstallDocFormatter {
+       /** @var string */
+       private $text;
+
        public static function format( $text ) {
                $obj = new self( $text );
 
index a2179c6..6921361 100644 (file)
@@ -30,6 +30,7 @@
 class LocalSettingsGenerator {
 
        protected $extensions = [];
+       protected $skins = [];
        protected $values = [];
        protected $groupPermissions = [];
        protected $dbSettings = '';
index 5e7485c..f511229 100644 (file)
@@ -48,6 +48,9 @@ class MemoizedCallable {
        /** @var string Unique name of callable; used for cache keys. */
        private $callableName;
 
+       /** @var int */
+       private $ttl;
+
        /**
         * @throws InvalidArgumentException if $callable is not a callable.
         * @param callable $callable Function or method to memoize.
index 54d43d3..b036f05 100644 (file)
@@ -7,6 +7,10 @@
  * @since 1.27
  */
 class ComposerInstalled {
+       /**
+        * @var array[]
+        */
+       private $contents;
 
        /**
         * @param string $location
@@ -18,7 +22,7 @@ class ComposerInstalled {
        /**
         * Dependencies currently installed according to installed.json
         *
-        * @return array
+        * @return array[]
         */
        public function getInstalledDependencies() {
                $deps = [];
index 62231a8..f92759b 100644 (file)
@@ -7,6 +7,10 @@
  * @since 1.25
  */
 class ComposerJson {
+       /**
+        * @var array[]
+        */
+       private $contents;
 
        /**
         * @param string $location
@@ -18,7 +22,7 @@ class ComposerJson {
        /**
         * Dependencies as specified by composer.json
         *
-        * @return array
+        * @return string[]
         */
        public function getRequiredDependencies() {
                $deps = [];
index c5b5f00..7f6b875 100644 (file)
@@ -7,6 +7,10 @@
  * @since 1.25
  */
 class ComposerLock {
+       /**
+        * @var array[]
+        */
+       private $contents;
 
        /**
         * @param string $location
@@ -18,7 +22,7 @@ class ComposerLock {
        /**
         * Dependencies currently installed according to composer.lock
         *
-        * @return array
+        * @return array[]
         */
        public function getInstalledDependencies() {
                $deps = [];
index 5caf250..d7d428e 100644 (file)
@@ -1396,12 +1396,12 @@ abstract class FileBackendStore extends FileBackend {
                        }
                }
 
-               $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
+               $statuses = $this->doExecuteOpHandlesInternal( $fileOpHandles );
                foreach ( $fileOpHandles as $fileOpHandle ) {
                        $fileOpHandle->closeResources();
                }
 
-               return $res;
+               return $statuses;
        }
 
        /**
index 5632d5e..820634c 100644 (file)
@@ -172,64 +172,33 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        /**
-        * Sanitize and filter the custom headers from a $params array.
-        * Only allows certain "standard" Content- and X-Content- headers.
+        * Filter/normalize a header map to only include mutable "content-"/"x-content-" headers
         *
-        * @param array $params
-        * @return array Sanitized value of 'headers' field in $params
-        */
-       protected function sanitizeHdrsStrict( array $params ) {
-               if ( !isset( $params['headers'] ) ) {
-                       return [];
-               }
-
-               $headers = $this->getCustomHeaders( $params['headers'] );
-               unset( $headers[ 'content-type' ] );
-
-               return $headers;
-       }
-
-       /**
-        * Sanitize and filter the custom headers from a $params array.
-        * Only allows certain "standard" Content- and X-Content- headers.
+        * Mutable headers can be changed via HTTP POST even if the file content is the same
         *
-        * When POSTing data, libcurl adds Content-Type: application/x-www-form-urlencoded
-        * if Content-Type is not set, which overwrites the stored Content-Type header
-        * in Swift - therefore for POSTing data do not strip the Content-Type header (the
-        * previously-stored header that has been already read back from swift is sent)
-        *
-        * @param array $params
-        * @return array Sanitized value of 'headers' field in $params
+        * @see https://docs.openstack.org/api-ref/object-store
+        * @param string[] $headers Map of (header => value) for a swift object
+        * @return string[] Map of (header => value) for Content-* headers mutable via POST
         */
-       protected function sanitizeHdrs( array $params ) {
-               return isset( $params['headers'] )
-                       ? $this->getCustomHeaders( $params['headers'] )
-                       : [];
-       }
-
-       /**
-        * @param array $rawHeaders
-        * @return array Custom non-metadata HTTP headers
-        */
-       protected function getCustomHeaders( array $rawHeaders ) {
-               $headers = [];
-
+       protected function extractMutableContentHeaders( array $headers ) {
+               $contentHeaders = [];
                // Normalize casing, and strip out illegal headers
-               foreach ( $rawHeaders as $name => $value ) {
+               foreach ( $headers as $name => $value ) {
                        $name = strtolower( $name );
-                       if ( preg_match( '/^content-length$/', $name ) ) {
-                               continue; // blacklisted
-                       } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
-                               $headers[$name] = $value; // allowed
-                       } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
-                               $headers[$name] = $value; // allowed
+                       if ( !preg_match( '/^(x-)?content-(?!length$)/', $name ) ) {
+                               // Only allow content-* and x-content-* headers (but not content-length)
+                               continue;
+                       } elseif ( $name === 'content-type' && !strlen( $value ) ) {
+                               // This header can be set to a value but not unset for sanity
+                               continue;
                        }
+                       $contentHeaders[$name] = $value;
                }
                // By default, Swift has annoyingly low maximum header value limits
-               if ( isset( $headers['content-disposition'] ) ) {
+               if ( isset( $contentHeaders['content-disposition'] ) ) {
                        $disposition = '';
                        // @note: assume FileBackend::makeContentDisposition() already used
-                       foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
+                       foreach ( explode( ';', $contentHeaders['content-disposition'] ) as $part ) {
                                $part = trim( $part );
                                $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
                                if ( strlen( $new ) <= 255 ) {
@@ -238,36 +207,40 @@ class SwiftFileBackend extends FileBackendStore {
                                        break; // too long; sigh
                                }
                        }
-                       $headers['content-disposition'] = $disposition;
+                       $contentHeaders['content-disposition'] = $disposition;
                }
 
-               return $headers;
+               return $contentHeaders;
        }
 
        /**
-        * @param array $rawHeaders
-        * @return array Custom metadata headers
+        * @see https://docs.openstack.org/api-ref/object-store
+        * @param string[] $headers Map of (header => value) for a swift object
+        * @return string[] Map of (metadata header name => metadata value)
         */
-       protected function getMetadataHeaders( array $rawHeaders ) {
-               $headers = [];
-               foreach ( $rawHeaders as $name => $value ) {
+       protected function extractMetadataHeaders( array $headers ) {
+               $metadataHeaders = [];
+               foreach ( $headers as $name => $value ) {
                        $name = strtolower( $name );
                        if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
-                               $headers[$name] = $value;
+                               $metadataHeaders[$name] = $value;
                        }
                }
 
-               return $headers;
+               return $metadataHeaders;
        }
 
        /**
-        * @param array $rawHeaders
-        * @return array Custom metadata headers with prefix removed
+        * @see https://docs.openstack.org/api-ref/object-store
+        * @param string[] $headers Map of (header => value) for a swift object
+        * @return string[] Map of (metadata key name => metadata value)
         */
-       protected function getMetadata( array $rawHeaders ) {
+       protected function getMetadataFromHeaders( array $headers ) {
+               $prefixLen = strlen( 'x-object-meta-' );
+
                $metadata = [];
-               foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
-                       $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
+               foreach ( $this->extractMetadataHeaders( $headers ) as $name => $value ) {
+                       $metadata[substr( $name, $prefixLen )] = $value;
                }
 
                return $metadata;
@@ -283,19 +256,24 @@ class SwiftFileBackend extends FileBackendStore {
                        return $status;
                }
 
-               $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
-               $contentType = $params['headers']['content-type']
+               // Headers that are not strictly a function of the file content
+               $mutableHeaders = $this->extractMutableContentHeaders( $params['headers'] ?? [] );
+               // Make sure that the "content-type" header is set to something sensible
+               $mutableHeaders['content-type'] = $mutableHeaders['content-type']
                        ?? $this->getContentType( $params['dst'], $params['content'], null );
 
                $reqs = [ [
                        'method' => 'PUT',
                        'url' => [ $dstCont, $dstRel ],
-                       'headers' => [
-                               'content-length' => strlen( $params['content'] ),
-                               'etag' => md5( $params['content'] ),
-                               'content-type' => $contentType,
-                               'x-object-meta-sha1base36' => $sha1Hash
-                       ] + $this->sanitizeHdrsStrict( $params ),
+                       'headers' => array_merge(
+                               $mutableHeaders,
+                               [
+                                       'content-length' => strlen( $params['content'] ),
+                                       'etag' => md5( $params['content'] ),
+                                       'x-object-meta-sha1base36' =>
+                                               Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 )
+                               ]
+                       ),
                        'body' => $params['content']
                ] ];
 
@@ -309,6 +287,8 @@ class SwiftFileBackend extends FileBackendStore {
                        } else {
                                $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
                        }
+
+                       return SwiftFileOpHandle::CONTINUE_IF_OK;
                };
 
                $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
@@ -332,16 +312,13 @@ class SwiftFileBackend extends FileBackendStore {
                }
 
                AtEase::suppressWarnings();
-               $sha1Hash = sha1_file( $params['src'] );
+               $sha1Base16 = sha1_file( $params['src'] );
                AtEase::restoreWarnings();
-               if ( $sha1Hash === false ) { // source doesn't exist?
+               if ( $sha1Base16 === false ) { // source doesn't exist?
                        $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
 
                        return $status;
                }
-               $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
-               $contentType = $params['headers']['content-type']
-                       ?? $this->getContentType( $params['dst'], null, $params['src'] );
 
                $handle = fopen( $params['src'], 'rb' );
                if ( $handle === false ) { // source doesn't exist?
@@ -350,15 +327,23 @@ class SwiftFileBackend extends FileBackendStore {
                        return $status;
                }
 
+               // Headers that are not strictly a function of the file content
+               $mutableHeaders = $this->extractMutableContentHeaders( $params['headers'] ?? [] );
+               // Make sure that the "content-type" header is set to something sensible
+               $mutableHeaders['content-type'] = $mutableHeaders['content-type']
+                       ?? $this->getContentType( $params['dst'], null, $params['src'] );
+
                $reqs = [ [
                        'method' => 'PUT',
                        'url' => [ $dstCont, $dstRel ],
-                       'headers' => [
-                               'content-length' => filesize( $params['src'] ),
-                               'etag' => md5_file( $params['src'] ),
-                               'content-type' => $contentType,
-                               'x-object-meta-sha1base36' => $sha1Hash
-                       ] + $this->sanitizeHdrsStrict( $params ),
+                       'headers' => array_merge(
+                               $mutableHeaders,
+                               [
+                                       'content-length' => fstat( $handle )['size'],
+                                       'etag' => md5_file( $params['src'] ),
+                                       'x-object-meta-sha1base36' => Wikimedia\base_convert( $sha1Base16, 16, 36, 31 )
+                               ]
+                       ),
                        'body' => $handle // resource
                ] ];
 
@@ -372,6 +357,8 @@ class SwiftFileBackend extends FileBackendStore {
                        } else {
                                $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
                        }
+
+                       return SwiftFileOpHandle::CONTINUE_IF_OK;
                };
 
                $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
@@ -406,10 +393,13 @@ class SwiftFileBackend extends FileBackendStore {
                $reqs = [ [
                        'method' => 'PUT',
                        'url' => [ $dstCont, $dstRel ],
-                       'headers' => [
-                               'x-copy-from' => '/' . rawurlencode( $srcCont ) .
-                                       '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
-                       ] + $this->sanitizeHdrsStrict( $params ), // extra headers merged into object
+                       'headers' => array_merge(
+                               $this->extractMutableContentHeaders( $params['headers'] ?? [] ),
+                               [
+                                       'x-copy-from' => '/' . rawurlencode( $srcCont ) . '/' .
+                                               str_replace( "%2F", "/", rawurlencode( $srcRel ) )
+                               ]
+                       )
                ] ];
 
                $method = __METHOD__;
@@ -418,10 +408,14 @@ class SwiftFileBackend extends FileBackendStore {
                        if ( $rcode === 201 ) {
                                // good
                        } elseif ( $rcode === 404 ) {
-                               $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+                               if ( empty( $params['ignoreMissingSource'] ) ) {
+                                       $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+                               }
                        } else {
                                $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
                        }
+
+                       return SwiftFileOpHandle::CONTINUE_IF_OK;
                };
 
                $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
@@ -451,16 +445,17 @@ class SwiftFileBackend extends FileBackendStore {
                        return $status;
                }
 
-               $reqs = [
-                       [
-                               'method' => 'PUT',
-                               'url' => [ $dstCont, $dstRel ],
-                               'headers' => [
-                                       'x-copy-from' => '/' . rawurlencode( $srcCont ) .
-                                               '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
-                               ] + $this->sanitizeHdrsStrict( $params ) // extra headers merged into object
-                       ]
-               ];
+               $reqs = [ [
+                       'method' => 'PUT',
+                       'url' => [ $dstCont, $dstRel ],
+                       'headers' => array_merge(
+                               $this->extractMutableContentHeaders( $params['headers'] ?? [] ),
+                               [
+                                       'x-copy-from' => '/' . rawurlencode( $srcCont ) . '/' .
+                                               str_replace( "%2F", "/", rawurlencode( $srcRel ) )
+                               ]
+                       )
+               ] ];
                if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
                        $reqs[] = [
                                'method' => 'DELETE',
@@ -477,10 +472,17 @@ class SwiftFileBackend extends FileBackendStore {
                        } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
                                // good
                        } elseif ( $rcode === 404 ) {
-                               $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+                               if ( empty( $params['ignoreMissingSource'] ) ) {
+                                       $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+                               } else {
+                                       // Leave Status as OK but skip the DELETE request
+                                       return SwiftFileOpHandle::CONTINUE_NO;
+                               }
                        } else {
                                $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
                        }
+
+                       return SwiftFileOpHandle::CONTINUE_IF_OK;
                };
 
                $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
@@ -521,6 +523,8 @@ class SwiftFileBackend extends FileBackendStore {
                        } else {
                                $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
                        }
+
+                       return SwiftFileOpHandle::CONTINUE_IF_OK;
                };
 
                $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
@@ -554,17 +558,20 @@ class SwiftFileBackend extends FileBackendStore {
                        return $status;
                }
 
-               // POST clears prior headers, so we need to merge the changes in to the old ones
-               $metaHdrs = [];
+               // Swift object POST clears any prior headers, so merge the new and old headers here.
+               // Also, during, POST, libcurl adds "Content-Type: application/x-www-form-urlencoded"
+               // if "Content-Type" is not set, which would clobber the header value for the object.
+               $oldMetadataHeaders = [];
                foreach ( $stat['xattr']['metadata'] as $name => $value ) {
-                       $metaHdrs["x-object-meta-$name"] = $value;
+                       $oldMetadataHeaders["x-object-meta-$name"] = $value;
                }
-               $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
+               $newContentHeaders = $this->extractMutableContentHeaders( $params['headers'] ?? [] );
+               $oldContentHeaders = $stat['xattr']['headers'];
 
                $reqs = [ [
                        'method' => 'POST',
                        'url' => [ $srcCont, $srcRel ],
-                       'headers' => $metaHdrs + $customHdrs
+                       'headers' => $oldMetadataHeaders + $newContentHeaders + $oldContentHeaders
                ] ];
 
                $method = __METHOD__;
@@ -743,9 +750,9 @@ class SwiftFileBackend extends FileBackendStore {
                }
 
                // Find prior custom HTTP headers
-               $postHeaders = $this->getCustomHeaders( $objHdrs );
+               $postHeaders = $this->extractMutableContentHeaders( $objHdrs );
                // Find prior metadata headers
-               $postHeaders += $this->getMetadataHeaders( $objHdrs );
+               $postHeaders += $this->extractMetadataHeaders( $objHdrs );
 
                $status = $this->newStatus();
                /** @noinspection PhpUnusedLocalVariableInspection */
@@ -1293,11 +1300,6 @@ class SwiftFileBackend extends FileBackendStore {
                return $hdrs;
        }
 
-       /**
-        * @param FileBackendStoreOpHandle[] $fileOpHandles
-        *
-        * @return StatusValue[]
-        */
        protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
                /** @var SwiftFileOpHandle[] $fileOpHandles */
                '@phan-var SwiftFileOpHandle[] $fileOpHandles';
@@ -1334,13 +1336,18 @@ class SwiftFileBackend extends FileBackendStore {
                for ( $stage = 0; $stage < $reqCount; ++$stage ) {
                        $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
                        foreach ( $httpReqs as $index => $httpReq ) {
+                               /** @var SwiftFileOpHandle $fileOpHandle */
+                               $fileOpHandle = $fileOpHandles[$index];
                                // Run the callback for each request of this operation
-                               $callback = $fileOpHandles[$index]->callback;
-                               $callback( $httpReq, $statuses[$index] );
-                               // On failure, abort all remaining requests for this operation
-                               // (e.g. abort the DELETE request if the COPY request fails for a move)
-                               if ( !$statuses[$index]->isOK() ) {
-                                       $stages = count( $fileOpHandles[$index]->httpOp );
+                               $status = $statuses[$index];
+                               ( $fileOpHandle->callback )( $httpReq, $status );
+                               // On failure, abort all remaining requests for this operation. This is used
+                               // in "move" operations to abort the DELETE request if the PUT request fails.
+                               if (
+                                       !$status->isOK() ||
+                                       $fileOpHandle->state === $fileOpHandle::CONTINUE_NO
+                               ) {
+                                       $stages = count( $fileOpHandle->httpOp );
                                        for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
                                                unset( $httpReqsByStage[$s][$index] );
                                        }
@@ -1670,9 +1677,9 @@ class SwiftFileBackend extends FileBackendStore {
         */
        protected function getStatFromHeaders( array $rhdrs ) {
                // Fetch all of the custom metadata headers
-               $metadata = $this->getMetadata( $rhdrs );
+               $metadata = $this->getMetadataFromHeaders( $rhdrs );
                // Fetch all of the custom raw HTTP headers
-               $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
+               $headers = $this->extractMutableContentHeaders( $rhdrs );
 
                return [
                        // Convert various random Swift dates to TS_MW
index 1119867..70ed46a 100644 (file)
  * @see FileBackendStoreOpHandle
  */
 class SwiftFileOpHandle extends FileBackendStoreOpHandle {
-       /** @var array List of Requests for MultiHttpClient */
+       /** @var array[] List of HTTP request maps for MultiHttpClient */
        public $httpOp;
-       /** @var Closure */
+       /** @var Closure Function to run after each HTTP request finishes */
        public $callback;
 
+       /** @var int Class CONTINUE_* constant */
+       public $state = self::CONTINUE_IF_OK;
+
+       /** @var int Continue with the next requests stages if no errors occured */
+       const CONTINUE_IF_OK = 0;
+       /** @var int Cancel the next requests stages */
+       const CONTINUE_NO = 1;
+
        /**
+        * Construct a handle to be use with SwiftFileOpHandle::doExecuteOpHandlesInternal()
+        *
+        * The callback returns a class CONTINUE_* constant and takes the following parameters:
+        *   - An HTTP request map array with 'response' filled
+        *   - A StatusValue instance to be updated as needed
+        *
         * @param SwiftFileBackend $backend
-        * @param Closure $callback Function that takes (HTTP request array, status)
+        * @param Closure $callback
         * @param array $httpOp MultiHttpClient op
         */
        public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) {
index 6478a61..83dcc6b 100644 (file)
@@ -152,7 +152,7 @@ abstract class QuorumLockManager extends LockManager {
         * This is all or nothing; if any key is already pledged then this totally fails.
         *
         * @param int $bucket
-        * @param callable $callback Pledge method taking a server name and yeilding a StatusValue
+        * @param callable $callback Pledge method taking a server name and yielding a StatusValue
         * @return StatusValue
         */
        final protected function collectPledgeQuorum( $bucket, callable $callback ) {
@@ -194,7 +194,7 @@ abstract class QuorumLockManager extends LockManager {
         * Attempt to release pledges with the peers for a bucket
         *
         * @param int $bucket
-        * @param callable $callback Pledge method taking a server name and yeilding a StatusValue
+        * @param callable $callback Pledge method taking a server name and yielding a StatusValue
         * @return StatusValue
         */
        final protected function releasePledges( $bucket, callable $callback ) {
index 9d66326..16cd5ca 100644 (file)
@@ -68,6 +68,9 @@ class XmlTypeCheck {
         */
        protected $stackDepth = 0;
 
+       /** @var callable|null */
+       protected $filterCallback;
+
        /**
         * @var array Additional parsing options
         */
index 51f7316..77a7883 100644 (file)
@@ -46,7 +46,7 @@ class MultiWriteBagOStuff extends BagOStuff {
        /**
         * $params include:
         *   - caches: A numbered array of either ObjectFactory::getObjectFromSpec
-        *      arrays yeilding BagOStuff objects or direct BagOStuff objects.
+        *      arrays yielding BagOStuff objects or direct BagOStuff objects.
         *      If using the former, the 'args' field *must* be set.
         *      The first cache is the primary one, being the first to
         *      be read in the fallback chain. Writes happen to all stores
index ff87829..9f953e1 100644 (file)
@@ -47,9 +47,9 @@ class ReplicatedBagOStuff extends BagOStuff {
 
        /**
         * Constructor. Parameters are:
-        *   - writeFactory: ObjectFactory::getObjectFromSpec array yeilding BagOStuff.
+        *   - writeFactory: ObjectFactory::getObjectFromSpec array yielding BagOStuff.
         *      This object will be used for writes (e.g. the master DB).
-        *   - readFactory: ObjectFactory::getObjectFromSpec array yeilding BagOStuff.
+        *   - readFactory: ObjectFactory::getObjectFromSpec array yielding BagOStuff.
         *      This object will be used for reads (e.g. a replica DB).
         *   - sessionConsistencyWindow: Seconds to read from the master source for a key
         *      after writing to it. [Default: ReplicatedBagOStuff::MAX_WRITE_DELAY]
index 0abb6f6..652c36a 100644 (file)
@@ -32,6 +32,12 @@ use MediaWiki\Storage\RevisionRecord;
  * @since 1.19
  */
 class DeleteLogFormatter extends LogFormatter {
+       /** @var array|null */
+       private $parsedParametersDeleteLog;
+
+       /**
+        * @inheritDoc
+        */
        protected function getMessageKey() {
                $key = parent::getMessageKey();
                if ( in_array( $this->entry->getSubtype(), [ 'event', 'revision' ] ) ) {
@@ -51,8 +57,11 @@ class DeleteLogFormatter extends LogFormatter {
                return $key;
        }
 
+       /**
+        * @inheritDoc
+        */
        protected function getMessageParameters() {
-               if ( isset( $this->parsedParametersDeleteLog ) ) {
+               if ( $this->parsedParametersDeleteLog !== null ) {
                        return $this->parsedParametersDeleteLog;
                }
 
index 47aed56..5e9fdb8 100644 (file)
@@ -53,6 +53,12 @@ class LogPager extends ReverseChronologicalPager {
        /** @var bool */
        private $actionRestrictionsEnforced = false;
 
+       /** @var array */
+       private $mConds;
+
+       /** @var string */
+       private $mTagFilter;
+
        /** @var LogEventsList */
        public $mLogEventsList;
 
index 3b904e8..d54dd6b 100644 (file)
@@ -240,6 +240,7 @@ class DjVuHandler extends ImageHandler {
         * @param File|FSFile $image
         * @param string $path
         * @return DjVuImage
+        * @suppress PhanUndeclaredProperty Custom property
         */
        function getDjVuImage( $image, $path ) {
                if ( !$image ) {
@@ -290,6 +291,7 @@ class DjVuHandler extends ImageHandler {
         * @param File $image
         * @param bool $gettext DOCUMENT (Default: false)
         * @return bool|SimpleXMLElement
+        * @suppress PhanUndeclaredProperty Custom property
         */
        public function getMetaTree( $image, $gettext = false ) {
                if ( $gettext && isset( $image->djvuTextTree ) ) {
index 92fad52..50b13a4 100644 (file)
@@ -42,6 +42,9 @@ class DjVuImage {
         */
        const DJVUTXT_MEMORY_LIMIT = 300000;
 
+       /** @var string */
+       private $mFilename;
+
        /**
         * @param string $filename The DjVu file name.
         */
index 4bede96..6aeb038 100644 (file)
@@ -363,8 +363,16 @@ class Article implements Page {
                        }
                }
 
+               $rl = MediaWikiServices::getInstance()->getRevisionLookup();
+               $oldRev = $this->mRevision ? $this->mRevision->getRevisionRecord() : null;
                if ( $request->getVal( 'direction' ) == 'next' ) {
-                       $nextid = $this->getTitle()->getNextRevisionID( $oldid );
+                       $nextid = 0;
+                       if ( $oldRev ) {
+                               $nextRev = $rl->getNextRevision( $oldRev );
+                               if ( $nextRev ) {
+                                       $nextid = $nextRev->getId();
+                               }
+                       }
                        if ( $nextid ) {
                                $oldid = $nextid;
                                $this->mRevision = null;
@@ -372,7 +380,13 @@ class Article implements Page {
                                $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
                        }
                } elseif ( $request->getVal( 'direction' ) == 'prev' ) {
-                       $previd = $this->getTitle()->getPreviousRevisionID( $oldid );
+                       $previd = 0;
+                       if ( $oldRev ) {
+                               $prevRev = $rl->getPreviousRevision( $oldRev );
+                               if ( $prevRev ) {
+                                       $previd = $prevRev->getId();
+                               }
+                       }
                        if ( $previd ) {
                                $oldid = $previd;
                                $this->mRevision = null;
@@ -1599,8 +1613,9 @@ class Article implements Page {
                                        'oldid' => $oldid
                                ] + $extraParams
                        );
-               $prev = $this->getTitle()->getPreviousRevisionID( $oldid );
-               $prevlink = $prev
+               $rl = MediaWikiServices::getInstance()->getRevisionLookup();
+               $prevExist = (bool)$rl->getPreviousRevision( $revision->getRevisionRecord() );
+               $prevlink = $prevExist
                        ? Linker::linkKnown(
                                $this->getTitle(),
                                $context->msg( 'previousrevision' )->escaped(),
@@ -1611,7 +1626,7 @@ class Article implements Page {
                                ] + $extraParams
                        )
                        : $context->msg( 'previousrevision' )->escaped();
-               $prevdiff = $prev
+               $prevdiff = $prevExist
                        ? Linker::linkKnown(
                                $this->getTitle(),
                                $context->msg( 'diff' )->escaped(),
index 40c63d2..8b2ff6b 100644 (file)
@@ -756,10 +756,14 @@ class PageArchive {
 
                        Hooks::run( 'ArticleUndelete',
                                [ &$this->title, $created, $comment, $oldPageId, $restoredPages ] );
+
                        if ( $this->title->getNamespace() == NS_FILE ) {
-                               DeferredUpdates::addUpdate(
-                                       new HTMLCacheUpdate( $this->title, 'imagelinks', 'file-restore' )
+                               $job = HTMLCacheUpdateJob::newForBacklinks(
+                                       $this->title,
+                                       'imagelinks',
+                                       [ 'causeAction' => 'file-restore' ]
                                );
+                               JobQueueGroup::singleton()->lazyPush( $job );
                        }
                }
 
index acd506b..fd9f7b2 100644 (file)
@@ -176,9 +176,12 @@ class WikiFilePage extends WikiPage {
 
                if ( $this->mFile->exists() ) {
                        wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" );
-                       DeferredUpdates::addUpdate(
-                               new HTMLCacheUpdate( $this->mTitle, 'imagelinks', 'file-purge' )
+                       $job = HTMLCacheUpdateJob::newForBacklinks(
+                               $this->mTitle,
+                               'imagelinks',
+                               [ 'causeAction' => 'file-purge' ]
                        );
+                       JobQueueGroup::singleton()->lazyPush( $job );
                } else {
                        wfDebug( 'ImagePage::doPurge no image for '
                                . $this->mFile->getName() . "; limiting purge to cache only\n" );
index f0a656d..c8566ac 100644 (file)
@@ -3404,9 +3404,12 @@ class WikiPage implements Page, IDBAccessObject {
                MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
 
                // Invalidate caches of articles which include this page
-               DeferredUpdates::addUpdate(
-                       new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
+               $job = HTMLCacheUpdateJob::newForBacklinks(
+                       $title,
+                       'templatelinks',
+                       [ 'causeAction' => 'page-create' ]
                );
+               JobQueueGroup::singleton()->lazyPush( $job );
 
                if ( $title->getNamespace() == NS_CATEGORY ) {
                        // Load the Category object, which will schedule a job to create
@@ -3448,9 +3451,12 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Images
                if ( $title->getNamespace() == NS_FILE ) {
-                       DeferredUpdates::addUpdate(
-                               new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
+                       $job = HTMLCacheUpdateJob::newForBacklinks(
+                               $title,
+                               'imagelinks',
+                               [ 'causeAction' => 'page-delete' ]
                        );
+                       JobQueueGroup::singleton()->lazyPush( $job );
                }
 
                // User talk pages
@@ -3482,20 +3488,24 @@ class WikiPage implements Page, IDBAccessObject {
                $slotsChanged = null
        ) {
                // TODO: move this into a PageEventEmitter service
-
-               if ( $slotsChanged === null || in_array( SlotRecord::MAIN,  $slotsChanged ) ) {
+               $jobs = [];
+               if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
                        // Invalidate caches of articles which include this page.
                        // Only for the main slot, because only the main slot is transcluded.
                        // TODO: MCR: not true for TemplateStyles! [SlotHandler]
-                       DeferredUpdates::addUpdate(
-                               new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
+                       $jobs[] = HTMLCacheUpdateJob::newForBacklinks(
+                               $title,
+                               'templatelinks',
+                               [ 'causeAction' => 'page-edit' ]
                        );
                }
-
                // Invalidate the caches of all pages which redirect here
-               DeferredUpdates::addUpdate(
-                       new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
+               $jobs[] = HTMLCacheUpdateJob::newForBacklinks(
+                       $title,
+                       'redirect',
+                       [ 'causeAction' => 'page-edit' ]
                );
+               JobQueueGroup::singleton()->lazyPush( $jobs );
 
                MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
 
index 6497543..43ebfbf 100644 (file)
@@ -337,6 +337,7 @@ class ResourceLoaderImage {
                // Reattach all direct children of the `<svg>` root node to the `<g>` wrapper
                while ( $root->firstChild ) {
                        $node = $root->firstChild;
+                       // @phan-suppress-next-line PhanUndeclaredProperty False positive
                        if ( !$titleNode && $node->nodeType === XML_ELEMENT_NODE && $node->tagName === 'title' ) {
                                // Remember the first encountered `<title>` node
                                $titleNode = $node;
index 0954c45..bf5734a 100644 (file)
@@ -884,87 +884,85 @@ abstract class ChangesListSpecialPage extends SpecialPage {
        }
 
        /**
-        * Get (cheap to compute) information about change tags.
+        * Get information about change tags, without parsing messages, for getRcFiltersConfigSummary().
+        *
+        * Message contents are the raw values (->plain()), because parsing messages is expensive.
+        * Even though we're not parsing messages, building a data structure with the contents of
+        * hundreds of i18n messages is still not cheap (see T223260#5370610), so the result of this
+        * function is cached in WANCache for 24 hours.
         *
         * Returns an array of associative arrays with information about each tag:
         * - name: Tag name (string)
         * - labelMsg: Short description message (Message object)
+        * - label: Short description message (raw message contents)
         * - descriptionMsg: Long description message (Message object)
+        * - description: Long description message (raw message contents)
         * - cssClass: CSS class to use for RC entries with this tag
         * - hits: Number of RC entries that have this tag
         *
         * @param ResourceLoaderContext $context
         * @return array[] Information about each tag
         */
-       protected static function getChangeTagInfo( ResourceLoaderContext $context ) {
-               $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
-               $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
-
-               $tagStats = ChangeTags::tagUsageStatistics();
-               $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
-
-               $result = [];
-               foreach ( $tagHitCounts as $tagName => $hits ) {
-                       if (
-                               (
-                                       // Only get active tags
-                                       isset( $explicitlyDefinedTags[ $tagName ] ) ||
-                                       isset( $softwareActivatedTags[ $tagName ] )
-                               ) &&
-                               // Only get tags with more than 0 hits
-                               $hits > 0
-                       ) {
-                               $labelMsg = ChangeTags::tagShortDescriptionMessage( $tagName, $context );
-                               if ( $labelMsg === false ) {
-                                       // Tag is hidden, skip it
-                                       continue;
-                               }
-                               $result[] = [
-                                       'name' => $tagName,
-                                       // 'label' and 'description' filled in by getChangeTagList()
-                                       'labelMsg' => $labelMsg,
-                                       'descriptionMsg' => ChangeTags::tagLongDescriptionMessage( $tagName, $context ),
-                                       'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
-                                       'hits' => $hits,
-                               ];
-                       }
-               }
-               return $result;
-       }
-
-       /**
-        * Get information about change tags for use in getRcFiltersConfigSummary().
-        *
-        * This expands labelMsg and descriptionMsg to the raw values of each message, which captures
-        * changes in the messages but avoids the expensive step of parsing them.
-        *
-        * @param ResourceLoaderContext $context
-        * @return array[] Result of getChangeTagInfo(), with messages expanded to raw contents
-        */
        protected static function getChangeTagListSummary( ResourceLoaderContext $context ) {
-               $tags = self::getChangeTagInfo( $context );
-               foreach ( $tags as &$tagInfo ) {
-                       $tagInfo['labelMsg'] = $tagInfo['labelMsg']->plain();
-                       if ( $tagInfo['descriptionMsg'] ) {
-                               $tagInfo['descriptionMsg'] = $tagInfo['descriptionMsg']->plain();
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+               return $cache->getWithSetCallback(
+                       $cache->makeKey( 'ChangesListSpecialPage-changeTagListSummary', $context->getLanguage() ),
+                       WANObjectCache::TTL_DAY,
+                       function ( $oldValue, &$ttl, array &$setOpts ) use ( $context ) {
+                               $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
+                               $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
+
+                               $tagStats = ChangeTags::tagUsageStatistics();
+                               $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
+
+                               $result = [];
+                               foreach ( $tagHitCounts as $tagName => $hits ) {
+                                       if (
+                                               (
+                                                       // Only get active tags
+                                                       isset( $explicitlyDefinedTags[ $tagName ] ) ||
+                                                       isset( $softwareActivatedTags[ $tagName ] )
+                                               ) &&
+                                               // Only get tags with more than 0 hits
+                                               $hits > 0
+                                       ) {
+                                               $labelMsg = ChangeTags::tagShortDescriptionMessage( $tagName, $context );
+                                               if ( $labelMsg === false ) {
+                                                       // Tag is hidden, skip it
+                                                       continue;
+                                               }
+                                               $descriptionMsg = ChangeTags::tagLongDescriptionMessage( $tagName, $context );
+                                               $result[] = [
+                                                       'name' => $tagName,
+                                                       'labelMsg' => $labelMsg,
+                                                       'label' => $labelMsg->plain(),
+                                                       'descriptionMsg' => $descriptionMsg,
+                                                       'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
+                                                       'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
+                                                       'hits' => $hits,
+                                               ];
+                                       }
+                               }
+                               return $result;
                        }
-               }
-               return $tags;
+               );
        }
 
        /**
         * Get information about change tags to export to JS via getRcFiltersConfigVars().
         *
-        * This removes labelMsg and descriptionMsg, and adds label and description, which are parsed,
-        * stripped and (in the case of description) truncated versions of these messages. Message
+        * This manipulates the label and description of each tag, which are parsed, stripped
+        * and (in the case of description) truncated versions of these messages. Message
         * parsing is expensive, so to detect whether the tag list has changed, use
         * getChangeTagListSummary() instead.
         *
+        * The result of this function is cached in WANCache for 24 hours.
+        *
         * @param ResourceLoaderContext $context
-        * @return array[] Result of getChangeTagInfo(), with messages parsed, stripped and truncated
+        * @return array[] Same as getChangeTagListSummary(), with messages parsed, stripped and truncated
         */
        protected static function getChangeTagList( ResourceLoaderContext $context ) {
-               $tags = self::getChangeTagInfo( $context );
+               $tags = self::getChangeTagListSummary( $context );
                $language = Language::factory( $context->getLanguage() );
                foreach ( $tags as &$tagInfo ) {
                        $tagInfo['label'] = Sanitizer::stripAllTags( $tagInfo['labelMsg']->parse() );
index 2c8d432..34665dd 100644 (file)
@@ -381,6 +381,7 @@ class SpecialTags extends SpecialPage {
 
                $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
                $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
+               // @phan-suppress-next-line PhanUndeclaredProperty
                $form->tagAction = 'delete'; // custom property on HTMLForm object
                $form->setSubmitCallback( [ $this, 'processTagForm' ] );
                $form->setSubmitTextMsg( 'tags-delete-submit' );
@@ -433,6 +434,7 @@ class SpecialTags extends SpecialPage {
 
                $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
                $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
+               // @phan-suppress-next-line PhanUndeclaredProperty
                $form->tagAction = $actionStr;
                $form->setSubmitCallback( [ $this, 'processTagForm' ] );
                // tags-activate-submit, tags-deactivate-submit
@@ -441,6 +443,12 @@ class SpecialTags extends SpecialPage {
                $form->show();
        }
 
+       /**
+        * @param array $data
+        * @param HTMLForm $form
+        * @return bool
+        * @suppress PhanUndeclaredProperty $form->tagAction
+        */
        public function processTagForm( array $data, HTMLForm $form ) {
                $context = $form->getContext();
                $out = $context->getOutput();
index b1bfd0b..445f0c3 100644 (file)
@@ -111,6 +111,8 @@ class PreferencesFormOOUI extends OOUIHTMLForm {
        function filterDataForSubmit( $data ) {
                foreach ( $this->mFlatFields as $fieldname => $field ) {
                        if ( $field instanceof HTMLNestedFilterable ) {
+                               // @phan-suppress-next-next-line PhanUndeclaredProperty All HTMLForm fields have mParams,
+                               // but the instanceof confuses phan, which doesn't support intersections
                                $info = $field->mParams;
                                $prefix = $info['prefix'] ?? $fieldname;
                                foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
index 3bd66d4..3ebb443 100644 (file)
@@ -237,6 +237,7 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
         * @param LinkTarget $title
         *
         * @return string
+        * @suppress PhanUndeclaredProperty
         */
        public function getPrefixedText( LinkTarget $title ) {
                if ( !isset( $title->prefixedText ) ) {
index d71750b..fc96fe1 100644 (file)
@@ -2794,18 +2794,6 @@ class User implements IDBAccessObject, UserIdentity {
                }
        }
 
-       /**
-        * Set the password for a password reminder or new account email
-        *
-        * @deprecated Removed in 1.27. Use PasswordReset instead.
-        * @param string $str New password to set or null to set an invalid
-        *  password hash meaning that the user will not be able to use it
-        * @param bool $throttle If true, reset the throttle timestamp to the present
-        */
-       public function setNewpassword( $str, $throttle = true ) {
-               throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
-       }
-
        /**
         * Get the user's e-mail address
         * @return string User's email address
@@ -3738,11 +3726,17 @@ class User implements IDBAccessObject, UserIdentity {
                                $this->setNewtalk( false );
 
                                // If there is a new, unseen, revision, use its timestamp
-                               $nextid = $oldid
-                                       ? $title->getNextRevisionID( $oldid, Title::READ_LATEST )
-                                       : null;
-                               if ( $nextid ) {
-                                       $this->setNewtalk( true, Revision::newFromId( $nextid ) );
+                               if ( $oldid ) {
+                                       $rl = MediaWikiServices::getInstance()->getRevisionLookup();
+                                       $oldRev = $rl->getRevisionById( $oldid, Title::READ_LATEST );
+                                       if ( $oldRev ) {
+                                               $newRev = $rl->getNextRevision( $oldRev );
+                                               if ( $newRev ) {
+                                                       // TODO: actually no need to wrap in a revision,
+                                                       // setNewtalk really only needs a RevRecord
+                                                       $this->setNewtalk( true, new Revision( $newRev ) );
+                                               }
+                                       }
                                }
                        } );
                }
@@ -4320,13 +4314,13 @@ class User implements IDBAccessObject, UserIdentity {
                                'password' => $password,
                        ]
                );
-               $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
+               $res = $manager->beginAuthentication( $reqs, 'null:' );
                switch ( $res->status ) {
                        case AuthenticationResponse::PASS:
                                return true;
                        case AuthenticationResponse::FAIL:
                                // Hope it's not a PreAuthenticationProvider that failed...
-                               \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                               LoggerFactory::getInstance( 'authentication' )
                                        ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
                                return false;
                        default:
index f3a8810..77ea3d9 100644 (file)
@@ -34,7 +34,7 @@ class AvroValidator {
         * @return string|string[] An error or list of errors in the
         *  provided $datum. When no errors exist the empty array is
         *  returned.
-        * @suppress PhanUndeclaredMethod
+        * @suppress PhanUndeclaredMethod,PhanUndeclaredProperty
         */
        public static function getErrors( AvroSchema $schema, $datum ) {
                switch ( $schema->type ) {
index 353127d..783a211 100644 (file)
@@ -80,7 +80,7 @@ class Names {
                'az' => 'azərbaycanca', # Azerbaijani
                'azb' => 'تۆرکجه', # South Azerbaijani
                'ba' => 'башҡортса', # Bashkir
-               'ban' => 'Basa Bali', # Balinese
+               'ban' => 'Bali', # Balinese
                'bar' => 'Boarisch', # Bavarian (Austro-Bavarian and South Tyrolean)
                'bat-smg' => 'žemaitėška', # Samogitian (deprecated code, 'sgs' in ISO 639-3 since 2010-06-30 )
                'bbc' => 'Batak Toba', # Batak Toba (falls back to bbc-latn)
index 1a5daca..6ea046e 100644 (file)
@@ -59,11 +59,6 @@ ALIASES                = "type{1}=<b> \1 </b>:" \
                          "codeCoverageIgnore=" \
                          "codingStandardsIgnoreEnd=" \
                          "codingStandardsIgnoreStart=" \
-                         "covers=" \
-                         "dataProvider=" \
-                         "expectedException=" \
-                         "expectedExceptionMessage=" \
-                         "group=" \
                          "phan=" \
                          "suppress="
 TCL_SUBST              =
@@ -112,8 +107,8 @@ SORT_GROUP_NAMES       = NO
 SORT_BY_SCOPE_NAME     = NO
 STRICT_PROTO_MATCHING  = NO
 GENERATE_TODOLIST      = YES
-GENERATE_TESTLIST      = YES
-GENERATE_BUGLIST       = YES
+GENERATE_TESTLIST      = NO
+GENERATE_BUGLIST       = NO
 GENERATE_DEPRECATEDLIST= YES
 ENABLED_SECTIONS       =
 MAX_INITIALIZER_LINES  = 30
index d4f9c2d..03035f7 100644 (file)
@@ -20,6 +20,7 @@
  *
  * @file
  * @ingroup Maintenance
+ * @phan-file-suppress PhanUndeclaredProperty Lots of custom properties
  */
 
 require_once __DIR__ . '/Maintenance.php';
index f600f13..4401ea8 100644 (file)
@@ -120,10 +120,17 @@ class MWDocGen extends Maintenance {
                        'resources/lib',
                        'images',
                        'static',
+                       'tests',
+                       'includes/libs/Message/README.md',
+                       'includes/libs/objectcache/README.md',
+                       'includes/libs/ParamValidator/README.md',
+                       'maintenance/benchmarks/README.md',
+                       'resources/src/mediawiki.ui/styleguide.md',
                ];
                $this->excludePatterns = [];
                if ( $this->hasOption( 'no-extensions' ) ) {
                        $this->excludePatterns[] = 'extensions';
+                       $this->excludePatterns[] = 'skins';
                }
 
                $this->doDot = shell_exec( 'which dot' );
index ed4a27f..249fa78 100644 (file)
@@ -850,8 +850,7 @@ class RevisionTest extends MediaWikiTestCase {
                );
 
                $cacheKey = $cache->makeGlobalKey(
-                       'BlobStore',
-                       'address',
+                       'SqlBlobStore-blob',
                        $lb->getLocalDomainID(),
                        'tt:7777'
                );
index 6a00e67..230d36a 100644 (file)
@@ -6,6 +6,7 @@ use MediaWiki\Block\CompositeBlock;
 use MediaWiki\Block\SystemBlock;
 use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
+use Psr\Log\LoggerInterface;
 
 /**
  * @group Blocking
@@ -48,13 +49,15 @@ class BlockManagerTest extends MediaWikiTestCase {
        private function getBlockManagerConstructorArgs( $overrideConfig ) {
                $blockManagerConfig = array_merge( $this->blockManagerConfig, $overrideConfig );
                $this->setMwGlobals( $blockManagerConfig );
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
                return [
                        new LoggedServiceOptions(
                                self::$serviceOptionsAccessLog,
                                BlockManager::$constructorOptions,
                                MediaWikiServices::getInstance()->getMainConfig()
                        ),
-                       MediaWikiServices::getInstance()->getPermissionManager()
+                       MediaWikiServices::getInstance()->getPermissionManager(),
+                       $logger
                ];
        }
 
index cd3ddfa..1f9d29e 100644 (file)
@@ -430,4 +430,18 @@ class LinksUpdateTest extends MediaWikiLangTestCase {
                        $queueGroup->ack( $job );
                }
        }
+
+       public function testIsRecursive() {
+               list( $title, $po ) = $this->makeTitleAndParserOutput( 'Test', 1 );
+               $linksUpdate = new LinksUpdate( $title, $po );
+               $this->assertTrue( $linksUpdate->isRecursive(), 'LinksUpdate is recursive by default' );
+
+               $linksUpdate = new LinksUpdate( $title, $po, true );
+               $this->assertTrue( $linksUpdate->isRecursive(),
+                       'LinksUpdate is recursive when asked to be recursive' );
+
+               $linksUpdate = new LinksUpdate( $title, $po, false );
+               $this->assertFalse( $linksUpdate->isRecursive(),
+                       'LinksUpdate is not recursive when asked to be not recursive' );
+       }
 }
index 3b30d7e..e344d57 100644 (file)
@@ -2422,7 +2422,7 @@ class FileBackendTest extends MediaWikiTestCase {
                        "$base/subdir2/subdir/sub/120-px-file.txt",
                ];
 
-               for ( $i = 0; $i < 25; $i++ ) {
+               for ( $i = 0; $i < 2; $i++ ) {
                        $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
                        $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName) ($i)." );
index 35eca28..13f2b17 100644 (file)
@@ -13,7 +13,7 @@ use Wikimedia\TestingAccessWrapper;
  * @covers SwiftFileBackendList
  */
 class SwiftFileBackendTest extends MediaWikiTestCase {
-       /** @var TestingAccessWrapper Proxy to SwiftFileBackend */
+       /** @var TestingAccessWrapper|SwiftFileBackend */
        private $backend;
 
        protected function setUp() {
@@ -34,26 +34,28 @@ class SwiftFileBackendTest extends MediaWikiTestCase {
        }
 
        /**
-        * @dataProvider provider_testSanitizeHdrsStrict
+        * @covers SwiftFileBackend::extractMutableContentHeaders
+        * @dataProvider provider_testExtractPostableContentHeaders
         */
-       public function testSanitizeHdrsStrict( $raw, $sanitized ) {
-               $hdrs = $this->backend->sanitizeHdrsStrict( [ 'headers' => $raw ] );
+       public function testExtractPostableContentHeaders( $raw, $sanitized ) {
+               $hdrs = $this->backend->extractMutableContentHeaders( $raw );
 
-               $this->assertEquals( $hdrs, $sanitized, 'sanitizeHdrsStrict() has expected result' );
+               $this->assertEquals( $hdrs, $sanitized, 'Correct extractPostableContentHeaders() result' );
        }
 
-       public static function provider_testSanitizeHdrsStrict() {
+       public static function provider_testExtractPostableContentHeaders() {
                return [
                        [
                                [
                                        'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-type' => 'image+bitmap/jpeg',
                                        'content-disposition' => 'inline',
                                        'content-duration' => 35.6363,
                                        'content-Custom' => 'hello',
                                        'x-content-custom' => 'hello'
                                ],
                                [
+                                       'content-type' => 'image+bitmap/jpeg',
                                        'content-disposition' => 'inline',
                                        'content-duration' => 35.6363,
                                        'content-custom' => 'hello',
@@ -63,13 +65,14 @@ class SwiftFileBackendTest extends MediaWikiTestCase {
                        [
                                [
                                        'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-type' => 'image+bitmap/jpeg',
                                        'content-Disposition' => 'inline; filename=xxx; ' . str_repeat( 'o', 1024 ),
                                        'content-duration' => 35.6363,
                                        'content-custom' => 'hello',
                                        'x-content-custom' => 'hello'
                                ],
                                [
+                                       'content-type' => 'image+bitmap/jpeg',
                                        'content-disposition' => 'inline;filename=xxx',
                                        'content-duration' => 35.6363,
                                        'content-custom' => 'hello',
@@ -79,13 +82,14 @@ class SwiftFileBackendTest extends MediaWikiTestCase {
                        [
                                [
                                        'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-type' => 'image+bitmap/jpeg',
                                        'content-disposition' => 'filename=' . str_repeat( 'o', 1024 ) . ';inline',
                                        'content-duration' => 35.6363,
                                        'content-custom' => 'hello',
                                        'x-content-custom' => 'hello'
                                ],
                                [
+                                       'content-type' => 'image+bitmap/jpeg',
                                        'content-disposition' => '',
                                        'content-duration' => 35.6363,
                                        'content-custom' => 'hello',
@@ -96,75 +100,11 @@ class SwiftFileBackendTest extends MediaWikiTestCase {
        }
 
        /**
-        * @dataProvider provider_testSanitizeHdrs
-        */
-       public function testSanitizeHdrs( $raw, $sanitized ) {
-               $hdrs = $this->backend->sanitizeHdrs( [ 'headers' => $raw ] );
-
-               $this->assertEquals( $hdrs, $sanitized, 'sanitizeHdrs() has expected result' );
-       }
-
-       public static function provider_testSanitizeHdrs() {
-               return [
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'inline',
-                                       'content-duration' => 35.6363,
-                                       'content-Custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'inline',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ],
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-Disposition' => 'inline; filename=xxx; ' . str_repeat( 'o', 1024 ),
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'inline;filename=xxx',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ],
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'filename=' . str_repeat( 'o', 1024 ) . ';inline',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => '',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ]
-               ];
-       }
-
-       /**
+        * @covers SwiftFileBackend::extractMetadataHeaders
         * @dataProvider provider_testGetMetadataHeaders
         */
        public function testGetMetadataHeaders( $raw, $sanitized ) {
-               $hdrs = $this->backend->getMetadataHeaders( $raw );
+               $hdrs = $this->backend->extractMetadataHeaders( $raw );
 
                $this->assertEquals( $hdrs, $sanitized, 'getMetadataHeaders() has expected result' );
        }
@@ -188,10 +128,11 @@ class SwiftFileBackendTest extends MediaWikiTestCase {
        }
 
        /**
+        * @covers SwiftFileBackend::getMetadataFromHeaders
         * @dataProvider provider_testGetMetadata
         */
        public function testGetMetadata( $raw, $sanitized ) {
-               $hdrs = $this->backend->getMetadata( $raw );
+               $hdrs = $this->backend->getMetadataFromHeaders( $raw );
 
                $this->assertEquals( $hdrs, $sanitized, 'getMetadata() has expected result' );
        }