1<?php 2 3namespace MediaWiki\Revision; 4 5use ActorMigration; 6use ChangeTags; 7use ContribsPager; 8use FauxRequest; 9use IContextSource; 10use MediaWiki\Cache\LinkBatchFactory; 11use MediaWiki\HookContainer\HookContainer; 12use MediaWiki\Linker\LinkRendererFactory; 13use MediaWiki\Permissions\Authority; 14use MediaWiki\User\UserIdentity; 15use Message; 16use NamespaceInfo; 17use RequestContext; 18use Wikimedia\Rdbms\ILoadBalancer; 19 20/** 21 * @since 1.35 22 */ 23class ContributionsLookup { 24 25 /** @var RevisionStore */ 26 private $revisionStore; 27 28 /** @var LinkRendererFactory */ 29 private $linkRendererFactory; 30 31 /** @var LinkBatchFactory */ 32 private $linkBatchFactory; 33 34 /** @var HookContainer */ 35 private $hookContainer; 36 37 /** @var ILoadBalancer */ 38 private $loadBalancer; 39 40 /** @var ActorMigration */ 41 private $actorMigration; 42 43 /** @var NamespaceInfo */ 44 private $namespaceInfo; 45 46 /** 47 * @param RevisionStore $revisionStore 48 * @param LinkRendererFactory $linkRendererFactory 49 * @param LinkBatchFactory $linkBatchFactory 50 * @param HookContainer $hookContainer 51 * @param ILoadBalancer $loadBalancer 52 * @param ActorMigration $actorMigration 53 * @param NamespaceInfo $namespaceInfo 54 */ 55 public function __construct( 56 RevisionStore $revisionStore, 57 LinkRendererFactory $linkRendererFactory, 58 LinkBatchFactory $linkBatchFactory, 59 HookContainer $hookContainer, 60 ILoadBalancer $loadBalancer, 61 ActorMigration $actorMigration, 62 NamespaceInfo $namespaceInfo 63 ) { 64 $this->revisionStore = $revisionStore; 65 $this->linkRendererFactory = $linkRendererFactory; 66 $this->linkBatchFactory = $linkBatchFactory; 67 $this->hookContainer = $hookContainer; 68 $this->loadBalancer = $loadBalancer; 69 $this->actorMigration = $actorMigration; 70 $this->namespaceInfo = $namespaceInfo; 71 } 72 73 /** 74 * Constructs fake query parameters to be passed to ContribsPager 75 * 76 * @param int $limit Maximum number of revisions to return. 77 * @param string $segment Indicates which segment of the contributions to return. 78 * The segment should consist of 2 parts separated by a pipe character. 79 * The first part is mapped to the 'dir' parameter. 80 * The second part is mapped to the 'offset' parameter. 81 * The value for the offset is opaque and is ultimately supplied by ContribsPager::getPagingQueries(). 82 * @return array 83 */ 84 private function getPagerParams( int $limit, string $segment ): array { 85 $dir = 'next'; 86 $seg = explode( '|', $segment, 2 ); 87 if ( count( $seg ) > 1 ) { 88 if ( $seg[0] === 'after' ) { 89 $dir = 'prev'; 90 $segment = $seg[1]; 91 } elseif ( $seg[0] == 'before' ) { 92 $segment = $seg[1]; 93 } else { 94 $dir = null; 95 $segment = null; 96 } 97 } else { 98 $segment = null; 99 } 100 return [ 101 'limit' => $limit, 102 'offset' => $segment, 103 'dir' => $dir 104 ]; 105 } 106 107 /** 108 * @param UserIdentity $target the user from whom to retrieve contributions 109 * @param int $limit the maximum number of revisions to return 110 * @param Authority $performer the user used for permission checks 111 * @param string $segment 112 * @param string|null $tag 113 * 114 * @return ContributionsSegment 115 * @throws \MWException 116 */ 117 public function getContributions( 118 UserIdentity $target, 119 int $limit, 120 Authority $performer, 121 string $segment = '', 122 string $tag = null 123 ): ContributionsSegment { 124 $context = new RequestContext(); 125 $context->setAuthority( $performer ); 126 127 $paramArr = $this->getPagerParams( $limit, $segment ); 128 $context->setRequest( new FauxRequest( $paramArr ) ); 129 130 // TODO: explore moving this to factory method for testing 131 $pager = $this->getContribsPager( $context, $target, [ 132 'tagfilter' => $tag, 133 'revisionsOnly' => true 134 ] ); 135 $revisions = []; 136 $tags = []; 137 $count = 0; 138 if ( $pager->getNumRows() > 0 ) { 139 foreach ( $pager->mResult as $row ) { 140 // We retrieve and ignore one extra record to see if we are on the oldest segment. 141 if ( ++$count > $limit ) { 142 break; 143 } 144 145 // TODO: pre-load title batch? 146 $revision = $this->revisionStore->newRevisionFromRow( $row, 0 ); 147 $revisions[] = $revision; 148 if ( $row->ts_tags ) { 149 $tagNames = explode( ',', $row->ts_tags ); 150 $tags[ $row->rev_id ] = $this->getContributionTags( $tagNames ); 151 } 152 } 153 } 154 155 $deltas = $this->getContributionDeltas( $revisions ); 156 157 $flags = [ 158 'newest' => $pager->mIsFirst, 159 'oldest' => $pager->mIsLast, 160 ]; 161 162 // TODO: Make me an option in IndexPager 163 $pager->mIsFirst = false; // XXX: nasty... 164 $pagingQueries = $pager->getPagingQueries(); 165 166 $prev = $pagingQueries['prev']['offset'] ?? null; 167 $next = $pagingQueries['next']['offset'] ?? null; 168 169 $after = $prev ? 'after|' . $prev : null; // later in time 170 $before = $next ? 'before|' . $next : null; // earlier in time 171 172 // TODO: Possibly return public $pager properties to segment for populating URLS ($mIsFirst, $mIsLast) 173 // HACK: Force result set order to be descending. Sorting logic in ContribsPager::reallyDoQuery is confusing. 174 if ( $paramArr['dir'] === 'prev' ) { 175 $revisions = array_reverse( $revisions ); 176 } 177 return new ContributionsSegment( $revisions, $tags, $before, $after, $deltas, $flags ); 178 } 179 180 /** 181 * @param string[] $tagNames Array of tag names 182 * @return Message[] Associative array mapping tag name to a Message object containing the tag's display value 183 */ 184 private function getContributionTags( array $tagNames ): array { 185 $tagMetadata = []; 186 foreach ( $tagNames as $name ) { 187 $tagDisplay = ChangeTags::tagShortDescriptionMessage( $name, RequestContext::getMain() ); 188 if ( $tagDisplay ) { 189 $tagMetadata[$name] = $tagDisplay; 190 } 191 } 192 return $tagMetadata; 193 } 194 195 /** 196 * Gets size deltas of a revision and its parent revision 197 * @param RevisionRecord[] $revisions 198 * @return int[] Associative array of revision ids and their deltas. 199 * If revision is the first on a page, delta is revision size. 200 * If parent revision is unknown, delta is null. 201 */ 202 private function getContributionDeltas( $revisions ): array { 203 // SpecialContributions uses the size of the revision if the parent revision is unknown. Cases include: 204 // - revision has been deleted 205 // - parent rev id has not been populated (this is the case for very old revisions) 206 $parentIds = []; 207 foreach ( $revisions as $revision ) { 208 $revId = $revision->getId(); 209 $parentIds[$revId] = $revision->getParentId(); 210 } 211 $parentSizes = $this->revisionStore->getRevisionSizes( $parentIds ); 212 $deltas = []; 213 foreach ( $revisions as $revision ) { 214 $parentId = $revision->getParentId(); 215 if ( $parentId === 0 ) { // first revision on a page 216 $delta = $revision->getSize(); 217 } elseif ( !isset( $parentSizes[$parentId] ) ) { // parent revision is either deleted or untracked 218 $delta = null; 219 } else { 220 $delta = $revision->getSize() - $parentSizes[$parentId]; 221 } 222 $deltas[ $revision->getId() ] = $delta; 223 } 224 return $deltas; 225 } 226 227 /** 228 * Returns the number of edits by the given user. 229 * 230 * @param UserIdentity $user 231 * @param Authority $performer the user used for permission checks 232 * @param string|null $tag 233 * 234 * @return int 235 */ 236 public function getContributionCount( UserIdentity $user, Authority $performer, $tag = null ): int { 237 $context = new RequestContext(); 238 $context->setAuthority( $performer ); 239 $context->setRequest( new FauxRequest( [] ) ); 240 241 // TODO: explore moving this to factory method for testing 242 $pager = $this->getContribsPager( $context, $user, [ 243 'tagfilter' => $tag, 244 ] ); 245 246 $query = $pager->getQueryInfo(); 247 248 $count = $pager->mDb->selectField( 249 $query['tables'], 250 'COUNT(*)', 251 $query['conds'], 252 __METHOD__, 253 [], 254 $query['join_conds'] 255 ); 256 257 return (int)$count; 258 } 259 260 private function getContribsPager( 261 IContextSource $context, 262 UserIdentity $targetUser, 263 array $options 264 ) { 265 return new ContribsPager( 266 $context, 267 $options, 268 $this->linkRendererFactory->create(), 269 $this->linkBatchFactory, 270 $this->hookContainer, 271 $this->loadBalancer, 272 $this->actorMigration, 273 $this->revisionStore, 274 $this->namespaceInfo, 275 $targetUser 276 ); 277 } 278 279} 280