35 use Wikimedia\IPUtils;
109 private $revisionsOnly;
111 private $preventClickjacking =
false;
116 private $mParentLens;
127 private $actorMigration;
130 private $commentFormatter;
136 private $linkBatchFactory;
139 private $namespaceInfo;
142 private $revisionStore;
145 private $formattedComments = [];
148 private $revisions = [];
178 $services = MediaWikiServices::getInstance();
179 $loadBalancer ??= $services->getDBLoadBalancer();
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'] );
223 $this->actorMigration = $actorMigration ?? $services->getActorMigration();
224 parent::__construct( $context, $linkRenderer ?? $services->getLinkRenderer() );
233 foreach ( $msgs as $msg ) {
234 $this->messages[$msg] = $this->
msg( $msg )->escaped();
238 $startTimestamp =
'';
240 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
241 $startTimestamp = $options[
'start'] .
' 00:00:00';
243 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
244 $endTimestamp = $options[
'end'] .
' 23:59:59';
249 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
250 $this->hookRunner =
new HookRunner( $hookContainer ?? $services->getHookContainer() );
251 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
252 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
253 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
257 $query = parent::getDefaultQuery();
258 $query[
'target'] = $this->target;
273 [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->
buildQueryInfo(
279 $options[
'MAX_EXECUTION_TIME'] =
280 $this->
getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
300 $data = [
$dbr->select(
301 $tables, $fields, $conds, $fname, $options, $join_conds
303 if ( !$this->revisionsOnly ) {
304 $this->hookRunner->onContribsPager__reallyDoQuery(
305 $data, $this, $offset, $limit, $order );
311 foreach ( $data as $query ) {
312 foreach ( $query as $i => $row ) {
314 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
316 $index = str_pad( (
string)$index, strlen( (
string)$limit ),
'0', STR_PAD_LEFT );
323 if ( $order === self::QUERY_ASCENDING ) {
330 $result = array_slice( $result, 0, $limit );
333 $result = array_values( $result );
347 private function getTargetTable() {
348 $dbr = $this->getDatabase();
349 $ipRangeConds = $this->targetUser->isRegistered()
350 ? null : $this->getIpRangeConds(
$dbr, $this->target );
351 if ( $ipRangeConds ) {
359 $revQuery = $this->revisionStore->getQueryInfo( [
'page',
'user' ] );
362 'fields' => array_merge(
$revQuery[
'fields'], [
'page_is_new' ] ),
369 $dbr = $this->getDatabase();
370 $ipRangeConds = !$this->targetUser->isRegistered() ? $this->getIpRangeConds(
$dbr, $this->target ) :
null;
371 if ( $ipRangeConds ) {
373 array_unshift( $queryInfo[
'tables'],
'ip_changes' );
374 $queryInfo[
'join_conds'][
'revision'] = [
375 'JOIN', [
'rev_id = ipc_rev_id' ]
377 $queryInfo[
'conds'][] = $ipRangeConds;
380 $conds = $this->actorMigration->getWhere(
$dbr,
'rev_user', $this->targetUser );
381 $queryInfo[
'conds'][] = $conds[
'conds'];
383 if ( isset( $conds[
'orconds'][
'newactor'] ) ) {
384 $queryInfo[
'options'][
'USE INDEX'][
'revision'] =
'rev_actor_timestamp';
388 if ( $this->deletedOnly ) {
389 $queryInfo[
'conds'][] =
'rev_deleted != 0';
392 if ( $this->topOnly ) {
393 $queryInfo[
'conds'][] =
'rev_id = page_latest';
396 if ( $this->newOnly ) {
397 $queryInfo[
'conds'][] =
'rev_parent_id = 0';
400 if ( $this->hideMinor ) {
401 $queryInfo[
'conds'][] =
'rev_minor_edit = 0';
404 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
407 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
408 $queryInfo[
'conds'][] =
$dbr->bitAnd(
409 'rev_deleted', RevisionRecord::DELETED_USER
411 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
412 $queryInfo[
'conds'][] =
$dbr->bitAnd(
413 'rev_deleted', RevisionRecord::SUPPRESSED_USER
414 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
418 $indexField = $this->getIndexField();
419 if ( $indexField !==
'rev_timestamp' ) {
420 $queryInfo[
'fields'][] = $indexField;
424 $queryInfo[
'tables'],
425 $queryInfo[
'fields'],
427 $queryInfo[
'join_conds'],
428 $queryInfo[
'options'],
433 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
439 if ( $this->
namespace !==
'' ) {
440 $dbr = $this->getDatabase();
441 $selectedNS =
$dbr->addQuotes( $this->
namespace );
442 $eq_op = $this->nsInvert ?
'!=' :
'=';
443 $bool_op = $this->nsInvert ?
'AND' :
'OR';
445 if ( !$this->associated ) {
446 return [
"page_namespace $eq_op $selectedNS" ];
449 $associatedNS =
$dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
452 "page_namespace $eq_op $selectedNS " .
454 " page_namespace $eq_op $associatedNS"
467 private function getIpRangeConds( $db, $ip ) {
469 if ( !$this->isQueryableRange( $ip ) ) {
473 [ $start, $end ] = IPUtils::parseRange( $ip );
475 return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) .
' AND ' . $db->addQuotes( $end );
486 $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
488 $bits = IPUtils::parseCIDR( $ipRange )[1];
490 ( $bits ===
false ) ||
491 ( IPUtils::isIPv4( $ipRange ) && $bits < $limits[
'IPv4'] ) ||
492 ( IPUtils::isIPv6( $ipRange ) && $bits < $limits[
'IPv6'] )
508 $target = $this->getTargetTable();
511 return 'rev_timestamp';
513 return 'ipc_rev_timestamp';
516 __METHOD__ .
": Unknown value '$target' from " . static::class .
'::getTargetTable()', 0
518 return 'rev_timestamp';
526 return $this->tagFilter;
533 return $this->target;
540 return $this->newOnly;
547 return $this->namespace;
558 $target = $this->getTargetTable();
563 return [
'ipc_rev_id' ];
566 __METHOD__ .
": Unknown value '$target' from " . static::class .
'::getTargetTable()', 0
573 # Do a link batch query
574 $this->mResult->seek( 0 );
576 $this->mParentLens = [];
578 $linkBatch = $this->linkBatchFactory->newLinkBatch();
579 $isIpRange = $this->isQueryableRange( $this->target );
580 # Give some pointers to make (last) links
581 foreach ( $this->mResult as $row ) {
582 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
583 $parentRevIds[] = (int)$row->rev_parent_id;
585 if ( $this->revisionStore->isRevisionRow( $row ) ) {
586 $this->mParentLens[(int)$row->rev_id] = $row->rev_len;
591 $linkBatch->add( $row->page_namespace, $row->page_title );
592 $revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
595 # Fetch rev_len for revisions not already scanned above
596 $this->mParentLens += $this->revisionStore->getRevisionSizes(
597 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
599 $linkBatch->execute();
601 $this->formattedComments = $this->commentFormatter->createRevisionBatch()
603 ->revisions( $revisions )
607 # For performance, save the revision objects for later.
608 # The array is indexed by rev_id. doBatchLookups() may be called
609 # multiple times with different results, so merge the revisions array,
610 # ignoring any duplicates.
611 $this->revisions += $revisions;
618 return "<section class='mw-pager-body'>\n";
625 return "</section>\n";
639 if ( $row instanceof stdClass && isset( $row->rev_id )
640 && isset( $this->revisions[$row->rev_id] )
642 return $this->revisions[$row->rev_id];
643 } elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
644 return $this->revisionStore->newRevisionFromRow( $row, 0,
$title );
667 $linkRenderer = $this->getLinkRenderer();
672 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
680 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
681 if ( $revRecord && $page ) {
682 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
683 $attribs[
'data-mw-revid'] = $revRecord->getId();
685 $link = $linkRenderer->makeLink(
687 $page->getPrefixedText(),
688 [
'class' =>
'mw-contributions-title' ],
689 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
691 # Mark current revisions
697 $row->rev_id === $row->page_latest && !$row->page_is_new,
701 $this->getLinkRenderer()
703 if ( $row->rev_id === $row->page_latest ) {
704 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
705 $classes[] =
'mw-contributions-current';
707 if ( $pagerTools->shouldPreventClickjacking() ) {
708 $this->setPreventClickjacking(
true );
710 $topmarktext .= $pagerTools->toHTML();
711 # Is there a visible previous revision?
712 if ( $revRecord->getParentId() !== 0 &&
713 $revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() )
715 $difftext = $linkRenderer->makeKnownLink(
717 new HtmlArmor( $this->messages[
'diff'] ),
718 [
'class' =>
'mw-changeslist-diff' ],
721 'oldid' => $row->rev_id
725 $difftext = $this->messages[
'diff'];
727 $histlink = $linkRenderer->makeKnownLink(
729 new HtmlArmor( $this->messages[
'hist'] ),
730 [
'class' =>
'mw-changeslist-history' ],
731 [
'action' =>
'history' ]
734 if ( $row->rev_parent_id ===
null ) {
738 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
739 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
740 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
743 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
744 $parentLen = $this->mParentLens[$row->rev_parent_id];
747 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
753 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
756 $lang = $this->getLanguage();
758 $comment = $this->formattedComments[$row->rev_id];
760 if ( $comment ===
'' ) {
761 $defaultComment = $this->msg(
'changeslist-nocomment' )->escaped();
762 $comment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
765 $comment =
$lang->getDirMark() . $comment;
770 # When querying for an IP range, we want to always show user and user talk links.
772 $revUser = $revRecord->getUser();
773 $revUserId = $revUser ? $revUser->getId() : 0;
774 $revUserText = $revUser ? $revUser->getName() :
'';
775 if ( $this->isQueryableRange( $this->target ) ) {
776 $userlink =
' <span class="mw-changeslist-separator"></span> '
777 .
$lang->getDirMark()
778 . Linker::userLink( $revUserId, $revUserText );
779 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
780 Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() .
' ';
784 if ( $revRecord->getParentId() === 0 ) {
788 if ( $revRecord->isMinor() ) {
792 $del = Linker::getRevDeleteLink( $authority, $revRecord, $page );
802 [
'class' =>
'mw-changeslist-links' ],
816 $classes = array_merge( $classes, $newClasses );
818 $this->hookRunner->onSpecialContributions__formatRow__flags(
824 'diffHistLinks' => $diffHistLinks,
825 'charDifference' => $chardiff,
827 'articleLink' => $link,
828 'userlink' => $userlink,
829 'logText' => $comment,
830 'topmarktext' => $topmarktext,
831 'tagSummary' => $tagSummary,
834 # Denote if username is redacted for this edit
835 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
836 $templateParams[
'rev-deleted-user-contribs'] =
837 $this->msg(
'rev-deleted-user-contribs' )->escaped();
840 $ret = $this->templateParser->processTemplate(
841 'SpecialContributionsLine',
847 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
848 $attribs = array_filter( $attribs,
849 [ Sanitizer::class,
'isReservedDataAttribute' ],
856 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
857 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
858 return "<!-- Could not format Special:Contribution row. -->\n";
860 $attribs[
'class'] = $classes;
872 if ( $this->
namespace || $this->deletedOnly ) {
874 return 'contributions page filtered for namespace or RevisionDeleted edits';
876 return 'contributions page unfiltered';
884 $this->setPreventClickjacking(
true );
892 $this->preventClickjacking = $enable;
899 return $this->preventClickjacking;
909 $start = $opts[
'start'] ??
'';
910 $end = $opts[
'end'] ??
'';
911 $year = $opts[
'year'] ??
'';
912 $month = $opts[
'month'] ??
'';
914 if ( $start !==
'' && $end !==
'' && $start > $end ) {
921 if ( $year !==
'' || $month !==
'' ) {
926 $legacyDateTime =
new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
927 $legacyDateTime = $legacyDateTime->modify(
'-1 day' );
932 $end = $legacyDateTime->format(
'Y-m-d' );
935 $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'))
The persistent session ID (if any) loaded at startup.
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.
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
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...
static newFromRow( $row)
Make a Title object from a DB row.
Interface for objects which can provide a MediaWiki context on request.
if(!isset( $args[0])) $lang