1<?php 2 3namespace MediaWiki\Revision; 4 5use ContribsPager; 6use FauxRequest; 7use MediaWiki\User\UserIdentity; 8use RequestContext; 9use User; 10 11/** 12 * @since 1.35 13 */ 14class ContributionsLookup { 15 16 /** 17 * @var RevisionStore 18 */ 19 private $revisionStore; 20 21 /** 22 * ContributionsLookup constructor. 23 * 24 * @param RevisionStore $revisionStore 25 */ 26 public function __construct( RevisionStore $revisionStore ) { 27 $this->revisionStore = $revisionStore; 28 } 29 30 /** 31 * Constructs fake query parameters to be passed to ContribsPager 32 * 33 * @param int $limit Maximum number of revisions to return. 34 * @param string $segment Indicates which segment of the contributions to return. 35 * The segment should consist of 2 parts separated by a pipe character. 36 * The first part is mapped to the 'dir' parameter. 37 * The second part is mapped to the 'offset' parameter. 38 * The value for the offset is opaque and is ultimately supplied by ContribsPager::getPagingQueries(). 39 * @return array 40 */ 41 private function getPagerParams( int $limit, string $segment ) { 42 $dir = 'next'; 43 $seg = explode( '|', $segment, 2 ); 44 if ( count( $seg ) > 1 ) { 45 if ( $seg[0] === 'after' ) { 46 $dir = 'prev'; 47 $segment = $seg[1]; 48 } elseif ( $seg[0] == 'before' ) { 49 $dir = 'next'; 50 $segment = $seg[1]; 51 } else { 52 $dir = null; 53 $segment = null; 54 } 55 } else { 56 $segment = null; 57 } 58 return [ 59 'limit' => $limit, 60 'offset' => $segment, 61 'dir' => $dir 62 ]; 63 } 64 65 /** 66 * @param UserIdentity $target the user from whom to retrieve contributions 67 * @param int $limit the maximum number of revisions to return 68 * @param User $performer the user used for permission checks 69 * @param string $segment 70 * @param string|null $tag 71 * 72 * @return ContributionsSegment 73 * @throws \MWException 74 */ 75 public function getContributions( 76 UserIdentity $target, 77 int $limit, 78 User $performer, 79 string $segment = '', 80 string $tag = null 81 ): ContributionsSegment { 82 $context = new RequestContext(); 83 $context->setUser( $performer ); 84 85 $paramArr = $this->getPagerParams( $limit, $segment ); 86 $context->setRequest( new FauxRequest( $paramArr ) ); 87 88 // TODO: explore moving this to factory method for testing 89 $pager = new ContribsPager( $context, [ 90 'target' => $target->getName(), 91 'tagfilter' => $tag, 92 ] ); 93 $revisions = []; 94 $tags = []; 95 $count = 0; 96 if ( $pager->getNumRows() > 0 ) { 97 foreach ( $pager->mResult as $row ) { 98 // We retrieve and ignore one extra record to see if we are on the oldest segment. 99 if ( ++$count > $limit ) { 100 break; 101 } 102 103 // TODO: pre-load title batch? 104 $revision = $this->revisionStore->newRevisionFromRow( $row, 0 ); 105 $revisions[] = $revision; 106 $tags[ $row->rev_id ] = 107 $row->ts_tags ? explode( ',', $row->ts_tags ) : []; 108 } 109 } 110 111 $deltas = $this->getContributionDeltas( $revisions ); 112 113 $flags = [ 114 'newest' => $pager->mIsFirst, 115 'oldest' => $pager->mIsLast, 116 ]; 117 118 // TODO: Make me an option in IndexPager 119 $pager->mIsFirst = false; // XXX: nasty... 120 $pagingQueries = $pager->getPagingQueries(); 121 122 $prev = $pagingQueries['prev']['offset'] ?? null; 123 $next = $pagingQueries['next']['offset'] ?? null; 124 125 $after = $prev ? 'after|' . $prev : null; // later in time 126 $before = $next ? 'before|' . $next : null; // earlier in time 127 128 // TODO: Possibly return public $pager properties to segment for populating URLS ($mIsFirst, $mIsLast) 129 // HACK: Force result set order to be descending. Sorting logic in ContribsPager::reallyDoQuery is confusing. 130 if ( $paramArr['dir'] === 'prev' ) { 131 $revisions = array_reverse( $revisions ); 132 } 133 return new ContributionsSegment( $revisions, $tags, $before, $after, $deltas, $flags ); 134 } 135 136 /** 137 * Gets size deltas of a revision and its parent revision 138 * @param RevisionRecord[] $revisions 139 * @return int[] Associative array of revision ids and their deltas. 140 * If revision is the first on a page, delta is revision size. 141 * If parent revision is unknown, delta is null. 142 */ 143 private function getContributionDeltas( $revisions ) { 144 // SpecialContributions uses the size of the revision if the parent revision is unknown. Cases include: 145 // - revision has been deleted 146 // - parent rev id has not been populated (this is the case for very old revisions) 147 $parentIds = []; 148 foreach ( $revisions as $revision ) { 149 $revId = $revision->getId(); 150 $parentIds[$revId] = $revision->getParentId(); 151 } 152 $parentSizes = $this->revisionStore->getRevisionSizes( $parentIds ); 153 $deltas = []; 154 foreach ( $revisions as $revision ) { 155 $parentId = $revision->getParentId(); 156 if ( $parentId === 0 ) { // first revision on a page 157 $delta = $revision->getSize(); 158 } elseif ( !isset( $parentSizes[$parentId] ) ) { // parent revision is either deleted or untracked 159 $delta = null; 160 } else { 161 $delta = $revision->getSize() - $parentSizes[$parentId]; 162 } 163 $deltas[ $revision->getId() ] = $delta; 164 } 165 return $deltas; 166 } 167 168 /** 169 * Returns the number of edits by the given user. 170 * 171 * @param UserIdentity $user 172 * @param User $performer the user used for permission checks 173 * @param string|null $tag 174 * 175 * @return int 176 */ 177 public function getContributionCount( UserIdentity $user, User $performer, $tag = null ): int { 178 $context = new RequestContext(); 179 $context->setUser( $performer ); 180 $context->setRequest( new FauxRequest( [] ) ); 181 182 // TODO: explore moving this to factory method for testing 183 $pager = new ContribsPager( $context, [ 184 'target' => $user->getName(), 185 'tagfilter' => $tag, 186 ] ); 187 188 $query = $pager->getQueryInfo(); 189 190 $count = $pager->mDb->selectField( 191 $query['tables'], 192 'COUNT(*)', 193 $query['conds'], 194 __METHOD__, 195 [], 196 $query['join_conds'] 197 ); 198 199 // FIXME: this count does not include contributions that extensions would be injecting 200 // via the ContribsPager__reallyDoQuery. 201 202 return (int)$count; 203 } 204} 205