191 $this->isArchive = $options[
'isArchive'] ??
false;
204 $this->target = $options[
'target'] ??
'';
207 $this->target, UserRigorOptions::RIGOR_NONE
209 if ( !$this->targetUser ) {
213 throw new InvalidArgumentException( __METHOD__ .
': the user name is too ' .
214 'broken to use even with validation disabled.' );
218 $this->
namespace = $options['namespace'] ?? '';
219 $this->tagFilter = $options[
'tagfilter'] ??
false;
220 $this->tagInvert = $options[
'tagInvert'] ??
false;
221 $this->nsInvert = $options[
'nsInvert'] ??
false;
222 $this->associated = $options[
'associated'] ??
false;
224 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
225 $this->topOnly = !empty( $options[
'topOnly'] );
226 $this->newOnly = !empty( $options[
'newOnly'] );
227 $this->hideMinor = !empty( $options[
'hideMinor'] );
228 $this->revisionsOnly = !empty( $options[
'revisionsOnly'] );
230 parent::__construct( $context, $linkRenderer );
237 'changeslist-nocomment',
243 foreach ( $msgs as $msg ) {
244 $this->messages[$msg] = $this->
msg( $msg )->escaped();
248 $startTimestamp =
'';
250 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
251 $startTimestamp = $options[
'start'] .
' 00:00:00';
253 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
254 $endTimestamp = $options[
'end'] .
' 23:59:59';
259 $this->linkBatchFactory = $linkBatchFactory;
260 $this->hookRunner =
new HookRunner( $hookContainer );
262 $this->namespaceInfo = $namespaceInfo;
263 $this->commentFormatter = $commentFormatter;
284 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->
buildQueryInfo(
290 $options[
'MAX_EXECUTION_TIME'] =
311 $data = [ $dbr->newSelectQueryBuilder()
312 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
316 ->options( $options )
317 ->joinConds( $join_conds )
319 ->fetchResultSet() ];
320 if ( !$this->revisionsOnly && !$this->isArchive ) {
324 $this->hookRunner->onContribsPager__reallyDoQuery(
325 $data, $this, $offset, $limit, $order );
331 foreach ( $data as $query ) {
332 foreach ( $query as $i => $row ) {
334 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
336 $index = str_pad( (
string)$index, strlen( (
string)$limit ),
'0', STR_PAD_LEFT );
343 if ( $order === self::QUERY_ASCENDING ) {
350 $result = array_slice( $result, 0, $limit );
353 $result = array_values( $result );
355 return new FakeResultWrapper( $result );
367 $queryInfo = $this->getRevisionQuery();
369 if ( $this->deletedOnly ) {
370 $queryInfo[
'conds'][] = $this->revisionDeletedField .
' != 0';
373 if ( $this->topOnly ) {
374 $queryInfo[
'conds'][] = $this->revisionIdField .
' = page_latest';
377 if ( $this->newOnly ) {
378 $queryInfo[
'conds'][] = $this->revisionParentIdField .
' = 0';
381 if ( $this->hideMinor ) {
382 $queryInfo[
'conds'][] = $this->revisionMinorField .
' = 0';
385 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
388 $dbr = $this->getDatabase();
389 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
390 $queryInfo[
'conds'][] = $dbr->bitAnd(
391 $this->revisionDeletedField, RevisionRecord::DELETED_USER
393 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
394 $queryInfo[
'conds'][] = $dbr->bitAnd(
395 $this->revisionDeletedField, RevisionRecord::SUPPRESSED_USER
396 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
400 $indexField = $this->getIndexField();
401 if ( $indexField !== $this->revisionTimestampField ) {
402 $queryInfo[
'fields'][] = $indexField;
406 $queryInfo[
'tables'],
407 $queryInfo[
'fields'],
409 $queryInfo[
'join_conds'],
410 $queryInfo[
'options'],
415 if ( !$this->isArchive ) {
416 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
472 # Do a link batch query
473 $this->mResult->seek( 0 );
475 $this->mParentLens = [];
477 $linkBatch = $this->linkBatchFactory->newLinkBatch();
478 # Give some pointers to make (last) links
479 foreach ( $this->mResult as $row ) {
480 if ( isset( $row->{$this->revisionParentIdField} ) && $row->{$this->revisionParentIdField} ) {
481 $parentRevIds[] = (int)$row->{$this->revisionParentIdField};
483 if ( $this->revisionStore->isRevisionRow( $row, $this->isArchive ?
'archive' :
'revision' ) ) {
484 $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField};
485 if ( $this->target !== $row->{$this->userNameField} ) {
487 $linkBatch->add(
NS_USER_TALK, $row->{$this->userNameField} );
489 $linkBatch->add( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} );
490 $revisions[$row->{$this->revisionIdField}] = $this->createRevisionRecord( $row );
493 # Fetch rev_len for revisions not already scanned above
494 $this->mParentLens += $this->revisionStore->getRevisionSizes(
495 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
497 $linkBatch->execute();
499 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
501 ->revisions( $revisions )
505 # For performance, save the revision objects for later.
506 # The array is indexed by rev_id. doBatchLookups() may be called
507 # multiple times with different results, so merge the revisions array,
508 # ignoring any duplicates.
509 $this->revisions += $revisions;
592 $linkRenderer = $this->getLinkRenderer();
597 if ( isset( $row->{$this->pageNamespaceField} ) && isset( $row->{$this->pageTitleField} ) ) {
598 $page = Title::makeTitle( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} );
606 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
607 if ( $revRecord && $page ) {
608 $revRecord = $this->createRevisionRecord( $row, $page );
609 $attribs[
'data-mw-revid'] = $revRecord->getId();
611 $link = $linkRenderer->makeLink(
613 $page->getPrefixedText(),
614 [
'class' =>
'mw-contributions-title' ],
615 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
617 # Mark current revisions
621 if ( $this->isArchive ) {
623 $undelete = SpecialPage::getTitleFor(
'Undelete' );
624 if ( $this->
getAuthority()->isAllowed(
'deletedtext' ) ) {
625 $last = $linkRenderer->makeKnownLink(
627 new HtmlArmor( $this->messages[
'diff'] ),
630 'target' => $page->getPrefixedText(),
631 'timestamp' => $revRecord->getTimestamp(),
636 $last = $this->messages[
'diff'];
639 $logs = SpecialPage::getTitleFor(
'Log' );
640 $dellog = $linkRenderer->makeKnownLink(
642 new HtmlArmor( $this->messages[
'deletionlog'] ),
646 'page' => $page->getPrefixedText()
650 $reviewlink = $linkRenderer->makeKnownLink(
651 SpecialPage::getTitleFor(
'Undelete', $page->getPrefixedDBkey() ),
652 new HtmlArmor( $this->messages[
'undeleteviewlink'] )
655 $diffHistLinks = Html::rawElement(
657 [
'class' =>
'mw-deletedcontribs-tools' ],
658 $this->msg(
'parentheses' )->rawParams( $this->getLanguage()->pipeList(
659 [ $last, $dellog, $reviewlink ] ) )->escaped()
663 $pagerTools =
new PagerTools(
666 $row->{$this->revisionIdField} === $row->page_latest && !$row->page_is_new,
670 $this->getLinkRenderer()
672 if ( $row->{$this->revisionIdField} === $row->page_latest ) {
673 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
674 $classes[] =
'mw-contributions-current';
676 if ( $pagerTools->shouldPreventClickjacking() ) {
677 $this->setPreventClickjacking(
true );
679 $topmarktext .= $pagerTools->toHTML();
680 # Is there a visible previous revision?
681 if ( $revRecord->getParentId() !== 0 &&
682 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
684 $difftext = $linkRenderer->makeKnownLink(
686 new HtmlArmor( $this->messages[
'diff'] ),
687 [
'class' =>
'mw-changeslist-diff' ],
690 'oldid' => $row->{$this->revisionIdField},
694 $difftext = $this->messages[
'diff'];
696 $histlink = $linkRenderer->makeKnownLink(
698 new HtmlArmor( $this->messages[
'hist'] ),
699 [
'class' =>
'mw-changeslist-history' ],
700 [
'action' =>
'history' ]
707 $diffHistLinks = Html::rawElement(
'span',
708 [
'class' =>
'mw-changeslist-links' ],
711 Html::rawElement(
'span', [], $difftext ) .
713 Html::rawElement(
'span', [], $histlink )
717 if ( $row->{$this->revisionParentIdField} === null ) {
721 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
722 $chardiff .= Linker::formatRevisionSize( $row->{$this->revisionLengthField} );
723 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
726 if ( isset( $this->mParentLens[$row->{$this->revisionParentIdField}] ) ) {
727 $parentLen = $this->mParentLens[$row->{$this->revisionParentIdField}];
730 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
733 $row->{$this->revisionLengthField},
736 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
739 $lang = $this->getLanguage();
741 $comment = $this->formattedComments[$row->{$this->revisionIdField}];
743 if ( $comment ===
'' ) {
744 $defaultComment = $this->messages[
'changeslist-nocomment'];
745 $comment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
748 $comment = $lang->getDirMark() . $comment;
755 $revUser = $revRecord->getUser();
756 $revUserId = $revUser ? $revUser->getId() : 0;
757 $revUserText = $revUser ? $revUser->getName() :
'';
758 if ( $this->target !== $revUserText ) {
759 $userlink =
' <span class="mw-changeslist-separator"></span> '
760 . $lang->getDirMark()
761 . Linker::userLink( $revUserId, $revUserText );
762 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
763 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() .
' ';
767 if ( $revRecord->getParentId() === 0 ) {
771 if ( $revRecord->isMinor() ) {
775 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
780 # Tags, if any. Save some time using a cache.
781 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
782 $this->tagsCache->makeKey(
784 $this->getUser()->getName(),
793 $classes = array_merge( $classes, $newClasses );
795 if ( !$this->isArchive ) {
796 $this->hookRunner->onSpecialContributions__formatRow__flags(
803 'diffHistLinks' => $diffHistLinks,
804 'charDifference' => $chardiff,
806 'articleLink' => $link,
807 'userlink' => $userlink,
808 'logText' => $comment,
809 'topmarktext' => $topmarktext,
810 'tagSummary' => $tagSummary,
813 # Denote if username is redacted for this edit
814 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
815 $templateParams[
'rev-deleted-user-contribs'] =
816 $this->msg(
'rev-deleted-user-contribs' )->escaped();
819 $ret = $this->templateParser->processTemplate(
820 'SpecialContributionsLine',
825 if ( !$this->isArchive ) {
827 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
828 $attribs = array_filter( $attribs,
829 [ Sanitizer::class,
'isReservedDataAttribute' ],
837 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
838 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
839 return "<!-- Could not format Special:Contribution row. -->\n";
841 $attribs[
'class'] = $classes;
845 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";