// 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
+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).
* 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.
* 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 ===
* …
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.
&$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
* @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 ) {
/** @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;
if ( wfReadOnly() ) {
$this->mPermErrors[] = [ 'readonlytext', wfReadOnlyReason() ];
}
- $this->disabled = $this->mPermErrors != [];
+ $this->disabled = $this->mPermErrors !== [];
$this->disabledAttrib = $this->disabled
? [ 'disabled' => 'disabled' ]
: [];
new ServiceOptions(
BlockManager::$constructorOptions, $services->getMainConfig()
),
- $services->getPermissionManager()
+ $services->getPermissionManager(),
+ LoggerFactory::getInstance( 'BlockManager' )
);
},
* ]
*
* @return Status[]
+ * @suppress PhanUndeclaredProperty Status vs StatusValue
*/
public function splitByErrorType() {
list( $errorsOnlyStatus, $warningsOnlyStatus ) = parent::splitByErrorType();
* @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(
* 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
);
* @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 );
}
* @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 );
}
* @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(
$text,
$this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
$rev->getTimestamp(),
- $rev->getUserText(),
+ $revUserText,
$this->getTitle()->getTalkPage()->getFullURL()
);
}
*/
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;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\SlotRecord;
+/**
+ * @ingroup API
+ */
class ApiComparePages extends ApiBase {
/** @var RevisionStore */
);
if ( $row ) {
$rev = $this->revisionStore->newRevisionFromArchiveRow( $row );
+ // @phan-suppress-next-line PhanUndeclaredProperty
$rev->isArchive = true;
}
}
}
}
+ // @phan-suppress-next-line PhanUndeclaredProperty
if ( !empty( $rev->isArchive ) ) {
$this->getMain()->setCacheMode( 'private' );
$vals["{$prefix}archive"] = true;
* @file
*/
+use MediaWiki\MediaWikiServices;
use MediaWiki\Storage\RevisionRecord;
/**
$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();
+ }
}
}
* @file
*/
+/**
+ * @ingroup API
+ */
class ApiFormatXmlRsd extends ApiFormatXml {
public function __construct( ApiMain $main, $format ) {
parent::__construct( $main, $format );
* 'APIGetParamDescriptionMessages' hook simple.
*
* @since 1.25
+ * @ingroup API
*/
class ApiHelpParamValueMessage extends Message {
<?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
* @file
*/
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @ingroup API
+ */
class ApiImageRotate extends ApiBase {
private $mPageSet = null;
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
*
$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() );
+ }
}
}
}
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\User\UserIdentity;
use MWCryptHash;
+use Psr\Log\LoggerInterface;
use User;
use WebRequest;
use WebResponse;
* @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)
*
'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;
}
/**
$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." );
}
}
<?php
+use MediaWiki\MediaWikiServices;
use MediaWiki\Storage\RevisionRecord;
/**
* @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;
}
}
*/
public function newFromRecentChange( RecentChange $baseRC, $watched ) {
$user = $this->context->getUser();
- $counter = $baseRC->counter;
$cacheEntry = RCCacheEntry::newFromParent( $baseRC );
// 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
}
/**
- * @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
/**
* @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 ];
/**
* @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 ];
/**
* 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 );
/** @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
*
* goal.
*/
class ConsoleLogger extends AbstractLogger {
+ /** @var string */
+ private $channel;
+
/**
* @param string $channel
*/
foreach ( $chunks as $chunk ) {
$client = new SquidPurgeClient( $server );
foreach ( $chunk as $url ) {
- $client->queuePurge( $url );
+ $client->queuePurge( self::expand( $url ) );
}
$pool->addClient( $client );
}
* @param string $url
* @return string
*/
- public static function expand( $url ) {
+ private static function expand( $url ) {
return wfExpandUrl( $url, PROTO_INTERNAL );
}
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;
+ }
}
* 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 );
* @ingroup Dump
*/
class DumpMultiWriter {
+ /** @var array */
+ private $sinks;
+ /** @var int */
+ private $count;
/**
* @param array $sinks
/** @var WANObjectCache */
protected $wanCache;
+ /**
+ * @var string
+ * @protected Use $this->getName(). Public for back-compat only
+ */
+ public $name;
+
/**
* @param array|null $info
* @throws MWException
/** @var Title */
protected $title; # image title
+ /** @var bool */
+ private $exists;
+
/**
* @throws MWException
* @param Title $title
/** @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
/** @var array */
protected $mAttribs = [];
+ /** @var int */
+ protected $mPerRow;
+
+ /** @var int */
+ protected $mWidths;
+
+ /** @var int */
+ protected $mHeights;
+
/** @var array */
private static $modeMapping;
* @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 ) ) {
* @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
*
* @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;
* @ingroup SpecialPage
*/
class ImportStreamSource implements ImportSource {
+ /** @var resource */
+ private $mHandle;
+
/**
* @param resource $handle
*/
* @ingroup SpecialPage
*/
class ImportStringSource implements ImportSource {
+ /** @var string */
+ private $mString;
+
+ /** @var bool */
+ private $mRead;
+
/**
* @param string $string
*/
*/
class InstallDocFormatter {
+ /** @var string */
+ private $text;
+
public static function format( $text ) {
$obj = new self( $text );
class LocalSettingsGenerator {
protected $extensions = [];
+ protected $skins = [];
protected $values = [];
protected $groupPermissions = [];
protected $dbSettings = '';
/** @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.
* @since 1.27
*/
class ComposerInstalled {
+ /**
+ * @var array[]
+ */
+ private $contents;
/**
* @param string $location
/**
* Dependencies currently installed according to installed.json
*
- * @return array
+ * @return array[]
*/
public function getInstalledDependencies() {
$deps = [];
* @since 1.25
*/
class ComposerJson {
+ /**
+ * @var array[]
+ */
+ private $contents;
/**
* @param string $location
/**
* Dependencies as specified by composer.json
*
- * @return array
+ * @return string[]
*/
public function getRequiredDependencies() {
$deps = [];
* @since 1.25
*/
class ComposerLock {
+ /**
+ * @var array[]
+ */
+ private $contents;
/**
* @param string $location
/**
* Dependencies currently installed according to composer.lock
*
- * @return array
+ * @return array[]
*/
public function getInstalledDependencies() {
$deps = [];
}
}
- $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
+ $statuses = $this->doExecuteOpHandlesInternal( $fileOpHandles );
foreach ( $fileOpHandles as $fileOpHandle ) {
$fileOpHandle->closeResources();
}
- return $res;
+ return $statuses;
}
/**
}
/**
- * 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 ) {
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;
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']
] ];
} else {
$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
}
+
+ return SwiftFileOpHandle::CONTINUE_IF_OK;
};
$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
}
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?
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
] ];
} else {
$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
}
+
+ return SwiftFileOpHandle::CONTINUE_IF_OK;
};
$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
$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__;
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 );
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',
} 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 );
} else {
$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
}
+
+ return SwiftFileOpHandle::CONTINUE_IF_OK;
};
$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
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__;
}
// 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 */
return $hdrs;
}
- /**
- * @param FileBackendStoreOpHandle[] $fileOpHandles
- *
- * @return StatusValue[]
- */
protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
/** @var SwiftFileOpHandle[] $fileOpHandles */
'@phan-var SwiftFileOpHandle[] $fileOpHandles';
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] );
}
*/
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
* @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 ) {
* 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 ) {
* 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 ) {
*/
protected $stackDepth = 0;
+ /** @var callable|null */
+ protected $filterCallback;
+
/**
* @var array Additional parsing options
*/
/**
* $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
/**
* 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]
* @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' ] ) ) {
return $key;
}
+ /**
+ * @inheritDoc
+ */
protected function getMessageParameters() {
- if ( isset( $this->parsedParametersDeleteLog ) ) {
+ if ( $this->parsedParametersDeleteLog !== null ) {
return $this->parsedParametersDeleteLog;
}
/** @var bool */
private $actionRestrictionsEnforced = false;
+ /** @var array */
+ private $mConds;
+
+ /** @var string */
+ private $mTagFilter;
+
/** @var LogEventsList */
public $mLogEventsList;
* @param File|FSFile $image
* @param string $path
* @return DjVuImage
+ * @suppress PhanUndeclaredProperty Custom property
*/
function getDjVuImage( $image, $path ) {
if ( !$image ) {
* @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 ) ) {
*/
const DJVUTXT_MEMORY_LIMIT = 300000;
+ /** @var string */
+ private $mFilename;
+
/**
* @param string $filename The DjVu file name.
*/
}
}
+ $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;
$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;
'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(),
] + $extraParams
)
: $context->msg( 'previousrevision' )->escaped();
- $prevdiff = $prev
+ $prevdiff = $prevExist
? Linker::linkKnown(
$this->getTitle(),
$context->msg( 'diff' )->escaped(),
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 );
}
}
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" );
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
// 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
$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 );
// 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;
}
/**
- * 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() );
$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' );
$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
$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();
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 ) {
* @param LinkTarget $title
*
* @return string
+ * @suppress PhanUndeclaredProperty
*/
public function getPrefixedText( LinkTarget $title ) {
if ( !isset( $title->prefixedText ) ) {
}
}
- /**
- * 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
$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 ) );
+ }
+ }
}
} );
}
'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:
* @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 ) {
'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)
"codeCoverageIgnore=" \
"codingStandardsIgnoreEnd=" \
"codingStandardsIgnoreStart=" \
- "covers=" \
- "dataProvider=" \
- "expectedException=" \
- "expectedExceptionMessage=" \
- "group=" \
"phan=" \
"suppress="
TCL_SUBST =
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
*
* @file
* @ingroup Maintenance
+ * @phan-file-suppress PhanUndeclaredProperty Lots of custom properties
*/
require_once __DIR__ . '/Maintenance.php';
'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' );
);
$cacheKey = $cache->makeGlobalKey(
- 'BlobStore',
- 'address',
+ 'SqlBlobStore-blob',
$lb->getLocalDomainID(),
'tt:7777'
);
use MediaWiki\Block\SystemBlock;
use MediaWiki\MediaWikiServices;
use Wikimedia\TestingAccessWrapper;
+use Psr\Log\LoggerInterface;
/**
* @group Blocking
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
];
}
$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' );
+ }
}
"$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)." );
* @covers SwiftFileBackendList
*/
class SwiftFileBackendTest extends MediaWikiTestCase {
- /** @var TestingAccessWrapper Proxy to SwiftFileBackend */
+ /** @var TestingAccessWrapper|SwiftFileBackend */
private $backend;
protected function setUp() {
}
/**
- * @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',
[
[
'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',
[
[
'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',
}
/**
- * @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' );
}
}
/**
+ * @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' );
}