179 $dbProvider ??= $services->getConnectionProvider();
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 );
577 # Do a link batch query
578 $this->mResult->seek( 0 );
580 $this->mParentLens = [];
582 $linkBatch = $this->linkBatchFactory->newLinkBatch();
583 # Give some pointers to make (last) links
584 foreach ( $this->mResult as $row ) {
585 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
586 $parentRevIds[] = (int)$row->rev_parent_id;
588 if ( $this->revisionStore->isRevisionRow( $row ) ) {
589 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
590 if ( $this->target !== $row->rev_user_text ) {
594 $linkBatch->add( $row->page_namespace, $row->page_title );
595 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
598 # Fetch rev_len for revisions not already scanned above
599 $this->mParentLens += $this->revisionStore->getRevisionSizes(
600 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
602 $linkBatch->execute();
604 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
606 ->revisions( $revisions )
610 # For performance, save the revision objects for later.
611 # The array is indexed by rev_id. doBatchLookups() may be called
612 # multiple times with different results, so merge the revisions array,
613 # ignoring any duplicates.
614 $this->revisions += $revisions;
670 $linkRenderer = $this->getLinkRenderer();
675 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
676 $page = Title::newFromRow( $row );
683 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
684 if ( $revRecord && $page ) {
685 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
686 $attribs[
'data-mw-revid'] = $revRecord->getId();
688 $link = $linkRenderer->makeLink(
690 $page->getPrefixedText(),
691 [
'class' =>
'mw-contributions-title' ],
692 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
694 # Mark current revisions
700 $row->rev_id === $row->page_latest && !$row->page_is_new,
704 $this->getLinkRenderer()
706 if ( $row->rev_id === $row->page_latest ) {
707 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
708 $classes[] =
'mw-contributions-current';
710 if ( $pagerTools->shouldPreventClickjacking() ) {
711 $this->setPreventClickjacking(
true );
713 $topmarktext .= $pagerTools->toHTML();
714 # Is there a visible previous revision?
715 if ( $revRecord->getParentId() !== 0 &&
716 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
718 $difftext = $linkRenderer->makeKnownLink(
720 new HtmlArmor( $this->messages[
'diff'] ),
721 [
'class' =>
'mw-changeslist-diff' ],
724 'oldid' => $row->rev_id
728 $difftext = $this->messages[
'diff'];
730 $histlink = $linkRenderer->makeKnownLink(
732 new HtmlArmor( $this->messages[
'hist'] ),
733 [
'class' =>
'mw-changeslist-history' ],
734 [
'action' =>
'history' ]
737 if ( $row->rev_parent_id ===
null ) {
741 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
742 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
743 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
746 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
747 $parentLen = $this->mParentLens[$row->rev_parent_id];
750 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
751 $chardiff .= ChangesList::showCharacterDifference(
756 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
759 $lang = $this->getLanguage();
761 $comment = $this->formattedComments[$row->rev_id];
763 if ( $comment ===
'' ) {
764 $defaultComment = $this->messages[
'changeslist-nocomment'];
765 $comment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
768 $comment = $lang->getDirMark() . $comment;
771 $d = ChangesList::revDateLink( $revRecord, $authority, $lang, $page );
775 $revUser = $revRecord->getUser();
776 $revUserId = $revUser ? $revUser->getId() : 0;
777 $revUserText = $revUser ? $revUser->getName() :
'';
778 if ( $this->target !== $revUserText ) {
779 $userlink =
' <span class="mw-changeslist-separator"></span> '
780 . $lang->getDirMark()
781 . Linker::userLink( $revUserId, $revUserText );
782 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
783 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() .
' ';
787 if ( $revRecord->getParentId() === 0 ) {
788 $flags[] = ChangesList::flag(
'newpage' );
791 if ( $revRecord->isMinor() ) {
792 $flags[] = ChangesList::flag(
'minor' );
795 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
804 $diffHistLinks = Html::rawElement(
'span',
805 [
'class' =>
'mw-changeslist-links' ],
808 Html::rawElement(
'span', [], $difftext ) .
810 Html::rawElement(
'span', [], $histlink )
813 # Tags, if any. Save some time using a cache.
814 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
815 $this->tagsCache->makeKey(
817 $this->getUser()->getName(),
826 $classes = array_merge( $classes, $newClasses );
828 $this->hookRunner->onSpecialContributions__formatRow__flags(
834 'diffHistLinks' => $diffHistLinks,
835 'charDifference' => $chardiff,
837 'articleLink' => $link,
838 'userlink' => $userlink,
839 'logText' => $comment,
840 'topmarktext' => $topmarktext,
841 'tagSummary' => $tagSummary,
844 # Denote if username is redacted for this edit
845 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
846 $templateParams[
'rev-deleted-user-contribs'] =
847 $this->msg(
'rev-deleted-user-contribs' )->escaped();
850 $ret = $this->templateParser->processTemplate(
851 'SpecialContributionsLine',
857 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
858 $attribs = array_filter( $attribs,
859 [ Sanitizer::class,
'isReservedDataAttribute' ],
866 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
867 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
868 return "<!-- Could not format Special:Contribution row. -->\n";
870 $attribs[
'class'] = $classes;
874 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";