38 use Wikimedia\IPUtils;
112 private $revisionsOnly;
114 private $preventClickjacking =
false;
119 private $mParentLens;
130 private $actorMigration;
133 private $commentFormatter;
139 private $linkBatchFactory;
142 private $namespaceInfo;
145 private $revisionStore;
148 private $formattedComments = [];
151 private $revisions = [];
181 $services = MediaWikiServices::getInstance();
182 $dbProvider ??= $services->getDBLoadBalancerFactory();
188 $this->target = $options[
'target'] ?? $targetUser->
getName();
189 $this->targetUser = $targetUser;
195 $this->target = $options[
'target'] ??
'';
197 $this->targetUser = $services->getUserFactory()->newFromName(
198 $this->target, UserRigorOptions::RIGOR_NONE
200 if ( !$this->targetUser ) {
204 throw new InvalidArgumentException( __METHOD__ .
': the user name is too ' .
205 'broken to use even with validation disabled.' );
209 $this->
namespace = $options['namespace'] ?? '';
210 $this->tagFilter = $options[
'tagfilter'] ??
false;
211 $this->tagInvert = $options[
'tagInvert'] ??
false;
212 $this->nsInvert = $options[
'nsInvert'] ??
false;
213 $this->associated = $options[
'associated'] ??
false;
215 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
216 $this->topOnly = !empty( $options[
'topOnly'] );
217 $this->newOnly = !empty( $options[
'newOnly'] );
218 $this->hideMinor = !empty( $options[
'hideMinor'] );
219 $this->revisionsOnly = !empty( $options[
'revisionsOnly'] );
222 $this->actorMigration = $actorMigration ?? $services->getActorMigration();
223 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
232 foreach ( $msgs as $msg ) {
233 $this->messages[$msg] = $this->
msg( $msg )->escaped();
237 $startTimestamp =
'';
239 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
240 $startTimestamp = $options[
'start'] .
' 00:00:00';
242 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
243 $endTimestamp = $options[
'end'] .
' 23:59:59';
248 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
249 $this->hookRunner =
new HookRunner( $hookContainer ?? $services->getHookContainer() );
250 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
251 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
252 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
256 $query = parent::getDefaultQuery();
257 $query[
'target'] = $this->target;
272 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->
buildQueryInfo(
278 $options[
'MAX_EXECUTION_TIME'] =
279 $this->
getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
299 $data = [
$dbr->select(
300 $tables, $fields, $conds, $fname, $options, $join_conds
302 if ( !$this->revisionsOnly ) {
306 $this->hookRunner->onContribsPager__reallyDoQuery(
307 $data, $this, $offset, $limit, $order );
313 foreach ( $data as $query ) {
314 foreach ( $query as $i => $row ) {
316 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
318 $index = str_pad( (
string)$index, strlen( (
string)$limit ),
'0', STR_PAD_LEFT );
325 if ( $order === self::QUERY_ASCENDING ) {
332 $result = array_slice( $result, 0, $limit );
335 $result = array_values( $result );
349 private function getTargetTable() {
350 $dbr = $this->getDatabase();
351 $ipRangeConds = $this->targetUser->isRegistered()
352 ? null : $this->getIpRangeConds(
$dbr, $this->target );
353 if ( $ipRangeConds ) {
361 $revQuery = $this->revisionStore->getQueryInfo( [
'page',
'user' ] );
364 'fields' => array_merge(
$revQuery[
'fields'], [
'page_is_new' ] ),
371 $dbr = $this->getDatabase();
372 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds(
$dbr, $this->target ) :
null;
373 if ( $ipRangeConds ) {
375 array_unshift( $queryInfo[
'tables'],
'ip_changes' );
376 $queryInfo[
'join_conds'][
'revision'] = [
377 'JOIN', [
'rev_id = ipc_rev_id' ]
379 $queryInfo[
'conds'][] = $ipRangeConds;
382 $conds = $this->actorMigration->getWhere(
$dbr,
'rev_user', $this->targetUser );
383 $queryInfo[
'conds'][] = $conds[
'conds'];
385 if ( isset( $conds[
'orconds'][
'newactor'] ) ) {
386 $queryInfo[
'options'][
'USE INDEX'][
'revision'] =
'rev_actor_timestamp';
390 if ( $this->deletedOnly ) {
391 $queryInfo[
'conds'][] =
'rev_deleted != 0';
394 if ( $this->topOnly ) {
395 $queryInfo[
'conds'][] =
'rev_id = page_latest';
398 if ( $this->newOnly ) {
399 $queryInfo[
'conds'][] =
'rev_parent_id = 0';
402 if ( $this->hideMinor ) {
403 $queryInfo[
'conds'][] =
'rev_minor_edit = 0';
406 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
409 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
410 $queryInfo[
'conds'][] =
$dbr->bitAnd(
411 'rev_deleted', RevisionRecord::DELETED_USER
413 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
414 $queryInfo[
'conds'][] =
$dbr->bitAnd(
415 'rev_deleted', RevisionRecord::SUPPRESSED_USER
416 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
420 $indexField = $this->getIndexField();
421 if ( $indexField !==
'rev_timestamp' ) {
422 $queryInfo[
'fields'][] = $indexField;
426 $queryInfo[
'tables'],
427 $queryInfo[
'fields'],
429 $queryInfo[
'join_conds'],
430 $queryInfo[
'options'],
435 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
441 if ( $this->
namespace !==
'' ) {
442 $dbr = $this->getDatabase();
443 $selectedNS =
$dbr->addQuotes( $this->
namespace );
444 $eq_op = $this->nsInvert ?
'!=' :
'=';
445 $bool_op = $this->nsInvert ?
'AND' :
'OR';
447 if ( !$this->associated ) {
448 return [
"page_namespace $eq_op $selectedNS" ];
451 $associatedNS =
$dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
454 "page_namespace $eq_op $selectedNS " .
456 " page_namespace $eq_op $associatedNS"
469 private function getIpRangeConds( $db, $ip ) {
471 if ( !$this->isQueryableRange( $ip ) ) {
475 [ $start, $end ] = IPUtils::parseRange( $ip );
477 return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) .
' AND ' . $db->addQuotes( $end );
488 $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
490 $bits = IPUtils::parseCIDR( $ipRange )[1];
492 ( $bits ===
false ) ||
493 ( IPUtils::isIPv4( $ipRange ) && $bits < $limits[
'IPv4'] ) ||
494 ( IPUtils::isIPv6( $ipRange ) && $bits < $limits[
'IPv6'] )
510 $target = $this->getTargetTable();
513 return 'rev_timestamp';
515 return 'ipc_rev_timestamp';
518 __METHOD__ .
": Unknown value '$target' from " . static::class .
'::getTargetTable()', 0
520 return 'rev_timestamp';
528 return $this->tagFilter;
535 return $this->target;
542 return $this->newOnly;
549 return $this->namespace;
560 $target = $this->getTargetTable();
565 return [
'ipc_rev_id' ];
568 __METHOD__ .
": Unknown value '$target' from " . static::class .
'::getTargetTable()', 0
575 # Do a link batch query
576 $this->mResult->seek( 0 );
578 $this->mParentLens = [];
580 $linkBatch = $this->linkBatchFactory->newLinkBatch();
581 $isIpRange = $this->isQueryableRange( $this->target );
582 # Give some pointers to make (last) links
583 foreach ( $this->mResult as $row ) {
584 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
585 $parentRevIds[] = (int)$row->rev_parent_id;
587 if ( $this->revisionStore->isRevisionRow( $row ) ) {
588 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
593 $linkBatch->add( $row->page_namespace, $row->page_title );
594 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
597 # Fetch rev_len for revisions not already scanned above
598 $this->mParentLens += $this->revisionStore->getRevisionSizes(
599 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
601 $linkBatch->execute();
603 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
605 ->revisions( $revisions )
609 # For performance, save the revision objects for later.
610 # The array is indexed by rev_id. doBatchLookups() may be called
611 # multiple times with different results, so merge the revisions array,
612 # ignoring any duplicates.
613 $this->revisions += $revisions;
620 return "<section class='mw-pager-body'>\n";
627 return "</section>\n";
641 if ( $row instanceof stdClass && isset( $row->rev_id )
642 && isset( $this->revisions[$row->rev_id] )
644 return $this->revisions[$row->rev_id];
645 } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
646 return $this->revisionStore->newRevisionFromRow( $row, 0,
$title );
669 $linkRenderer = $this->getLinkRenderer();
674 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
675 $page = Title::newFromRow( $row );
682 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
683 if ( $revRecord && $page ) {
684 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
685 $attribs[
'data-mw-revid'] = $revRecord->getId();
687 $link = $linkRenderer->makeLink(
689 $page->getPrefixedText(),
690 [
'class' =>
'mw-contributions-title' ],
691 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
693 # Mark current revisions
699 $row->rev_id === $row->page_latest && !$row->page_is_new,
703 $this->getLinkRenderer()
705 if ( $row->rev_id === $row->page_latest ) {
706 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
707 $classes[] =
'mw-contributions-current';
709 if ( $pagerTools->shouldPreventClickjacking() ) {
710 $this->setPreventClickjacking(
true );
712 $topmarktext .= $pagerTools->toHTML();
713 # Is there a visible previous revision?
714 if ( $revRecord->getParentId() !== 0 &&
715 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
717 $difftext = $linkRenderer->makeKnownLink(
719 new HtmlArmor( $this->messages[
'diff'] ),
720 [
'class' =>
'mw-changeslist-diff' ],
723 'oldid' => $row->rev_id
727 $difftext = $this->messages[
'diff'];
729 $histlink = $linkRenderer->makeKnownLink(
731 new HtmlArmor( $this->messages[
'hist'] ),
732 [
'class' =>
'mw-changeslist-history' ],
733 [
'action' =>
'history' ]
736 if ( $row->rev_parent_id ===
null ) {
740 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
741 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
742 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
745 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
746 $parentLen = $this->mParentLens[$row->rev_parent_id];
749 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
755 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
758 $lang = $this->getLanguage();
760 $comment = $this->formattedComments[$row->rev_id];
762 if ( $comment ===
'' ) {
763 $defaultComment = $this->msg(
'changeslist-nocomment' )->escaped();
764 $comment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
767 $comment =
$lang->getDirMark() . $comment;
772 # When querying for an IP range, we want to always show user and user talk links.
774 $revUser = $revRecord->getUser();
775 $revUserId = $revUser ? $revUser->getId() : 0;
776 $revUserText = $revUser ? $revUser->getName() :
'';
777 if ( $this->isQueryableRange( $this->target ) ) {
778 $userlink =
' <span class="mw-changeslist-separator"></span> '
779 .
$lang->getDirMark()
780 . Linker::userLink( $revUserId, $revUserText );
781 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
782 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() .
' ';
786 if ( $revRecord->getParentId() === 0 ) {
790 if ( $revRecord->isMinor() ) {
794 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
803 $diffHistLinks = Html::rawElement(
'span',
804 [
'class' =>
'mw-changeslist-links' ],
807 Html::rawElement(
'span', [], $difftext ) .
809 Html::rawElement(
'span', [], $histlink )
818 $classes = array_merge( $classes, $newClasses );
820 $this->hookRunner->onSpecialContributions__formatRow__flags(
826 'diffHistLinks' => $diffHistLinks,
827 'charDifference' => $chardiff,
829 'articleLink' => $link,
830 'userlink' => $userlink,
831 'logText' => $comment,
832 'topmarktext' => $topmarktext,
833 'tagSummary' => $tagSummary,
836 # Denote if username is redacted for this edit
837 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
838 $templateParams[
'rev-deleted-user-contribs'] =
839 $this->msg(
'rev-deleted-user-contribs' )->escaped();
842 $ret = $this->templateParser->processTemplate(
843 'SpecialContributionsLine',
849 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
850 $attribs = array_filter( $attribs,
851 [ Sanitizer::class,
'isReservedDataAttribute' ],
858 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
859 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
860 return "<!-- Could not format Special:Contribution row. -->\n";
862 $attribs[
'class'] = $classes;
866 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";
874 if ( $this->
namespace || $this->deletedOnly ) {
876 return 'contributions page filtered for namespace or RevisionDeleted edits';
878 return 'contributions page unfiltered';
886 $this->setPreventClickjacking(
true );
894 $this->preventClickjacking = $enable;
901 return $this->preventClickjacking;
911 $start = $opts[
'start'] ??
'';
912 $end = $opts[
'end'] ??
'';
913 $year = $opts[
'year'] ??
'';
914 $month = $opts[
'month'] ??
'';
916 if ( $start !==
'' && $end !==
'' && $start > $end ) {
923 if ( $year !==
'' || $month !==
'' ) {
928 $legacyDateTime =
new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
929 $legacyDateTime = $legacyDateTime->modify(
'-1 day' );
934 $end = $legacyDateTime->format(
'Y-m-d' );
937 $opts[
'start'] = $start;
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
if(!defined('MW_SETUP_CALLBACK'))
static revDateLink(RevisionRecord $rev, Authority $performer, Language $lang, $title=null)
Render the date and time of a revision in the current user language based on whether the user is able...
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Marks HTML that shouldn't be escaped.
A class containing constants representing the names of configuration variables.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Interface for objects which can provide a MediaWiki context on request.
if(!isset( $args[0])) $lang