193 $this->isArchive = $options[
'isArchive'] ??
false;
206 $this->target = $options[
'target'] ??
'';
209 $this->target, UserRigorOptions::RIGOR_NONE
211 if ( !$this->targetUser ) {
215 throw new InvalidArgumentException( __METHOD__ .
': the user name is too ' .
216 'broken to use even with validation disabled.' );
220 $this->
namespace = $options['namespace'] ?? '';
221 $this->tagFilter = $options[
'tagfilter'] ??
false;
222 $this->tagInvert = $options[
'tagInvert'] ??
false;
223 $this->nsInvert = $options[
'nsInvert'] ??
false;
224 $this->associated = $options[
'associated'] ??
false;
226 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
227 $this->topOnly = !empty( $options[
'topOnly'] );
228 $this->newOnly = !empty( $options[
'newOnly'] );
229 $this->hideMinor = !empty( $options[
'hideMinor'] );
230 $this->revisionsOnly = !empty( $options[
'revisionsOnly'] );
232 parent::__construct( $context, $linkRenderer );
239 'changeslist-nocomment',
245 foreach ( $msgs as $msg ) {
246 $this->messages[$msg] = $this->
msg( $msg )->escaped();
250 $startTimestamp =
'';
252 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
253 $startTimestamp = $options[
'start'] .
' 00:00:00';
255 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
256 $endTimestamp = $options[
'end'] .
' 23:59:59';
261 $this->linkBatchFactory = $linkBatchFactory;
262 $this->hookRunner =
new HookRunner( $hookContainer );
264 $this->namespaceInfo = $namespaceInfo;
265 $this->commentFormatter = $commentFormatter;
286 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->
buildQueryInfo(
292 $options[
'MAX_EXECUTION_TIME'] =
313 $data = [ $dbr->newSelectQueryBuilder()
314 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
318 ->options( $options )
319 ->joinConds( $join_conds )
321 ->fetchResultSet() ];
322 if ( !$this->revisionsOnly ) {
326 $reallyDoQueryHook = $this->isArchive ?
327 'onDeletedContribsPager__reallyDoQuery' :
328 'onContribsPager__reallyDoQuery';
332 $this->hookRunner->$reallyDoQueryHook( $data, $this, $offset, $limit, $order );
338 foreach ( $data as $query ) {
339 foreach ( $query as $i => $row ) {
341 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
343 $index = str_pad( (
string)$index, strlen( (
string)$limit ),
'0', STR_PAD_LEFT );
350 if ( $order === self::QUERY_ASCENDING ) {
357 $result = array_slice( $result, 0, $limit );
360 $result = array_values( $result );
362 return new FakeResultWrapper( $result );
374 $queryInfo = $this->getRevisionQuery();
376 if ( $this->deletedOnly ) {
377 $queryInfo[
'conds'][] = $this->revisionDeletedField .
' != 0';
380 if ( !$this->isArchive && $this->topOnly ) {
381 $queryInfo[
'conds'][] = $this->revisionIdField .
' = page_latest';
384 if ( $this->newOnly ) {
385 $queryInfo[
'conds'][] = $this->revisionParentIdField .
' = 0';
388 if ( $this->hideMinor ) {
389 $queryInfo[
'conds'][] = $this->revisionMinorField .
' = 0';
392 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
395 $dbr = $this->getDatabase();
396 if ( !$this->getAuthority()->isAllowed(
'deletedhistory' ) ) {
397 $queryInfo[
'conds'][] = $dbr->bitAnd(
398 $this->revisionDeletedField, RevisionRecord::DELETED_USER
400 } elseif ( !$this->getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
401 $queryInfo[
'conds'][] = $dbr->bitAnd(
402 $this->revisionDeletedField, RevisionRecord::SUPPRESSED_USER
403 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
407 $indexField = $this->getIndexField();
408 if ( $indexField !== $this->revisionTimestampField ) {
409 $queryInfo[
'fields'][] = $indexField;
413 $queryInfo[
'tables'],
414 $queryInfo[
'fields'],
416 $queryInfo[
'join_conds'],
417 $queryInfo[
'options'],
422 if ( !$this->isArchive ) {
423 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
479 # Do a link batch query
480 $this->mResult->seek( 0 );
482 $this->mParentLens = [];
484 $linkBatch = $this->linkBatchFactory->newLinkBatch();
485 # Give some pointers to make (last) links
486 foreach ( $this->mResult as $row ) {
487 if ( isset( $row->{$this->revisionParentIdField} ) && $row->{$this->revisionParentIdField} ) {
488 $parentRevIds[] = (int)$row->{$this->revisionParentIdField};
490 if ( $this->revisionStore->isRevisionRow( $row, $this->isArchive ?
'archive' :
'revision' ) ) {
491 $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField};
492 if ( $this->target !== $row->{$this->userNameField} ) {
494 $linkBatch->add(
NS_USER_TALK, $row->{$this->userNameField} );
496 $linkBatch->add( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} );
497 $revisions[$row->{$this->revisionIdField}] = $this->createRevisionRecord( $row );
502 if ( $this->isArchive ) {
503 $parentRevIds = array_diff( $parentRevIds, array_keys( $this->mParentLens ) );
504 if ( $parentRevIds ) {
505 $result = $this->revisionStore
506 ->newArchiveSelectQueryBuilder( $this->getDatabase() )
508 ->fields( [ $this->revisionIdField, $this->revisionLengthField ] )
509 ->where( [ $this->revisionIdField => $parentRevIds ] )
510 ->caller( __METHOD__ )
512 foreach ( $result as $row ) {
513 $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField};
517 $this->mParentLens += $this->revisionStore->getRevisionSizes(
518 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
520 $linkBatch->execute();
522 $revisionBatch = $this->commentFormatter->createRevisionBatch()
524 ->revisions( $revisions );
526 if ( !$this->isArchive ) {
528 $revisionBatch = $revisionBatch->hideIfDeleted();
531 $this->formattedComments = $revisionBatch->execute();
533 # For performance, save the revision objects for later.
534 # The array is indexed by rev_id. doBatchLookups() may be called
535 # multiple times with different results, so merge the revisions array,
536 # ignoring any duplicates.
537 $this->revisions += $revisions;
619 $authority = $this->getAuthority();
620 $language = $this->getLanguage();
622 $linkRenderer = $this->getLinkRenderer();
627 if ( isset( $row->{$this->pageNamespaceField} ) && isset( $row->{$this->pageTitleField} ) ) {
628 $page = Title::makeTitle( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} );
631 $dir = $language->getDir();
638 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
639 if ( $revRecord && $page ) {
640 $revRecord = $this->createRevisionRecord( $row, $page );
641 $attribs[
'data-mw-revid'] = $revRecord->getId();
643 $link = Html::rawElement(
'bdi', [
'dir' => $dir ], $linkRenderer->makeLink(
645 $page->getPrefixedText(),
646 [
'class' =>
'mw-contributions-title' ],
647 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
649 # Mark current revisions
653 if ( $this->isArchive ) {
655 $undelete = SpecialPage::getTitleFor(
'Undelete' );
656 if ( $authority->isAllowed(
'deletedtext' ) ) {
657 $last = $linkRenderer->makeKnownLink(
659 new HtmlArmor( $this->messages[
'diff'] ),
662 'target' => $page->getPrefixedText(),
663 'timestamp' => $revRecord->getTimestamp(),
668 $last = $this->messages[
'diff'];
671 $logs = SpecialPage::getTitleFor(
'Log' );
672 $dellog = $linkRenderer->makeKnownLink(
674 new HtmlArmor( $this->messages[
'deletionlog'] ),
678 'page' => $page->getPrefixedText()
682 $reviewlink = $linkRenderer->makeKnownLink(
683 SpecialPage::getTitleFor(
'Undelete', $page->getPrefixedDBkey() ),
684 new HtmlArmor( $this->messages[
'undeleteviewlink'] )
687 $diffHistLinks = Html::rawElement(
689 [
'class' =>
'mw-deletedcontribs-tools' ],
690 $this->msg(
'parentheses' )->rawParams( $language->pipeList(
691 [ $last, $dellog, $reviewlink ] ) )->escaped()
694 $date = $language->userTimeAndDate(
695 $revRecord->getTimestamp(),
699 if ( $authority->isAllowed(
'undelete' ) &&
700 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $authority )
702 $dateLink = $linkRenderer->makeKnownLink(
703 SpecialPage::getTitleFor(
'Undelete' ),
705 [
'class' =>
'mw-changeslist-date' ],
707 'target' => $page->getPrefixedText(),
708 'timestamp' => $revRecord->getTimestamp()
712 $dateLink = htmlspecialchars( $date );
714 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
715 $class = Linker::getRevisionDeletedClass( $revRecord );
716 $dateLink = Html::rawElement(
718 [
'class' => $class ],
724 $pagerTools =
new PagerTools(
727 $row->{$this->revisionIdField} === $row->page_latest && !$row->page_is_new,
731 $this->getLinkRenderer()
733 if ( $row->{$this->revisionIdField} === $row->page_latest ) {
734 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
735 $classes[] =
'mw-contributions-current';
737 if ( $pagerTools->shouldPreventClickjacking() ) {
738 $this->setPreventClickjacking(
true );
740 $topmarktext .= $pagerTools->toHTML();
741 # Is there a visible previous revision?
742 if ( $revRecord->getParentId() !== 0 &&
743 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $authority )
745 $difftext = $linkRenderer->makeKnownLink(
747 new HtmlArmor( $this->messages[
'diff'] ),
748 [
'class' =>
'mw-changeslist-diff' ],
751 'oldid' => $row->{$this->revisionIdField},
755 $difftext = $this->messages[
'diff'];
757 $histlink = $linkRenderer->makeKnownLink(
759 new HtmlArmor( $this->messages[
'hist'] ),
760 [
'class' =>
'mw-changeslist-history' ],
761 [
'action' =>
'history' ]
768 $diffHistLinks = Html::rawElement(
'span',
769 [
'class' =>
'mw-changeslist-links' ],
772 Html::rawElement(
'span', [], $difftext ) .
774 Html::rawElement(
'span', [], $histlink )
780 if ( $row->{$this->revisionParentIdField} === null ) {
784 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
785 $chardiff .= Linker::formatRevisionSize( $row->{$this->revisionLengthField} );
786 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
789 if ( isset( $this->mParentLens[$row->{$this->revisionParentIdField}] ) ) {
790 $parentLen = $this->mParentLens[$row->{$this->revisionParentIdField}];
793 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
796 $row->{$this->revisionLengthField},
799 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
802 $comment = $this->formattedComments[$row->{$this->revisionIdField}];
804 if ( $comment ===
'' ) {
805 $defaultComment = $this->messages[
'changeslist-nocomment'];
806 $comment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
809 $comment = Html::rawElement(
'bdi', [
'dir' => $dir ], $comment );
813 $revUser = $revRecord->getUser();
814 $revUserId = $revUser ? $revUser->getId() : 0;
815 $revUserText = $revUser ? $revUser->getName() :
'';
816 if ( $this->target !== $revUserText ) {
817 $userlink =
' <span class="mw-changeslist-separator"></span> '
818 . Html::rawElement(
'bdi', [
'dir' => $dir ],
819 Linker::userLink( $revUserId, $revUserText ) );
820 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
821 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() .
' ';
825 if ( $revRecord->getParentId() === 0 ) {
829 if ( $revRecord->isMinor() ) {
833 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
838 # Tags, if any. Save some time using a cache.
839 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
840 $this->tagsCache->makeKey(
842 $this->getUser()->getName(),
851 $classes = array_merge( $classes, $newClasses );
853 if ( !$this->isArchive ) {
854 $this->hookRunner->onSpecialContributions__formatRow__flags(
860 'timestamp' => $dateLink,
861 'diffHistLinks' => $diffHistLinks,
862 'charDifference' => $chardiff,
864 'articleLink' => $link,
865 'userlink' => $userlink,
866 'logText' => $comment,
867 'topmarktext' => $topmarktext,
868 'tagSummary' => $tagSummary,
871 # Denote if username is redacted for this edit
872 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
873 $templateParams[
'rev-deleted-user-contribs'] =
874 $this->msg(
'rev-deleted-user-contribs' )->escaped();
877 $ret = $this->templateParser->processTemplate(
878 'SpecialContributionsLine',
884 $lineEndingsHook = $this->isArchive ?
885 'onDeletedContributionsLineEnding' :
886 'onContributionsLineEnding';
887 $this->hookRunner->$lineEndingsHook( $this, $ret, $row, $classes, $attribs );
888 $attribs = array_filter( $attribs,
889 [ Sanitizer::class,
'isReservedDataAttribute' ],
896 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
897 wfDebug(
"Dropping ContributionsSpecialPage row that could not be formatted" );
898 return "<!-- Could not format ContributionsSpecialPage row. -->\n";
900 $attribs[
'class'] = $classes;
904 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";