171 $services = MediaWikiServices::getInstance();
172 $loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
178 $this->target = $options[
'target'] ?? $targetUser->
getName();
179 $this->targetUser = $targetUser;
185 $this->target = $options[
'target'] ??
'';
187 $this->targetUser = $services->getUserFactory()->newFromName(
188 $this->target, UserRigorOptions::RIGOR_NONE
190 if ( !$this->targetUser ) {
194 throw new InvalidArgumentException( __METHOD__ .
': the user name is too ' .
195 'broken to use even with validation disabled.' );
199 $this->
namespace = $options['namespace'] ?? '';
200 $this->tagFilter = $options[
'tagfilter'] ??
false;
201 $this->nsInvert = $options[
'nsInvert'] ??
false;
202 $this->associated = $options[
'associated'] ??
false;
204 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
205 $this->topOnly = !empty( $options[
'topOnly'] );
206 $this->newOnly = !empty( $options[
'newOnly'] );
207 $this->hideMinor = !empty( $options[
'hideMinor'] );
208 $this->revisionsOnly = !empty( $options[
'revisionsOnly'] );
213 $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA,
'contributions' );
215 $this->actorMigration = $actorMigration ?? $services->getActorMigration();
216 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
225 foreach ( $msgs as $msg ) {
226 $this->messages[$msg] = $this->
msg( $msg )->escaped();
230 $startTimestamp =
'';
232 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
233 $startTimestamp = $options[
'start'] .
' 00:00:00';
235 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
236 $endTimestamp = $options[
'end'] .
' 23:59:59';
241 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
242 $this->hookRunner =
new HookRunner( $hookContainer ?? $services->getHookContainer() );
243 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
244 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
245 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
265 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->
buildQueryInfo(
271 $options[
'MAX_EXECUTION_TIME'] =
272 $this->
getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
292 $data = [
$dbr->select(
293 $tables, $fields, $conds, $fname, $options, $join_conds
295 if ( !$this->revisionsOnly ) {
296 $this->hookRunner->onContribsPager__reallyDoQuery(
297 $data, $this, $offset, $limit, $order );
303 foreach ( $data as $query ) {
304 foreach ( $query as $i => $row ) {
306 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
308 $index = str_pad( (
string)$index, strlen( (
string)$limit ),
'0', STR_PAD_LEFT );
315 if ( $order === self::QUERY_ASCENDING ) {
322 $result = array_slice( $result, 0, $limit );
325 $result = array_values( $result );
351 $revQuery = $this->revisionStore->getQueryInfo( [
'page',
'user' ] );
354 'fields' => array_merge(
$revQuery[
'fields'], [
'page_is_new' ] ),
361 $dbr = $this->getDatabase();
362 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds(
$dbr, $this->target ) :
null;
363 if ( $ipRangeConds ) {
365 array_unshift( $queryInfo[
'tables'],
'ip_changes' );
366 $queryInfo[
'join_conds'][
'revision'] = [
367 'JOIN', [
'rev_id = ipc_rev_id' ]
369 $queryInfo[
'conds'][] = $ipRangeConds;
372 $conds = $this->actorMigration->getWhere(
$dbr,
'rev_user', $this->targetUser );
373 $queryInfo[
'conds'][] = $conds[
'conds'];
375 if ( isset( $conds[
'orconds'][
'actor'] ) ) {
376 $queryInfo[
'options'][
'USE INDEX'][
'temp_rev_user'] =
'actor_timestamp';
378 if ( isset( $conds[
'orconds'][
'newactor'] ) ) {
379 $queryInfo[
'options'][
'USE INDEX'][
'revision'] =
'rev_actor_timestamp';
383 if ( $this->deletedOnly ) {
384 $queryInfo[
'conds'][] =
'rev_deleted != 0';
387 if ( $this->topOnly ) {
388 $queryInfo[
'conds'][] =
'rev_id = page_latest';
391 if ( $this->newOnly ) {
392 $queryInfo[
'conds'][] =
'rev_parent_id = 0';
395 if ( $this->hideMinor ) {
396 $queryInfo[
'conds'][] =
'rev_minor_edit = 0';
399 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
402 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
403 $queryInfo[
'conds'][] =
$dbr->bitAnd(
404 'rev_deleted', RevisionRecord::DELETED_USER
406 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
407 $queryInfo[
'conds'][] =
$dbr->bitAnd(
408 'rev_deleted', RevisionRecord::SUPPRESSED_USER
409 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
413 $indexField = $this->getIndexField();
414 if ( $indexField !==
'rev_timestamp' ) {
415 $queryInfo[
'fields'][] = $indexField;
419 $queryInfo[
'tables'],
420 $queryInfo[
'fields'],
422 $queryInfo[
'join_conds'],
423 $queryInfo[
'options'],
427 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
567 # Do a link batch query
568 $this->mResult->seek( 0 );
570 $this->mParentLens = [];
572 $linkBatch = $this->linkBatchFactory->newLinkBatch();
573 $isIpRange = $this->isQueryableRange( $this->target );
574 # Give some pointers to make (last) links
575 foreach ( $this->mResult as $row ) {
576 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
577 $parentRevIds[] = (int)$row->rev_parent_id;
579 if ( $this->revisionStore->isRevisionRow( $row ) ) {
580 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
585 $linkBatch->add( $row->page_namespace, $row->page_title );
586 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
589 # Fetch rev_len for revisions not already scanned above
590 $this->mParentLens += $this->revisionStore->getRevisionSizes(
591 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
593 $linkBatch->execute();
595 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
597 ->revisions( $revisions )
601 # For performance, save the revision objects for later.
602 # The array is indexed by rev_id. doBatchLookups() may be called
603 # multiple times with different results, so merge the revisions array,
604 # ignoring any duplicates.
605 $this->revisions += $revisions;
661 $linkRenderer = $this->getLinkRenderer();
666 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
667 $page = Title::newFromRow( $row );
674 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
676 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
677 $attribs[
'data-mw-revid'] = $revRecord->getId();
679 $link = $linkRenderer->makeLink(
682 $page->getPrefixedText(),
683 [
'class' =>
'mw-contributions-title' ],
684 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
686 # Mark current revisions
689 if ( $row->rev_id === $row->page_latest ) {
690 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
691 $classes[] =
'mw-contributions-current';
693 if ( !$row->page_is_new &&
695 $this->getAuthority()->probablyCan(
'rollback', $page ) &&
699 $this->setPreventClickjacking(
true );
707 # Is there a visible previous revision?
708 if ( $revRecord->getParentId() !== 0 &&
709 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
711 $difftext = $linkRenderer->makeKnownLink(
714 new HtmlArmor( $this->messages[
'diff'] ),
715 [
'class' =>
'mw-changeslist-diff' ],
718 'oldid' => $row->rev_id
722 $difftext = $this->messages[
'diff'];
724 $histlink = $linkRenderer->makeKnownLink(
727 new HtmlArmor( $this->messages[
'hist'] ),
728 [
'class' =>
'mw-changeslist-history' ],
729 [
'action' =>
'history' ]
732 if ( $row->rev_parent_id ===
null ) {
736 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
738 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
741 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
742 $parentLen = $this->mParentLens[$row->rev_parent_id];
745 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
746 $chardiff .= ChangesList::showCharacterDifference(
751 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
754 $lang = $this->getLanguage();
756 $comment = $this->formattedComments[$row->rev_id];
758 if ( $comment ===
'' ) {
759 $defaultComment = $this->msg(
'changeslist-nocomment' )->escaped();
760 $comment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
763 $comment =
$lang->getDirMark() . $comment;
766 $d = ChangesList::revDateLink( $revRecord, $authority,
$lang, $page );
768 # When querying for an IP range, we want to always show user and user talk links.
770 $revUser = $revRecord->getUser();
771 $revUserId = $revUser ? $revUser->getId() : 0;
772 $revUserText = $revUser ? $revUser->getName() :
'';
773 if ( $this->isQueryableRange( $this->target ) ) {
774 $userlink =
' <span class="mw-changeslist-separator"></span> '
775 .
$lang->getDirMark()
777 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
782 if ( $revRecord->getParentId() === 0 ) {
783 $flags[] = ChangesList::flag(
'newpage' );
786 if ( $revRecord->isMinor() ) {
787 $flags[] = ChangesList::flag(
'minor' );
800 $diffHistLinks = Html::rawElement(
'span',
801 [
'class' =>
'mw-changeslist-links' ],
804 Html::rawElement(
'span', [], $difftext ) .
806 Html::rawElement(
'span', [], $histlink )
815 $classes = array_merge( $classes, $newClasses );
817 $this->hookRunner->onSpecialContributions__formatRow__flags(
823 'diffHistLinks' => $diffHistLinks,
824 'charDifference' => $chardiff,
826 'articleLink' => $link,
827 'userlink' => $userlink,
828 'logText' => $comment,
829 'topmarktext' => $topmarktext,
830 'tagSummary' => $tagSummary,
833 # Denote if username is redacted for this edit
834 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
835 $templateParams[
'rev-deleted-user-contribs'] =
836 $this->msg(
'rev-deleted-user-contribs' )->escaped();
839 $ret = $this->templateParser->processTemplate(
840 'SpecialContributionsLine',
846 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
847 $attribs = array_filter( $attribs,
848 [ Sanitizer::class,
'isReservedDataAttribute' ],
855 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
856 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
857 return "<!-- Could not format Special:Contribution row. -->\n";
859 $attribs[
'class'] = $classes;
863 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";