Based on the patch that introduces manual revert detection, this is to create a software-defined change tag that will be applied to all edits that restore a page to an exact previous state. This is also for consistency with mw-undo and mw-rollback that will make a "reverted" tag appear on reverted edits in the future. Note about the REST API tests: in the next patch in this chain I encountered even more issues with comparing returned changed tags with expectations, so I decided it'd better if we just checked the change tags applied manually in these tests. Otherwise we can run into nasty race conditions, as the reverted tag is processed after sending the response to client in the deferred update. It also makes the test hard to maintain. Bug: T256001 Change-Id: Ic367886f39faedcb823222b7d63bf4d5cb236ae9
694 lines
19 KiB
PHP
694 lines
19 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Rest\Handler;
|
|
|
|
use MediaWiki\Permissions\PermissionManager;
|
|
use MediaWiki\Rest\LocalizedHttpException;
|
|
use MediaWiki\Rest\Response;
|
|
use MediaWiki\Rest\SimpleHandler;
|
|
use MediaWiki\Revision\RevisionRecord;
|
|
use MediaWiki\Revision\RevisionStore;
|
|
use MediaWiki\Storage\NameTableAccessException;
|
|
use MediaWiki\Storage\NameTableStore;
|
|
use MediaWiki\Storage\NameTableStoreFactory;
|
|
use RequestContext;
|
|
use Title;
|
|
use User;
|
|
use WANObjectCache;
|
|
use Wikimedia\Message\MessageValue;
|
|
use Wikimedia\Message\ParamType;
|
|
use Wikimedia\Message\ScalarParam;
|
|
use Wikimedia\ParamValidator\ParamValidator;
|
|
use Wikimedia\Rdbms\ILoadBalancer;
|
|
|
|
/**
|
|
* Handler class for Core REST API endpoints that perform operations on revisions
|
|
*/
|
|
class PageHistoryCountHandler extends SimpleHandler {
|
|
/** The maximum number of counts to return per type of revision */
|
|
private const COUNT_LIMITS = [
|
|
'anonymous' => 10000,
|
|
'bot' => 10000,
|
|
'editors' => 25000,
|
|
'edits' => 30000,
|
|
'minor' => 1000,
|
|
'reverted' => 30000
|
|
];
|
|
|
|
private const DEPRECATED_COUNT_TYPES = [
|
|
'anonedits' => 'anonymous',
|
|
'botedits' => 'bot',
|
|
'revertededits' => 'reverted'
|
|
];
|
|
|
|
private const MAX_AGE_200 = 60;
|
|
|
|
private const REVERTED_TAG_NAMES = [ 'mw-undo', 'mw-rollback', 'mw-manual-revert' ];
|
|
|
|
/** @var RevisionStore */
|
|
private $revisionStore;
|
|
|
|
/** @var NameTableStore */
|
|
private $changeTagDefStore;
|
|
|
|
/** @var PermissionManager */
|
|
private $permissionManager;
|
|
|
|
/** @var ILoadBalancer */
|
|
private $loadBalancer;
|
|
|
|
/** @var WANObjectCache */
|
|
private $cache;
|
|
|
|
/** @var User */
|
|
private $user;
|
|
|
|
/** @var RevisionRecord|bool */
|
|
private $revision;
|
|
|
|
/** @var array */
|
|
private $lastModifiedTimes;
|
|
|
|
/** @var Title */
|
|
private $titleObject;
|
|
|
|
/**
|
|
* @param RevisionStore $revisionStore
|
|
* @param NameTableStoreFactory $nameTableStoreFactory
|
|
* @param PermissionManager $permissionManager
|
|
* @param ILoadBalancer $loadBalancer
|
|
* @param WANObjectCache $cache
|
|
*/
|
|
public function __construct(
|
|
RevisionStore $revisionStore,
|
|
NameTableStoreFactory $nameTableStoreFactory,
|
|
PermissionManager $permissionManager,
|
|
ILoadBalancer $loadBalancer,
|
|
WANObjectCache $cache
|
|
) {
|
|
$this->revisionStore = $revisionStore;
|
|
$this->changeTagDefStore = $nameTableStoreFactory->getChangeTagDef();
|
|
$this->permissionManager = $permissionManager;
|
|
$this->loadBalancer = $loadBalancer;
|
|
$this->cache = $cache;
|
|
|
|
// @todo Inject this, when there is a good way to do that
|
|
$this->user = RequestContext::getMain()->getUser();
|
|
}
|
|
|
|
private function normalizeType( $type ) {
|
|
return self::DEPRECATED_COUNT_TYPES[$type] ?? $type;
|
|
}
|
|
|
|
/**
|
|
* Validates that the provided parameter combination is supported.
|
|
*
|
|
* @param string $type
|
|
* @throws LocalizedHttpException
|
|
*/
|
|
private function validateParameterCombination( $type ) {
|
|
$params = $this->getValidatedParams();
|
|
if ( !$params ) {
|
|
return;
|
|
}
|
|
|
|
if ( $params['from'] || $params['to'] ) {
|
|
if ( $type === 'edits' || $type === 'editors' ) {
|
|
if ( !$params['from'] || !$params['to'] ) {
|
|
throw new LocalizedHttpException(
|
|
new MessageValue( 'rest-pagehistorycount-parameters-invalid' ),
|
|
400
|
|
);
|
|
}
|
|
} else {
|
|
throw new LocalizedHttpException(
|
|
new MessageValue( 'rest-pagehistorycount-parameters-invalid' ),
|
|
400
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Title $title the title of the page to load history for
|
|
* @param string $type the validated count type
|
|
* @return Response
|
|
* @throws LocalizedHttpException
|
|
*/
|
|
public function run( $title, $type ) {
|
|
$normalizedType = $this->normalizeType( $type );
|
|
$this->validateParameterCombination( $normalizedType );
|
|
$titleObj = $this->getTitle();
|
|
if ( !$titleObj || !$titleObj->getArticleID() ) {
|
|
throw new LocalizedHttpException(
|
|
new MessageValue( 'rest-nonexistent-title',
|
|
[ new ScalarParam( ParamType::PLAINTEXT, $title ) ]
|
|
),
|
|
404
|
|
);
|
|
}
|
|
|
|
if ( !$this->permissionManager->userCan( 'read', $this->user, $titleObj ) ) {
|
|
throw new LocalizedHttpException(
|
|
new MessageValue( 'rest-permission-denied-title',
|
|
[ new ScalarParam( ParamType::PLAINTEXT, $title ) ]
|
|
),
|
|
403
|
|
);
|
|
}
|
|
|
|
$count = $this->getCount( $normalizedType );
|
|
$countLimit = self::COUNT_LIMITS[$normalizedType];
|
|
$response = $this->getResponseFactory()->createJson( [
|
|
'count' => $count > $countLimit ? $countLimit : $count,
|
|
'limit' => $count > $countLimit
|
|
] );
|
|
$response->setHeader( 'Cache-Control', 'max-age=' . self::MAX_AGE_200 );
|
|
|
|
// Inform clients who use a deprecated "type" value, so they can adjust
|
|
if ( isset( self::DEPRECATED_COUNT_TYPES[$type] ) ) {
|
|
$docs = '<https://www.mediawiki.org/wiki/API:REST/History_API' .
|
|
'#Get_page_history_counts>; rel="deprecation"';
|
|
$response->setHeader( 'Deprecation', 'version="v1"' );
|
|
$response->setHeader( 'Link', $docs );
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* @param string $type the validated count type
|
|
* @return int the article count
|
|
* @throws LocalizedHttpException
|
|
*/
|
|
private function getCount( $type ) {
|
|
$pageId = $this->getTitle()->getArticleID();
|
|
switch ( $type ) {
|
|
case 'anonymous':
|
|
return $this->getCachedCount( $type,
|
|
function ( RevisionRecord $fromRev = null ) use ( $pageId ) {
|
|
return $this->getAnonCount( $pageId, $fromRev );
|
|
}
|
|
);
|
|
|
|
case 'bot':
|
|
return $this->getCachedCount( $type,
|
|
function ( RevisionRecord $fromRev = null ) use ( $pageId ) {
|
|
return $this->getBotCount( $pageId, $fromRev );
|
|
}
|
|
);
|
|
|
|
case 'editors':
|
|
$from = $this->getValidatedParams()['from'] ?? null;
|
|
$to = $this->getValidatedParams()['to'] ?? null;
|
|
if ( $from || $to ) {
|
|
return $this->getEditorsCount(
|
|
$pageId,
|
|
$from ? $this->getRevisionOrThrow( $from ) : null,
|
|
$to ? $this->getRevisionOrThrow( $to ) : null
|
|
);
|
|
} else {
|
|
return $this->getCachedCount( $type,
|
|
function ( RevisionRecord $fromRev = null ) use ( $pageId ) {
|
|
return $this->getEditorsCount( $pageId, $fromRev );
|
|
} );
|
|
}
|
|
|
|
case 'edits':
|
|
$from = $this->getValidatedParams()['from'] ?? null;
|
|
$to = $this->getValidatedParams()['to'] ?? null;
|
|
if ( $from || $to ) {
|
|
return $this->getEditsCount(
|
|
$pageId,
|
|
$from ? $this->getRevisionOrThrow( $from ) : null,
|
|
$to ? $this->getRevisionOrThrow( $to ) : null
|
|
);
|
|
} else {
|
|
return $this->getCachedCount( $type,
|
|
function ( RevisionRecord $fromRev = null ) use ( $pageId ) {
|
|
return $this->getEditsCount( $pageId, $fromRev );
|
|
}
|
|
);
|
|
}
|
|
|
|
case 'reverted':
|
|
return $this->getCachedCount( $type,
|
|
function ( RevisionRecord $fromRev = null ) use ( $pageId ) {
|
|
return $this->getRevertedCount( $pageId, $fromRev );
|
|
}
|
|
);
|
|
|
|
case 'minor':
|
|
// The query for minor counts is inefficient for the database for pages with many revisions.
|
|
// If the specified title contains more revisions than allowed, we will return an error.
|
|
$editsCount = $this->getCachedCount( 'edits',
|
|
function ( RevisionRecord $fromRev = null ) use ( $pageId ) {
|
|
return $this->getEditsCount( $pageId, $fromRev );
|
|
}
|
|
);
|
|
if ( $editsCount > self::COUNT_LIMITS[$type] * 2 ) {
|
|
throw new LocalizedHttpException(
|
|
new MessageValue( 'rest-pagehistorycount-too-many-revisions' ),
|
|
500
|
|
);
|
|
}
|
|
return $this->getCachedCount( $type,
|
|
function ( RevisionRecord $fromRev = null ) use ( $pageId ) {
|
|
return $this->getMinorCount( $pageId, $fromRev );
|
|
}
|
|
);
|
|
|
|
// Sanity check
|
|
default:
|
|
throw new LocalizedHttpException(
|
|
new MessageValue( 'rest-pagehistorycount-type-unrecognized',
|
|
[ new ScalarParam( ParamType::PLAINTEXT, $type ) ]
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return RevisionRecord|bool current revision or false if unable to retrieve revision
|
|
*/
|
|
private function getCurrentRevision() {
|
|
if ( $this->revision === null ) {
|
|
$title = $this->getTitle();
|
|
if ( $title && $title->getArticleID() ) {
|
|
$this->revision = $this->revisionStore->getKnownCurrentRevision( $title );
|
|
} else {
|
|
$this->revision = false;
|
|
}
|
|
}
|
|
return $this->revision;
|
|
}
|
|
|
|
/**
|
|
* @return Title|bool Title or false if unable to retrieve title
|
|
*/
|
|
private function getTitle() {
|
|
if ( $this->titleObject === null ) {
|
|
$this->titleObject = Title::newFromText( $this->getValidatedParams()['title'] );
|
|
}
|
|
return $this->titleObject;
|
|
}
|
|
|
|
/**
|
|
* Returns latest of 2 timestamps:
|
|
* 1. Current revision
|
|
* 2. OR entry from the DB logging table for the given page
|
|
* @return int|null
|
|
*/
|
|
protected function getLastModified() {
|
|
$lastModifiedTimes = $this->getLastModifiedTimes();
|
|
if ( $lastModifiedTimes ) {
|
|
return max( array_values( $lastModifiedTimes ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns array with 2 timestamps:
|
|
* 1. Current revision
|
|
* 2. OR entry from the DB logging table for the given page
|
|
* @return array
|
|
*/
|
|
protected function getLastModifiedTimes() {
|
|
$currentRev = $this->getCurrentRevision();
|
|
if ( !$currentRev ) {
|
|
return null;
|
|
}
|
|
if ( $this->lastModifiedTimes === null ) {
|
|
$currentRevTime = (int)wfTimestampOrNull( TS_UNIX, $currentRev->getTimestamp() );
|
|
$loggingTableTime = $this->loggingTableTime( $currentRev->getPageId() );
|
|
$this->lastModifiedTimes = [
|
|
'currentRevTS' => $currentRevTime,
|
|
'dependencyModTS' => $loggingTableTime
|
|
];
|
|
}
|
|
return $this->lastModifiedTimes;
|
|
}
|
|
|
|
/**
|
|
* Return timestamp of latest entry in logging table for given page id
|
|
* @param int $pageId
|
|
* @return int|null
|
|
*/
|
|
private function loggingTableTime( $pageId ) {
|
|
$res = $this->loadBalancer->getConnectionRef( DB_REPLICA )->selectField(
|
|
'logging',
|
|
'MAX(log_timestamp)',
|
|
[ 'log_page' => $pageId ],
|
|
__METHOD__
|
|
);
|
|
return $res ? (int)wfTimestamp( TS_UNIX, $res ) : null;
|
|
}
|
|
|
|
/**
|
|
* Choosing to not implement etags in this handler.
|
|
* Generating an etag when getting revision counts must account for things like visibility settings
|
|
* (e.g. rev_deleted bit) which requires hitting the database anyway. The response for these
|
|
* requests are so small that we wouldn't be gaining much efficiency.
|
|
* Etags are strong validators and if provided would take precendence over
|
|
* last modified time, a weak validator. We want to ensure only last modified time is used
|
|
* since it is more efficient than using etags for this particular case.
|
|
* @return null
|
|
*/
|
|
protected function getEtag() {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param callable $fetchCount
|
|
* @return int
|
|
*/
|
|
private function getCachedCount( $type,
|
|
callable $fetchCount
|
|
) {
|
|
$titleObj = $this->getTitle();
|
|
$pageId = $titleObj->getArticleID();
|
|
return $this->cache->getWithSetCallback(
|
|
$this->cache->makeKey( 'rest', 'pagehistorycount', $pageId, $type ),
|
|
WANObjectCache::TTL_WEEK,
|
|
function ( $oldValue ) use ( $fetchCount ) {
|
|
$currentRev = $this->getCurrentRevision();
|
|
if ( $oldValue ) {
|
|
// Last modified timestamp was NOT a dependency change (e.g. revdel)
|
|
$doIncrementalUpdate = (
|
|
$this->getLastModified() != $this->getLastModifiedTimes()['dependencyModTS']
|
|
);
|
|
if ( $doIncrementalUpdate ) {
|
|
$rev = $this->revisionStore->getRevisionById( $oldValue['revision'] );
|
|
if ( $rev ) {
|
|
$additionalCount = $fetchCount( $rev );
|
|
return [
|
|
'revision' => $currentRev->getId(),
|
|
'count' => $oldValue['count'] + $additionalCount,
|
|
'dependencyModTS' => $this->getLastModifiedTimes()['dependencyModTS']
|
|
];
|
|
}
|
|
}
|
|
}
|
|
// Nothing was previously stored, or incremental update was done for too long,
|
|
// recalculate from scratch.
|
|
return [
|
|
'revision' => $currentRev->getId(),
|
|
'count' => $fetchCount(),
|
|
'dependencyModTS' => $this->getLastModifiedTimes()['dependencyModTS']
|
|
];
|
|
},
|
|
[
|
|
'touchedCallback' => function (){
|
|
return $this->getLastModified();
|
|
},
|
|
'version' => 2,
|
|
'lockTSE' => WANObjectCache::TTL_MINUTE * 5
|
|
]
|
|
)['count'];
|
|
}
|
|
|
|
/**
|
|
* @param int $pageId the id of the page to load history for
|
|
* @param RevisionRecord|null $fromRev
|
|
* @return int the count
|
|
*/
|
|
protected function getAnonCount( $pageId, RevisionRecord $fromRev = null ) {
|
|
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
|
|
|
|
$cond = [
|
|
'rev_page' => $pageId,
|
|
'actor_user IS NULL',
|
|
$dbr->bitAnd( 'rev_deleted',
|
|
RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_USER ) . " = 0"
|
|
];
|
|
|
|
if ( $fromRev ) {
|
|
$oldTs = $dbr->addQuotes( $dbr->timestamp( $fromRev->getTimestamp() ) );
|
|
$cond[] = "(rev_timestamp = {$oldTs} AND rev_id > {$fromRev->getId()}) " .
|
|
"OR rev_timestamp > {$oldTs}";
|
|
}
|
|
|
|
$edits = $dbr->selectRowCount(
|
|
[
|
|
'revision_actor_temp',
|
|
'revision',
|
|
'actor'
|
|
],
|
|
'1',
|
|
$cond,
|
|
__METHOD__,
|
|
[ 'LIMIT' => self::COUNT_LIMITS['anonymous'] + 1 ], // extra to detect truncation
|
|
[
|
|
'revision' => [
|
|
'JOIN',
|
|
'revactor_rev = rev_id AND revactor_page = rev_page'
|
|
],
|
|
'actor' => [
|
|
'JOIN',
|
|
'revactor_actor = actor_id'
|
|
]
|
|
]
|
|
);
|
|
return $edits;
|
|
}
|
|
|
|
/**
|
|
* @param int $pageId the id of the page to load history for
|
|
* @param RevisionRecord|null $fromRev
|
|
* @return int the count
|
|
*/
|
|
protected function getBotCount( $pageId, RevisionRecord $fromRev = null ) {
|
|
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
|
|
|
|
$cond = [
|
|
'rev_page=' . intval( $pageId ),
|
|
$dbr->bitAnd( 'rev_deleted',
|
|
RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_USER ) . " = 0",
|
|
'EXISTS(' .
|
|
$dbr->selectSQLText(
|
|
'user_groups',
|
|
'1',
|
|
[
|
|
'actor.actor_user = ug_user',
|
|
'ug_group' => $this->permissionManager->getGroupsWithPermission( 'bot' ),
|
|
'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
|
|
],
|
|
__METHOD__
|
|
) .
|
|
')'
|
|
];
|
|
if ( $fromRev ) {
|
|
$oldTs = $dbr->addQuotes( $dbr->timestamp( $fromRev->getTimestamp() ) );
|
|
$cond[] = "(rev_timestamp = {$oldTs} AND rev_id > {$fromRev->getId()}) " .
|
|
"OR rev_timestamp > {$oldTs}";
|
|
}
|
|
|
|
$edits = $dbr->selectRowCount(
|
|
[
|
|
'revision_actor_temp',
|
|
'revision',
|
|
'actor',
|
|
],
|
|
'1',
|
|
$cond,
|
|
__METHOD__,
|
|
[ 'LIMIT' => self::COUNT_LIMITS['bot'] + 1 ], // extra to detect truncation
|
|
[
|
|
'revision' => [
|
|
'JOIN',
|
|
'revactor_rev = rev_id AND revactor_page = rev_page'
|
|
],
|
|
'actor' => [
|
|
'JOIN',
|
|
'revactor_actor = actor_id'
|
|
],
|
|
]
|
|
);
|
|
return $edits;
|
|
}
|
|
|
|
/**
|
|
* @param int $pageId the id of the page to load history for
|
|
* @param RevisionRecord|null $fromRev
|
|
* @param RevisionRecord|null $toRev
|
|
* @return int the count
|
|
*/
|
|
protected function getEditorsCount( $pageId,
|
|
RevisionRecord $fromRev = null,
|
|
RevisionRecord $toRev = null
|
|
) {
|
|
list( $fromRev, $toRev ) = $this->orderRevisions( $fromRev, $toRev );
|
|
return $this->revisionStore->countAuthorsBetween( $pageId, $fromRev,
|
|
$toRev, $this->user, self::COUNT_LIMITS['editors'] );
|
|
}
|
|
|
|
/**
|
|
* @param int $pageId the id of the page to load history for
|
|
* @param RevisionRecord|null $fromRev
|
|
* @return int the count
|
|
*/
|
|
protected function getRevertedCount( $pageId, RevisionRecord $fromRev = null ) {
|
|
$tagIds = [];
|
|
|
|
foreach ( self::REVERTED_TAG_NAMES as $tagName ) {
|
|
try {
|
|
$tagIds[] = $this->changeTagDefStore->getId( $tagName );
|
|
} catch ( NameTableAccessException $e ) {
|
|
// If no revisions are tagged with a name, no tag id will be present
|
|
}
|
|
}
|
|
if ( !$tagIds ) {
|
|
return 0;
|
|
}
|
|
|
|
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
|
|
|
|
$cond = [
|
|
'rev_page' => $pageId,
|
|
$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
|
|
];
|
|
if ( $fromRev ) {
|
|
$oldTs = $dbr->addQuotes( $dbr->timestamp( $fromRev->getTimestamp() ) );
|
|
$cond[] = "(rev_timestamp = {$oldTs} AND rev_id > {$fromRev->getId()}) " .
|
|
"OR rev_timestamp > {$oldTs}";
|
|
}
|
|
$edits = $dbr->selectRowCount(
|
|
[
|
|
'revision',
|
|
'change_tag'
|
|
],
|
|
'1',
|
|
[ 'rev_page' => $pageId ],
|
|
__METHOD__,
|
|
[
|
|
'LIMIT' => self::COUNT_LIMITS['reverted'] + 1, // extra to detect truncation
|
|
'GROUP BY' => 'rev_id'
|
|
],
|
|
[
|
|
'change_tag' => [
|
|
'JOIN',
|
|
[
|
|
'ct_rev_id = rev_id',
|
|
'ct_tag_id' => $tagIds,
|
|
]
|
|
],
|
|
]
|
|
);
|
|
return $edits;
|
|
}
|
|
|
|
/**
|
|
* @param int $pageId the id of the page to load history for
|
|
* @param RevisionRecord|null $fromRev
|
|
* @return int the count
|
|
*/
|
|
protected function getMinorCount( $pageId, RevisionRecord $fromRev = null ) {
|
|
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
|
|
$cond = [
|
|
'rev_page' => $pageId,
|
|
'rev_minor_edit != 0',
|
|
$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
|
|
];
|
|
if ( $fromRev ) {
|
|
$oldTs = $dbr->addQuotes( $dbr->timestamp( $fromRev->getTimestamp() ) );
|
|
$cond[] = "(rev_timestamp = {$oldTs} AND rev_id > {$fromRev->getId()}) " .
|
|
"OR rev_timestamp > {$oldTs}";
|
|
}
|
|
$edits = $dbr->selectRowCount( 'revision', '1',
|
|
$cond,
|
|
__METHOD__,
|
|
[ 'LIMIT' => self::COUNT_LIMITS['minor'] + 1 ] // extra to detect truncation
|
|
);
|
|
|
|
return $edits;
|
|
}
|
|
|
|
/**
|
|
* @param int $pageId the id of the page to load history for
|
|
* @param RevisionRecord|null $fromRev
|
|
* @param RevisionRecord|null $toRev
|
|
* @return int the count
|
|
*/
|
|
protected function getEditsCount(
|
|
$pageId,
|
|
RevisionRecord $fromRev = null,
|
|
RevisionRecord $toRev = null
|
|
) {
|
|
list( $fromRev, $toRev ) = $this->orderRevisions( $fromRev, $toRev );
|
|
return $this->revisionStore->countRevisionsBetween(
|
|
$pageId,
|
|
$fromRev,
|
|
$toRev,
|
|
self::COUNT_LIMITS['edits'] // Will be increased by 1 to detect truncation
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param int $revId
|
|
* @return RevisionRecord
|
|
* @throws LocalizedHttpException
|
|
*/
|
|
private function getRevisionOrThrow( $revId ) {
|
|
$rev = $this->revisionStore->getRevisionById( $revId );
|
|
if ( !$rev ) {
|
|
throw new LocalizedHttpException(
|
|
new MessageValue( 'rest-nonexistent-revision', [ $revId ] ),
|
|
404
|
|
);
|
|
}
|
|
return $rev;
|
|
}
|
|
|
|
/**
|
|
* Reorders revisions if they are present
|
|
* @param RevisionRecord|null $fromRev
|
|
* @param RevisionRecord|null $toRev
|
|
* @return array
|
|
* @phan-return array{0:RevisionRecord|null,1:RevisionRecord|null}
|
|
*/
|
|
private function orderRevisions(
|
|
RevisionRecord $fromRev = null,
|
|
RevisionRecord $toRev = null
|
|
) {
|
|
if ( $fromRev && $toRev && ( $fromRev->getTimestamp() > $toRev->getTimestamp() ||
|
|
( $fromRev->getTimestamp() === $toRev->getTimestamp()
|
|
&& $fromRev->getId() > $toRev->getId() ) )
|
|
) {
|
|
return [ $toRev, $fromRev ];
|
|
}
|
|
return [ $fromRev, $toRev ];
|
|
}
|
|
|
|
public function needsWriteAccess() {
|
|
return false;
|
|
}
|
|
|
|
public function getParamSettings() {
|
|
return [
|
|
'title' => [
|
|
self::PARAM_SOURCE => 'path',
|
|
ParamValidator::PARAM_TYPE => 'string',
|
|
ParamValidator::PARAM_REQUIRED => true,
|
|
],
|
|
'type' => [
|
|
self::PARAM_SOURCE => 'path',
|
|
ParamValidator::PARAM_TYPE => array_merge(
|
|
array_keys( self::COUNT_LIMITS ),
|
|
array_keys( self::DEPRECATED_COUNT_TYPES )
|
|
),
|
|
ParamValidator::PARAM_REQUIRED => true,
|
|
],
|
|
'from' => [
|
|
self::PARAM_SOURCE => 'query',
|
|
ParamValidator::PARAM_TYPE => 'integer',
|
|
ParamValidator::PARAM_REQUIRED => false
|
|
],
|
|
'to' => [
|
|
self::PARAM_SOURCE => 'query',
|
|
ParamValidator::PARAM_TYPE => 'integer',
|
|
ParamValidator::PARAM_REQUIRED => false
|
|
]
|
|
];
|
|
}
|
|
}
|