29 use Wikimedia\IPUtils;
148 $services = MediaWikiServices::getInstance();
149 $loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
153 $this->target = $options[
'target'] ??
'';
154 $this->
namespace = $options['namespace'] ?? '';
155 $this->tagFilter = $options[
'tagfilter'] ??
false;
156 $this->nsInvert = $options[
'nsInvert'] ??
false;
157 $this->associated = $options[
'associated'] ??
false;
159 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
160 $this->topOnly = !empty( $options[
'topOnly'] );
161 $this->newOnly = !empty( $options[
'newOnly'] );
162 $this->hideMinor = !empty( $options[
'hideMinor'] );
163 $this->revisionsOnly = !empty( $options[
'revisionsOnly'] );
170 $this->actorMigration =
$actorMigration ?? $services->getActorMigration();
180 foreach ( $msgs as $msg ) {
181 $this->messages[$msg] = $this->
msg( $msg )->escaped();
185 $startTimestamp =
'';
187 if ( isset( $options[
'start'] ) && $options[
'start'] ) {
188 $startTimestamp = $options[
'start'] .
' 00:00:00';
190 if ( isset( $options[
'end'] ) && $options[
'end'] ) {
191 $endTimestamp = $options[
'end'] .
' 23:59:59';
197 $this->hookRunner =
new HookRunner( $hookContainer ?? $services->getHookContainer() );
198 $this->revisionStore =
$revisionStore ?? $services->getRevisionStore();
199 $this->namespaceInfo =
$namespaceInfo ?? $services->getNamespaceInfo();
203 $query = parent::getDefaultQuery();
218 parent::getNavigationBar()
232 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->
buildQueryInfo(
257 $data = [
$dbr->select(
258 $tables, $fields, $conds, $fname, $options, $join_conds
260 if ( !$this->revisionsOnly ) {
261 $this->hookRunner->onContribsPager__reallyDoQuery(
262 $data, $this, $offset, $limit, $order );
268 foreach ( $data as $query ) {
269 foreach ( $query as $i => $row ) {
271 $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i;
273 $index = str_pad( $index, strlen( $limit ),
'0', STR_PAD_LEFT );
280 if ( $order === self::QUERY_ASCENDING ) {
287 $result = array_slice( $result, 0, $limit );
290 $result = array_values( $result );
305 $dbr = $this->getDatabase();
307 $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds(
$dbr, $this->target ) :
null;
308 if ( $ipRangeConds ) {
311 $conds = $this->actorMigration->getWhere(
$dbr,
'rev_user', $user );
312 if ( isset( $conds[
'orconds'][
'actor'] ) ) {
314 return 'revision_actor_temp';
322 $revQuery = $this->revisionStore->getQueryInfo( [
'page',
'user' ] );
325 'fields' => array_merge(
$revQuery[
'fields'], [
'page_is_new' ] ),
333 $dbr = $this->getDatabase();
334 $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds(
$dbr, $this->target ) :
null;
335 if ( $ipRangeConds ) {
336 $queryInfo[
'tables'][] =
'ip_changes';
337 $queryInfo[
'join_conds'][
'ip_changes'] = [
338 'LEFT JOIN', [
'ipc_rev_id = rev_id' ]
340 $queryInfo[
'conds'][] = $ipRangeConds;
343 $conds = $this->actorMigration->getWhere(
$dbr,
'rev_user', $user );
344 $queryInfo[
'conds'][] = $conds[
'conds'];
346 if ( isset( $conds[
'orconds'][
'actor'] ) ) {
348 $queryInfo[
'options'][
'USE INDEX'][
'temp_rev_user'] =
'actor_timestamp';
352 if ( $this->deletedOnly ) {
353 $queryInfo[
'conds'][] =
'rev_deleted != 0';
356 if ( $this->topOnly ) {
357 $queryInfo[
'conds'][] =
'rev_id = page_latest';
360 if ( $this->newOnly ) {
361 $queryInfo[
'conds'][] =
'rev_parent_id = 0';
364 if ( $this->hideMinor ) {
365 $queryInfo[
'conds'][] =
'rev_minor_edit = 0';
368 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
371 if ( !$this->getAuthority()->isAllowed(
'deletedhistory' ) ) {
372 $queryInfo[
'conds'][] =
$dbr->bitAnd(
373 'rev_deleted', RevisionRecord::DELETED_USER
375 } elseif ( !$this->getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
376 $queryInfo[
'conds'][] =
$dbr->bitAnd(
377 'rev_deleted', RevisionRecord::SUPPRESSED_USER
378 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
382 $indexField = $this->getIndexField();
383 if ( $indexField !==
'rev_timestamp' ) {
384 $queryInfo[
'fields'][] = $indexField;
388 $queryInfo[
'tables'],
389 $queryInfo[
'fields'],
391 $queryInfo[
'join_conds'],
392 $queryInfo[
'options'],
396 $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo );
402 if ( $this->
namespace !==
'' ) {
403 $dbr = $this->getDatabase();
404 $selectedNS =
$dbr->addQuotes( $this->
namespace );
405 $eq_op = $this->nsInvert ?
'!=' :
'=';
406 $bool_op = $this->nsInvert ?
'AND' :
'OR';
408 if ( !$this->associated ) {
409 return [
"page_namespace $eq_op $selectedNS" ];
412 $associatedNS =
$dbr->addQuotes( $this->namespaceInfo->getAssociated( $this->namespace ) );
415 "page_namespace $eq_op $selectedNS " .
417 " page_namespace $eq_op $associatedNS"
432 if ( !$this->isQueryableRange( $ip ) ) {
436 list( $start, $end ) = IPUtils::parseRange( $ip );
438 return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) .
' AND ' . $db->addQuotes( $end );
449 $limits = $this->getConfig()->get(
'RangeContributionsCIDRLimit' );
451 $bits = IPUtils::parseCIDR( $ipRange )[1];
453 ( $bits ===
false ) ||
454 ( IPUtils::isIPv4( $ipRange ) && $bits < $limits[
'IPv4'] ) ||
455 ( IPUtils::isIPv6( $ipRange ) && $bits < $limits[
'IPv6'] )
471 $target = $this->getTargetTable();
474 return 'rev_timestamp';
476 return 'ipc_rev_timestamp';
477 case 'revision_actor_temp':
478 return 'revactor_timestamp';
481 __METHOD__ .
": Unknown value '$target' from " . static::class .
'::getTargetTable()', 0
483 return 'rev_timestamp';
491 return $this->tagFilter;
498 return $this->target;
505 return $this->newOnly;
512 return $this->namespace;
523 $target = $this->getTargetTable();
528 return [
'ipc_rev_id' ];
529 case 'revision_actor_temp':
530 return [
'revactor_rev' ];
533 __METHOD__ .
": Unknown value '$target' from " . static::class .
'::getTargetTable()', 0
540 # Do a link batch query
541 $this->mResult->seek( 0 );
543 $this->mParentLens = [];
544 $batch = $this->linkBatchFactory->newLinkBatch();
545 $isIpRange = $this->isQueryableRange( $this->target );
546 # Give some pointers to make (last) links
547 foreach ( $this->mResult as $row ) {
548 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
549 $parentRevIds[] = $row->rev_parent_id;
551 if ( isset( $row->rev_id ) ) {
552 $this->mParentLens[$row->rev_id] = $row->rev_len;
557 $batch->add( $row->page_namespace, $row->page_title );
560 # Fetch rev_len for revisions not already scanned above
561 $this->mParentLens += $this->revisionStore->getRevisionSizes(
562 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
565 $this->mResult->seek( 0 );
572 return "<ul class=\"mw-contributions-list\">\n";
594 $potentialRevRecord = $this->tryCreatingRevisionRecord( $row,
$title );
595 return $potentialRevRecord ?
new Revision( $potentialRevRecord ) :
null;
616 Wikimedia\suppressWarnings();
618 $revRecord = $this->revisionStore->newRevisionFromRow( $row, 0,
$title );
619 return $revRecord->getId() ? $revRecord :
null;
620 }
catch ( Exception $e ) {
623 Wikimedia\restoreWarnings();
644 $linkRenderer = $this->getLinkRenderer();
649 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
652 $revRecord = $this->tryCreatingRevisionRecord( $row, $page );
654 $attribs[
'data-mw-revid'] = $revRecord->getId();
656 $link = $linkRenderer->makeLink(
658 $page->getPrefixedText(),
659 [
'class' =>
'mw-contributions-title' ],
660 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
662 # Mark current revisions
666 if ( $row->rev_id === $row->page_latest ) {
667 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
668 $classes[] =
'mw-contributions-current';
670 if ( !$row->page_is_new &&
671 $this->getAuthority()->probablyCan(
'rollback', $page ) &&
672 $this->getAuthority()->probablyCan(
'edit', $page )
674 $this->preventClickjacking();
682 # Is there a visible previous revision?
683 if ( $revRecord->getParentId() !== 0 &&
684 RevisionRecord::userCanBitfield(
685 $revRecord->getVisibility(),
686 RevisionRecord::DELETED_TEXT,
690 $difftext = $linkRenderer->makeKnownLink(
692 new HtmlArmor( $this->messages[
'diff'] ),
693 [
'class' =>
'mw-changeslist-diff' ],
696 'oldid' => $row->rev_id
700 $difftext = $this->messages[
'diff'];
702 $histlink = $linkRenderer->makeKnownLink(
704 new HtmlArmor( $this->messages[
'hist'] ),
705 [
'class' =>
'mw-changeslist-history' ],
706 [
'action' =>
'history' ]
709 if ( $row->rev_parent_id ===
null ) {
713 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
715 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
718 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
719 $parentLen = $this->mParentLens[$row->rev_parent_id];
722 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
728 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
731 $lang = $this->getLanguage();
735 # When querying for an IP range, we want to always show user and user talk links.
737 $revUser = $revRecord->getUser();
738 $revUserId = $revUser ? $revUser->getId() : 0;
739 $revUserText = $revUser ? $revUser->getName() :
'';
740 if ( $this->isQueryableRange( $this->target ) ) {
741 $userlink =
' <span class="mw-changeslist-separator"></span> '
742 .
$lang->getDirMark()
744 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
749 if ( $revRecord->getParentId() === 0 ) {
753 if ( $revRecord->isMinor() ) {
767 [
'class' =>
'mw-changeslist-links' ],
781 $classes = array_merge( $classes, $newClasses );
783 $this->hookRunner->onSpecialContributions__formatRow__flags(
789 'diffHistLinks' => $diffHistLinks,
790 'charDifference' => $chardiff,
792 'articleLink' => $link,
793 'userlink' => $userlink,
794 'logText' => $comment,
795 'topmarktext' => $topmarktext,
796 'tagSummary' => $tagSummary,
799 # Denote if username is redacted for this edit
800 if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
801 $templateParams[
'rev-deleted-user-contribs'] =
802 $this->msg(
'rev-deleted-user-contribs' )->escaped();
805 $ret = $this->templateParser->processTemplate(
806 'SpecialContributionsLine',
812 $this->hookRunner->onContributionsLineEnding( $this, $ret, $row, $classes, $attribs );
813 $attribs = array_filter( $attribs,
814 [ Sanitizer::class,
'isReservedDataAttribute' ],
821 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
822 wfDebug(
"Dropping Special:Contribution row that could not be formatted" );
823 return "<!-- Could not format Special:Contribution row. -->\n";
825 $attribs[
'class'] = $classes;
837 if ( $this->
namespace || $this->deletedOnly ) {
839 return 'contributions page filtered for namespace or RevisionDeleted edits';
841 return 'contributions page unfiltered';
846 $this->preventClickjacking =
true;
853 return $this->preventClickjacking;
863 $start = $opts[
'start'] ??
'';
864 $end = $opts[
'end'] ??
'';
865 $year = $opts[
'year'] ??
'';
866 $month = $opts[
'month'] ??
'';
868 if ( $start !==
'' && $end !==
'' && $start > $end ) {
875 if ( $year !==
'' || $month !==
'' ) {
880 $legacyDateTime =
new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
881 $legacyDateTime = $legacyDateTime->modify(
'-1 day' );
886 $end = $legacyDateTime->format(
'Y-m-d' );
889 $opts[
'start'] = $start;