180 $dbProvider ??= $services->getConnectionProvider();
186 $this->target = $options[
'target'] ?? $targetUser->
getName();
187 $this->targetUser = $targetUser;
193 $this->target = $options[
'target'] ??
'';
195 $this->targetUser = $services->getUserFactory()->newFromName(
196 $this->target, UserRigorOptions::RIGOR_NONE
198 if ( !$this->targetUser ) {
202 throw new InvalidArgumentException( __METHOD__ .
': the user name is too ' .
203 'broken to use even with validation disabled.' );
207 $this->
namespace = $options['namespace'] ?? '';
208 $this->tagFilter = $options[
'tagfilter'] ??
false;
209 $this->tagInvert = $options[
'tagInvert'] ??
false;
210 $this->nsInvert = $options[
'nsInvert'] ??
false;
211 $this->associated = $options[
'associated'] ??
false;
213 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
214 $this->topOnly = !empty( $options[
'topOnly'] );
215 $this->newOnly = !empty( $options[
'newOnly'] );
216 $this->hideMinor = !empty( $options[
'hideMinor'] );
217 $this->revisionsOnly = !empty( $options[
'revisionsOnly'] );
219 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
226 'changeslist-nocomment',
229 foreach ( $msgs as $msg ) {
230 $this->messages[$msg] = $this->
msg( $msg )->escaped();
234 $startTimestamp =
'';
236 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
237 $startTimestamp = $options[
'start'] .
' 00:00:00';
239 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
240 $endTimestamp = $options[
'end'] .
' 23:59:59';
245 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
246 $this->hookRunner =
new HookRunner( $hookContainer ?? $services->getHookContainer() );
247 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
248 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
249 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
270 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->
buildQueryInfo(
295 $data = [ $dbr->newSelectQueryBuilder()
296 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
300 ->options( $options )
301 ->joinConds( $join_conds )
303 ->fetchResultSet() ];
304 if ( !$this->revisionsOnly ) {
308 $this->hookRunner->onContribsPager__reallyDoQuery(
309 $data, $this, $offset, $limit, $order );
315 foreach ( $data as $query ) {
316 foreach ( $query as $i => $row ) {
318 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
320 $index = str_pad( (
string)$index, strlen( (
string)$limit ),
'0', STR_PAD_LEFT );
327 if ( $order === self::QUERY_ASCENDING ) {
334 $result = array_slice( $result, 0, $limit );
337 $result = array_values( $result );
339 return new FakeResultWrapper( $result );
363 $revQuery = $this->revisionStore->getQueryInfo( [
'page',
'user' ] );
365 'tables' => $revQuery[
'tables'],
366 'fields' => array_merge( $revQuery[
'fields'], [
'page_is_new' ] ),
369 'join_conds' => $revQuery[
'joins'],
373 $dbr = $this->getDatabase();
374 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) :
null;
375 if ( $ipRangeConds ) {
377 array_unshift( $queryInfo[
'tables'],
'ip_changes' );
378 $queryInfo[
'join_conds'][
'revision'] = [
379 'JOIN', [
'rev_id = ipc_rev_id' ]
381 $queryInfo[
'conds'][] = $ipRangeConds;
383 $queryInfo[
'conds'][
'actor_name'] = $this->targetUser->getName();
385 $queryInfo[
'options'][
'USE INDEX'][
'revision'] =
'rev_actor_timestamp';
388 if ( $this->deletedOnly ) {
389 $queryInfo[
'conds'][] =
'rev_deleted != 0';
392 if ( $this->topOnly ) {
393 $queryInfo[
'conds'][] =
'rev_id = page_latest';
396 if ( $this->newOnly ) {
397 $queryInfo[
'conds'][] =
'rev_parent_id = 0';
400 if ( $this->hideMinor ) {
401 $queryInfo[
'conds'][] =
'rev_minor_edit = 0';
404 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
407 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
408 $queryInfo[
'conds'][] = $dbr->bitAnd(
409 'rev_deleted', RevisionRecord::DELETED_USER
411 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
412 $queryInfo[
'conds'][] = $dbr->bitAnd(
413 'rev_deleted', RevisionRecord::SUPPRESSED_USER
414 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
418 $indexField = $this->getIndexField();
419 if ( $indexField !==
'rev_timestamp' ) {
420 $queryInfo[
'fields'][] = $indexField;
424 $queryInfo[
'tables'],
425 $queryInfo[
'fields'],
427 $queryInfo[
'join_conds'],
428 $queryInfo[
'options'],
433 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
582 # Do a link batch query
583 $this->mResult->seek( 0 );
585 $this->mParentLens = [];
587 $linkBatch = $this->linkBatchFactory->newLinkBatch();
588 # Give some pointers to make (last) links
589 foreach ( $this->mResult as $row ) {
590 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
591 $parentRevIds[] = (int)$row->rev_parent_id;
593 if ( $this->revisionStore->isRevisionRow( $row ) ) {
594 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
595 if ( $this->target !== $row->rev_user_text ) {
599 $linkBatch->add( $row->page_namespace, $row->page_title );
600 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
603 # Fetch rev_len for revisions not already scanned above
604 $this->mParentLens += $this->revisionStore->getRevisionSizes(
605 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
607 $linkBatch->execute();
609 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
611 ->revisions( $revisions )
615 # For performance, save the revision objects for later.
616 # The array is indexed by rev_id. doBatchLookups() may be called
617 # multiple times with different results, so merge the revisions array,
618 # ignoring any duplicates.
619 $this->revisions += $revisions;
675 $linkRenderer = $this->getLinkRenderer();
680 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
681 $page = Title::newFromRow( $row );
688 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
689 if ( $revRecord && $page ) {
690 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
691 $attribs[
'data-mw-revid'] = $revRecord->getId();
693 $link = $linkRenderer->makeLink(
695 $page->getPrefixedText(),
696 [
'class' =>
'mw-contributions-title' ],
697 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
699 # Mark current revisions
705 $row->rev_id === $row->page_latest && !$row->page_is_new,
709 $this->getLinkRenderer()
711 if ( $row->rev_id === $row->page_latest ) {
712 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
713 $classes[] =
'mw-contributions-current';
715 if ( $pagerTools->shouldPreventClickjacking() ) {
716 $this->setPreventClickjacking(
true );
718 $topmarktext .= $pagerTools->toHTML();
719 # Is there a visible previous revision?
720 if ( $revRecord->getParentId() !== 0 &&
721 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
723 $difftext = $linkRenderer->makeKnownLink(
725 new HtmlArmor( $this->messages[
'diff'] ),
726 [
'class' =>
'mw-changeslist-diff' ],
729 'oldid' => $row->rev_id
733 $difftext = $this->messages[
'diff'];
735 $histlink = $linkRenderer->makeKnownLink(
737 new HtmlArmor( $this->messages[
'hist'] ),
738 [
'class' =>
'mw-changeslist-history' ],
739 [
'action' =>
'history' ]
742 if ( $row->rev_parent_id ===
null ) {
746 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
747 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
748 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
751 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
752 $parentLen = $this->mParentLens[$row->rev_parent_id];
755 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
756 $chardiff .= ChangesList::showCharacterDifference(
761 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
764 $lang = $this->getLanguage();
766 $comment = $this->formattedComments[$row->rev_id];
768 if ( $comment ===
'' ) {
769 $defaultComment = $this->messages[
'changeslist-nocomment'];
770 $comment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
773 $comment = $lang->getDirMark() . $comment;
776 $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
780 $revUser = $revRecord->getUser();
781 $revUserId = $revUser ? $revUser->getId() : 0;
782 $revUserText = $revUser ? $revUser->getName() :
'';
783 if ( $this->target !== $revUserText ) {
784 $userlink =
' <span class="mw-changeslist-separator"></span> '
785 . $lang->getDirMark()
786 . Linker::userLink( $revUserId, $revUserText );
787 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
788 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() .
' ';
792 if ( $revRecord->getParentId() === 0 ) {
793 $flags[] = ChangesList::flag(
'newpage' );
796 if ( $revRecord->isMinor() ) {
797 $flags[] = ChangesList::flag(
'minor' );
800 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
809 $diffHistLinks = Html::rawElement(
'span',
810 [
'class' =>
'mw-changeslist-links' ],
813 Html::rawElement(
'span', [], $difftext ) .
815 Html::rawElement(
'span', [], $histlink )
818 # Tags, if any. Save some time using a cache.
819 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
820 $this->tagsCache->makeKey(
822 $this->getUser()->getName(),
831 $classes = array_merge( $classes, $newClasses );
833 $this->hookRunner->onSpecialContributions__formatRow__flags(
839 'diffHistLinks' => $diffHistLinks,
840 'charDifference' => $chardiff,
842 'articleLink' => $link,
843 'userlink' => $userlink,
844 'logText' => $comment,
845 'topmarktext' => $topmarktext,
846 'tagSummary' => $tagSummary,
849 # Denote if username is redacted for this edit
850 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
851 $templateParams[
'rev-deleted-user-contribs'] =
852 $this->msg(
'rev-deleted-user-contribs' )->escaped();
855 $ret = $this->templateParser->processTemplate(
856 'SpecialContributionsLine',
862 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
863 $attribs = array_filter( $attribs,
864 [ Sanitizer::class,
'isReservedDataAttribute' ],
871 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
872 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
873 return "<!-- Could not format Special:Contribution row. -->\n";
875 $attribs[
'class'] = $classes;
879 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";