wiki.techinc.nl/includes/Revision/ContributionsLookup.php
Nikki Nikkhoui 268400fbde Add option to not call onContribsPager__reallyDoQuery hook
For some use cases (e.g. UserContributions endpoints (T235073) )
we do not want extensions to be able to inject their own
contributions into the result set of ContribsPager.

This is because extensions may not be adding revisions, but
other types of 'contributions'. With the hook enabled,
we are unable to reliably enforce strict chronological
ordering of contributions. (See T200259 for explanation).

Disabling the hook provides a consistent set of revisions for the
User Contribution endpoints.

Bug: T257839
Change-Id: I239395c572d4cb32a4d9ee871ffa02accfdce837
2020-07-22 18:00:51 +02:00

202 lines
5.6 KiB
PHP

<?php
namespace MediaWiki\Revision;
use ContribsPager;
use FauxRequest;
use MediaWiki\User\UserIdentity;
use RequestContext;
use User;
/**
* @since 1.35
*/
class ContributionsLookup {
/**
* @var RevisionStore
*/
private $revisionStore;
/**
* ContributionsLookup constructor.
*
* @param RevisionStore $revisionStore
*/
public function __construct( RevisionStore $revisionStore ) {
$this->revisionStore = $revisionStore;
}
/**
* Constructs fake query parameters to be passed to ContribsPager
*
* @param int $limit Maximum number of revisions to return.
* @param string $segment Indicates which segment of the contributions to return.
* The segment should consist of 2 parts separated by a pipe character.
* The first part is mapped to the 'dir' parameter.
* The second part is mapped to the 'offset' parameter.
* The value for the offset is opaque and is ultimately supplied by ContribsPager::getPagingQueries().
* @return array
*/
private function getPagerParams( int $limit, string $segment ) {
$dir = 'next';
$seg = explode( '|', $segment, 2 );
if ( count( $seg ) > 1 ) {
if ( $seg[0] === 'after' ) {
$dir = 'prev';
$segment = $seg[1];
} elseif ( $seg[0] == 'before' ) {
$dir = 'next';
$segment = $seg[1];
} else {
$dir = null;
$segment = null;
}
} else {
$segment = null;
}
return [
'limit' => $limit,
'offset' => $segment,
'dir' => $dir
];
}
/**
* @param UserIdentity $target the user from whom to retrieve contributions
* @param int $limit the maximum number of revisions to return
* @param User $performer the user used for permission checks
* @param string $segment
* @param string|null $tag
*
* @return ContributionsSegment
* @throws \MWException
*/
public function getContributions(
UserIdentity $target,
int $limit,
User $performer,
string $segment = '',
string $tag = null
): ContributionsSegment {
$context = new RequestContext();
$context->setUser( $performer );
$paramArr = $this->getPagerParams( $limit, $segment );
$context->setRequest( new FauxRequest( $paramArr ) );
// TODO: explore moving this to factory method for testing
$pager = new ContribsPager( $context, [
'target' => $target->getName(),
'tagfilter' => $tag,
'revisionsOnly' => true
] );
$revisions = [];
$tags = [];
$count = 0;
if ( $pager->getNumRows() > 0 ) {
foreach ( $pager->mResult as $row ) {
// We retrieve and ignore one extra record to see if we are on the oldest segment.
if ( ++$count > $limit ) {
break;
}
// TODO: pre-load title batch?
$revision = $this->revisionStore->newRevisionFromRow( $row, 0 );
$revisions[] = $revision;
$tags[ $row->rev_id ] =
$row->ts_tags ? explode( ',', $row->ts_tags ) : [];
}
}
$deltas = $this->getContributionDeltas( $revisions );
$flags = [
'newest' => $pager->mIsFirst,
'oldest' => $pager->mIsLast,
];
// TODO: Make me an option in IndexPager
$pager->mIsFirst = false; // XXX: nasty...
$pagingQueries = $pager->getPagingQueries();
$prev = $pagingQueries['prev']['offset'] ?? null;
$next = $pagingQueries['next']['offset'] ?? null;
$after = $prev ? 'after|' . $prev : null; // later in time
$before = $next ? 'before|' . $next : null; // earlier in time
// TODO: Possibly return public $pager properties to segment for populating URLS ($mIsFirst, $mIsLast)
// HACK: Force result set order to be descending. Sorting logic in ContribsPager::reallyDoQuery is confusing.
if ( $paramArr['dir'] === 'prev' ) {
$revisions = array_reverse( $revisions );
}
return new ContributionsSegment( $revisions, $tags, $before, $after, $deltas, $flags );
}
/**
* Gets size deltas of a revision and its parent revision
* @param RevisionRecord[] $revisions
* @return int[] Associative array of revision ids and their deltas.
* If revision is the first on a page, delta is revision size.
* If parent revision is unknown, delta is null.
*/
private function getContributionDeltas( $revisions ) {
// SpecialContributions uses the size of the revision if the parent revision is unknown. Cases include:
// - revision has been deleted
// - parent rev id has not been populated (this is the case for very old revisions)
$parentIds = [];
foreach ( $revisions as $revision ) {
$revId = $revision->getId();
$parentIds[$revId] = $revision->getParentId();
}
$parentSizes = $this->revisionStore->getRevisionSizes( $parentIds );
$deltas = [];
foreach ( $revisions as $revision ) {
$parentId = $revision->getParentId();
if ( $parentId === 0 ) { // first revision on a page
$delta = $revision->getSize();
} elseif ( !isset( $parentSizes[$parentId] ) ) { // parent revision is either deleted or untracked
$delta = null;
} else {
$delta = $revision->getSize() - $parentSizes[$parentId];
}
$deltas[ $revision->getId() ] = $delta;
}
return $deltas;
}
/**
* Returns the number of edits by the given user.
*
* @param UserIdentity $user
* @param User $performer the user used for permission checks
* @param string|null $tag
*
* @return int
*/
public function getContributionCount( UserIdentity $user, User $performer, $tag = null ): int {
$context = new RequestContext();
$context->setUser( $performer );
$context->setRequest( new FauxRequest( [] ) );
// TODO: explore moving this to factory method for testing
$pager = new ContribsPager( $context, [
'target' => $user->getName(),
'tagfilter' => $tag,
] );
$query = $pager->getQueryInfo();
$count = $pager->mDb->selectField(
$query['tables'],
'COUNT(*)',
$query['conds'],
__METHOD__,
[],
$query['join_conds']
);
return (int)$count;
}
}