179 $dbProvider ??= $services->getDBLoadBalancerFactory();
185 $this->target = $options[
'target'] ?? $targetUser->
getName();
186 $this->targetUser = $targetUser;
192 $this->target = $options[
'target'] ??
'';
194 $this->targetUser = $services->getUserFactory()->newFromName(
195 $this->target, UserRigorOptions::RIGOR_NONE
197 if ( !$this->targetUser ) {
201 throw new InvalidArgumentException( __METHOD__ .
': the user name is too ' .
202 'broken to use even with validation disabled.' );
206 $this->
namespace = $options['namespace'] ?? '';
207 $this->tagFilter = $options[
'tagfilter'] ??
false;
208 $this->tagInvert = $options[
'tagInvert'] ??
false;
209 $this->nsInvert = $options[
'nsInvert'] ??
false;
210 $this->associated = $options[
'associated'] ??
false;
212 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
213 $this->topOnly = !empty( $options[
'topOnly'] );
214 $this->newOnly = !empty( $options[
'newOnly'] );
215 $this->hideMinor = !empty( $options[
'hideMinor'] );
216 $this->revisionsOnly = !empty( $options[
'revisionsOnly'] );
218 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
225 'changeslist-nocomment',
228 foreach ( $msgs as $msg ) {
229 $this->messages[$msg] = $this->
msg( $msg )->escaped();
233 $startTimestamp =
'';
235 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
236 $startTimestamp = $options[
'start'] .
' 00:00:00';
238 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
239 $endTimestamp = $options[
'end'] .
' 23:59:59';
244 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
245 $this->hookRunner =
new HookRunner( $hookContainer ?? $services->getHookContainer() );
246 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
247 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
248 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
269 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->
buildQueryInfo(
275 $options[
'MAX_EXECUTION_TIME'] =
296 $data = [ $dbr->select(
297 $tables, $fields, $conds, $fname, $options, $join_conds
299 if ( !$this->revisionsOnly ) {
303 $this->hookRunner->onContribsPager__reallyDoQuery(
304 $data, $this, $offset, $limit, $order );
310 foreach ( $data as $query ) {
311 foreach ( $query as $i => $row ) {
313 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
315 $index = str_pad( (
string)$index, strlen( (
string)$limit ),
'0', STR_PAD_LEFT );
322 if ( $order === self::QUERY_ASCENDING ) {
329 $result = array_slice( $result, 0, $limit );
332 $result = array_values( $result );
334 return new FakeResultWrapper( $result );
358 $revQuery = $this->revisionStore->getQueryInfo( [
'page',
'user' ] );
360 'tables' => $revQuery[
'tables'],
361 'fields' => array_merge( $revQuery[
'fields'], [
'page_is_new' ] ),
364 'join_conds' => $revQuery[
'joins'],
368 $dbr = $this->getDatabase();
369 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds( $dbr, $this->target ) :
null;
370 if ( $ipRangeConds ) {
372 array_unshift( $queryInfo[
'tables'],
'ip_changes' );
373 $queryInfo[
'join_conds'][
'revision'] = [
374 'JOIN', [
'rev_id = ipc_rev_id' ]
376 $queryInfo[
'conds'][] = $ipRangeConds;
378 $queryInfo[
'conds'][
'actor_name'] = $this->targetUser->getName();
380 $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'],
428 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
570 # Do a link batch query
571 $this->mResult->seek( 0 );
573 $this->mParentLens = [];
575 $linkBatch = $this->linkBatchFactory->newLinkBatch();
576 $isIpRange = self::isQueryableRange( $this->target, $this->getConfig() );
577 # Give some pointers to make (last) links
578 foreach ( $this->mResult as $row ) {
579 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
580 $parentRevIds[] = (int)$row->rev_parent_id;
582 if ( $this->revisionStore->isRevisionRow( $row ) ) {
583 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
588 $linkBatch->add( $row->page_namespace, $row->page_title );
589 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
592 # Fetch rev_len for revisions not already scanned above
593 $this->mParentLens += $this->revisionStore->getRevisionSizes(
594 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
596 $linkBatch->execute();
598 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
600 ->revisions( $revisions )
604 # For performance, save the revision objects for later.
605 # The array is indexed by rev_id. doBatchLookups() may be called
606 # multiple times with different results, so merge the revisions array,
607 # ignoring any duplicates.
608 $this->revisions += $revisions;
664 $linkRenderer = $this->getLinkRenderer();
669 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
670 $page = Title::newFromRow( $row );
677 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
678 if ( $revRecord && $page ) {
679 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
680 $attribs[
'data-mw-revid'] = $revRecord->getId();
682 $link = $linkRenderer->makeLink(
684 $page->getPrefixedText(),
685 [
'class' =>
'mw-contributions-title' ],
686 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
688 # Mark current revisions
694 $row->rev_id === $row->page_latest && !$row->page_is_new,
698 $this->getLinkRenderer()
700 if ( $row->rev_id === $row->page_latest ) {
701 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
702 $classes[] =
'mw-contributions-current';
704 if ( $pagerTools->shouldPreventClickjacking() ) {
705 $this->setPreventClickjacking(
true );
707 $topmarktext .= $pagerTools->toHTML();
708 # Is there a visible previous revision?
709 if ( $revRecord->getParentId() !== 0 &&
710 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
712 $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(
726 new HtmlArmor( $this->messages[
'hist'] ),
727 [
'class' =>
'mw-changeslist-history' ],
728 [
'action' =>
'history' ]
731 if ( $row->rev_parent_id ===
null ) {
735 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
736 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
737 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
740 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
741 $parentLen = $this->mParentLens[$row->rev_parent_id];
744 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
745 $chardiff .= ChangesList::showCharacterDifference(
750 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
753 $lang = $this->getLanguage();
755 $comment = $this->formattedComments[$row->rev_id];
757 if ( $comment ===
'' ) {
758 $defaultComment = $this->messages[
'changeslist-nocomment'];
759 $comment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
762 $comment = $lang->getDirMark() . $comment;
765 $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
767 # When querying for an IP range, we want to always show user and user talk links.
769 $revUser = $revRecord->getUser();
770 $revUserId = $revUser ? $revUser->getId() : 0;
771 $revUserText = $revUser ? $revUser->getName() :
'';
772 if ( self::isQueryableRange( $this->target, $this->getConfig() ) ) {
773 $userlink =
' <span class="mw-changeslist-separator"></span> '
774 . $lang->getDirMark()
775 . Linker::userLink( $revUserId, $revUserText );
776 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
777 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() .
' ';
781 if ( $revRecord->getParentId() === 0 ) {
782 $flags[] = ChangesList::flag(
'newpage' );
785 if ( $revRecord->isMinor() ) {
786 $flags[] = ChangesList::flag(
'minor' );
789 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
798 $diffHistLinks = Html::rawElement(
'span',
799 [
'class' =>
'mw-changeslist-links' ],
802 Html::rawElement(
'span', [], $difftext ) .
804 Html::rawElement(
'span', [], $histlink )
807 # Tags, if any. Save some time using a cache.
808 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
809 $this->tagsCache->makeKey(
811 $this->getUser()->getName(),
820 $classes = array_merge( $classes, $newClasses );
822 $this->hookRunner->onSpecialContributions__formatRow__flags(
828 'diffHistLinks' => $diffHistLinks,
829 'charDifference' => $chardiff,
831 'articleLink' => $link,
832 'userlink' => $userlink,
833 'logText' => $comment,
834 'topmarktext' => $topmarktext,
835 'tagSummary' => $tagSummary,
838 # Denote if username is redacted for this edit
839 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
840 $templateParams[
'rev-deleted-user-contribs'] =
841 $this->msg(
'rev-deleted-user-contribs' )->escaped();
844 $ret = $this->templateParser->processTemplate(
845 'SpecialContributionsLine',
851 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
852 $attribs = array_filter( $attribs,
853 [ Sanitizer::class,
'isReservedDataAttribute' ],
860 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
861 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
862 return "<!-- Could not format Special:Contribution row. -->\n";
864 $attribs[
'class'] = $classes;
868 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";