155 $services = MediaWikiServices::getInstance();
156 $loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
169 $this->target = $options[
'target'] ??
'';
170 $this->targetUser = $services->getUserFactory()->newFromName(
171 $this->target, UserFactory::RIGOR_NONE
173 if ( !$this->targetUser ) {
177 throw new InvalidArgumentException( __METHOD__ .
': the user name is too ' .
178 'broken to use even with validation disabled.' );
182 $this->
namespace = $options['namespace'] ?? '';
183 $this->tagFilter = $options[
'tagfilter'] ??
false;
184 $this->nsInvert = $options[
'nsInvert'] ??
false;
185 $this->associated = $options[
'associated'] ??
false;
187 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
188 $this->topOnly = !empty( $options[
'topOnly'] );
189 $this->newOnly = !empty( $options[
'newOnly'] );
190 $this->hideMinor = !empty( $options[
'hideMinor'] );
191 $this->revisionsOnly = !empty( $options[
'revisionsOnly'] );
196 $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA,
'contributions' );
198 $this->actorMigration =
$actorMigration ?? $services->getActorMigration();
199 parent::__construct( $context,
$linkRenderer ?? $services->getLinkRenderer() );
208 foreach ( $msgs as $msg ) {
209 $this->messages[$msg] = $this->
msg( $msg )->escaped();
213 $startTimestamp =
'';
215 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
216 $startTimestamp = $options[
'start'] .
' 00:00:00';
218 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
219 $endTimestamp = $options[
'end'] .
' 23:59:59';
225 $this->hookRunner =
new HookRunner( $hookContainer ?? $services->getHookContainer() );
226 $this->revisionStore =
$revisionStore ?? $services->getRevisionStore();
227 $this->namespaceInfo =
$namespaceInfo ?? $services->getNamespaceInfo();
260 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->
buildQueryInfo(
266 $options[
'MAX_EXECUTION_TIME'] = $this->
getConfig()->get(
'MaxExecutionTimeForExpensiveQueries' );
286 $data = [
$dbr->select(
287 $tables, $fields, $conds, $fname, $options, $join_conds
289 if ( !$this->revisionsOnly ) {
290 $this->hookRunner->onContribsPager__reallyDoQuery(
291 $data, $this, $offset, $limit, $order );
297 foreach ( $data as $query ) {
298 foreach ( $query as $i => $row ) {
300 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
302 $index = str_pad( $index, strlen( $limit ),
'0', STR_PAD_LEFT );
309 if ( $order === self::QUERY_ASCENDING ) {
316 $result = array_slice( $result, 0, $limit );
319 $result = array_values( $result );
350 $revQuery = $this->revisionStore->getQueryInfo( [
'page',
'user' ] );
353 'fields' => array_merge(
$revQuery[
'fields'], [
'page_is_new' ] ),
360 $dbr = $this->getDatabase();
361 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds(
$dbr, $this->target ) :
null;
362 if ( $ipRangeConds ) {
364 array_unshift( $queryInfo[
'tables'],
'ip_changes' );
365 $queryInfo[
'join_conds'][
'revision'] = [
366 'JOIN', [
'rev_id = ipc_rev_id' ]
368 $queryInfo[
'conds'][] = $ipRangeConds;
371 $conds = $this->actorMigration->getWhere(
$dbr,
'rev_user', $this->targetUser );
372 $queryInfo[
'conds'][] = $conds[
'conds'];
374 if ( isset( $conds[
'orconds'][
'actor'] ) ) {
375 $queryInfo[
'options'][
'USE INDEX'][
'temp_rev_user'] =
'actor_timestamp';
379 if ( $this->deletedOnly ) {
380 $queryInfo[
'conds'][] =
'rev_deleted != 0';
383 if ( $this->topOnly ) {
384 $queryInfo[
'conds'][] =
'rev_id = page_latest';
387 if ( $this->newOnly ) {
388 $queryInfo[
'conds'][] =
'rev_parent_id = 0';
391 if ( $this->hideMinor ) {
392 $queryInfo[
'conds'][] =
'rev_minor_edit = 0';
395 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
398 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
399 $queryInfo[
'conds'][] =
$dbr->bitAnd(
400 'rev_deleted', RevisionRecord::DELETED_USER
402 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
403 $queryInfo[
'conds'][] =
$dbr->bitAnd(
404 'rev_deleted', RevisionRecord::SUPPRESSED_USER
405 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
409 $indexField = $this->getIndexField();
410 if ( $indexField !==
'rev_timestamp' ) {
411 $queryInfo[
'fields'][] = $indexField;
415 $queryInfo[
'tables'],
416 $queryInfo[
'fields'],
418 $queryInfo[
'join_conds'],
419 $queryInfo[
'options'],
423 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
567 # Do a link batch query
568 $this->mResult->seek( 0 );
570 $this->mParentLens = [];
571 $batch = $this->linkBatchFactory->newLinkBatch();
572 $isIpRange = $this->isQueryableRange( $this->target );
573 # Give some pointers to make (last) links
574 foreach ( $this->mResult as $row ) {
575 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
576 $parentRevIds[] = $row->rev_parent_id;
578 if ( isset( $row->rev_id ) ) {
579 $this->mParentLens[$row->rev_id] = $row->rev_len;
584 $batch->add( $row->page_namespace, $row->page_title );
587 # Fetch rev_len for revisions not already scanned above
588 $this->mParentLens += $this->revisionStore->getRevisionSizes(
589 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
592 $this->mResult->seek( 0 );
647 $linkRenderer = $this->getLinkRenderer();
652 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
653 $page = Title::newFromRow( $row );
660 if ( $this->revisionStore->isRevisionRow( $row ) ) {
661 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
662 $attribs[
'data-mw-revid'] = $revRecord->getId();
664 $link = $linkRenderer->makeLink(
666 $page->getPrefixedText(),
667 [
'class' =>
'mw-contributions-title' ],
668 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
670 # Mark current revisions
672 $user = $this->getUser();
674 if ( $row->rev_id === $row->page_latest ) {
675 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
676 $classes[] =
'mw-contributions-current';
678 if ( !$row->page_is_new &&
679 $this->getAuthority()->probablyCan(
'rollback', $page ) &&
680 $this->getAuthority()->probablyCan(
'edit', $page )
682 $this->preventClickjacking();
690 # Is there a visible previous revision?
691 if ( $revRecord->getParentId() !== 0 &&
692 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
694 $difftext = $linkRenderer->makeKnownLink(
696 new HtmlArmor( $this->messages[
'diff'] ),
697 [
'class' =>
'mw-changeslist-diff' ],
700 'oldid' => $row->rev_id
704 $difftext = $this->messages[
'diff'];
706 $histlink = $linkRenderer->makeKnownLink(
708 new HtmlArmor( $this->messages[
'hist'] ),
709 [
'class' =>
'mw-changeslist-history' ],
710 [
'action' =>
'history' ]
713 if ( $row->rev_parent_id ===
null ) {
717 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
719 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
722 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
723 $parentLen = $this->mParentLens[$row->rev_parent_id];
726 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
727 $chardiff .= ChangesList::showCharacterDifference(
732 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
735 $lang = $this->getLanguage();
737 $d = ChangesList::revDateLink( $revRecord, $user,
$lang, $page );
739 # When querying for an IP range, we want to always show user and user talk links.
741 $revUser = $revRecord->getUser();
742 $revUserId = $revUser ? $revUser->getId() : 0;
743 $revUserText = $revUser ? $revUser->getName() :
'';
744 if ( $this->isQueryableRange( $this->target ) ) {
745 $userlink =
' <span class="mw-changeslist-separator"></span> '
746 .
$lang->getDirMark()
748 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
753 if ( $revRecord->getParentId() === 0 ) {
754 $flags[] = ChangesList::flag(
'newpage' );
757 if ( $revRecord->isMinor() ) {
758 $flags[] = ChangesList::flag(
'minor' );
770 $diffHistLinks = Html::rawElement(
'span',
771 [
'class' =>
'mw-changeslist-links' ],
774 Html::rawElement(
'span', [], $difftext ) .
776 Html::rawElement(
'span', [], $histlink )
785 $classes = array_merge( $classes, $newClasses );
787 $this->hookRunner->onSpecialContributions__formatRow__flags(
793 'diffHistLinks' => $diffHistLinks,
794 'charDifference' => $chardiff,
796 'articleLink' => $link,
797 'userlink' => $userlink,
798 'logText' => $comment,
799 'topmarktext' => $topmarktext,
800 'tagSummary' => $tagSummary,
803 # Denote if username is redacted for this edit
804 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
805 $templateParams[
'rev-deleted-user-contribs'] =
806 $this->msg(
'rev-deleted-user-contribs' )->escaped();
809 $ret = $this->templateParser->processTemplate(
810 'SpecialContributionsLine',
816 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
817 $attribs = array_filter( $attribs,
818 [ Sanitizer::class,
'isReservedDataAttribute' ],
825 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
826 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
827 return "<!-- Could not format Special:Contribution row. -->\n";
829 $attribs[
'class'] = $classes;
833 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";