107 $this->target = $options[
'target'] ??
'';
108 $this->
namespace = $options['namespace'] ?? '';
109 $this->tagFilter = $options[
'tagfilter'] ??
false;
110 $this->nsInvert = $options[
'nsInvert'] ??
false;
111 $this->associated = $options[
'associated'] ??
false;
113 $this->deletedOnly = !empty( $options[
'deletedOnly'] );
114 $this->topOnly = !empty( $options[
'topOnly'] );
115 $this->newOnly = !empty( $options[
'newOnly'] );
116 $this->hideMinor = !empty( $options[
'hideMinor'] );
127 foreach ( $msgs as $msg ) {
128 $this->messages[$msg] = $this->
msg( $msg )->escaped();
132 $startTimestamp =
'';
134 if ( $options[
'start'] ) {
135 $startTimestamp = $options[
'start'] .
' 00:00:00';
137 if ( $options[
'end'] ) {
138 $endTimestamp = $options[
'end'] .
' 23:59:59';
147 $this->templateParser =
new TemplateParser();
151 $query = parent::getDefaultQuery();
165 return Html::rawElement(
'p', [
'class' =>
'mw-pager-navigation-bar' ],
166 parent::getNavigationBar()
180 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->
buildQueryInfo(
204 $data = [ $this->mDb->select(
205 $tables, $fields, $conds, $fname, $options, $join_conds
208 'ContribsPager::reallyDoQuery',
209 [ &$data, $this, $offset, $limit, $order ]
215 foreach ( $data as $query ) {
216 foreach ( $query as $i => $row ) {
223 if ( $order === self::QUERY_ASCENDING ) {
230 $result = array_slice( $result, 0, $limit );
233 $result = array_values( $result );
249 $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) :
null;
250 if ( $ipRangeConds ) {
254 if ( isset( $conds[
'orconds'][
'actor'] ) ) {
256 return 'revision_actor_temp';
267 'fields' => array_merge(
$revQuery[
'fields'], [
'page_is_new' ] ),
272 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
276 $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) :
null;
277 if ( $ipRangeConds ) {
278 $queryInfo[
'tables'][] =
'ip_changes';
279 $queryInfo[
'join_conds'][
'ip_changes'] = [
280 'LEFT JOIN', [
'ipc_rev_id = rev_id' ]
282 $queryInfo[
'conds'][] = $ipRangeConds;
286 $queryInfo[
'conds'][] = $conds[
'conds'];
288 if ( isset( $conds[
'orconds'][
'actor'] ) ) {
290 $queryInfo[
'options'][
'USE INDEX'][
'temp_rev_user'] =
'actor_timestamp';
292 $queryInfo[
'options'][
'USE INDEX'][
'revision'] =
293 isset( $conds[
'orconds'][
'userid'] ) ?
'user_timestamp' :
'usertext_timestamp';
297 if ( $this->deletedOnly ) {
298 $queryInfo[
'conds'][] =
'rev_deleted != 0';
301 if ( $this->topOnly ) {
302 $queryInfo[
'conds'][] =
'rev_id = page_latest';
305 if ( $this->newOnly ) {
306 $queryInfo[
'conds'][] =
'rev_parent_id = 0';
309 if ( $this->hideMinor ) {
310 $queryInfo[
'conds'][] =
'rev_minor_edit = 0';
314 $queryInfo[
'conds'] = array_merge( $queryInfo[
'conds'], $this->getNamespaceCond() );
317 if ( !$permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
318 $queryInfo[
'conds'][] = $this->mDb->bitAnd(
319 'rev_deleted', RevisionRecord::DELETED_USER
321 } elseif ( !$permissionManager->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' ) ) {
322 $queryInfo[
'conds'][] = $this->mDb->bitAnd(
323 'rev_deleted', RevisionRecord::SUPPRESSED_USER
324 ) .
' != ' . RevisionRecord::SUPPRESSED_USER;
328 $indexField = $this->getIndexField();
329 if ( $indexField !==
'rev_timestamp' ) {
330 $queryInfo[
'fields'][] = $indexField;
334 $queryInfo[
'tables'],
335 $queryInfo[
'fields'],
337 $queryInfo[
'join_conds'],
338 $queryInfo[
'options'],
344 Hooks::run(
'ContribsPager::getQueryInfo', [ &$pager, &$queryInfo ] );
350 if ( $this->
namespace !==
'' ) {
351 $selectedNS = $this->mDb->addQuotes( $this->
namespace );
352 $eq_op = $this->nsInvert ?
'!=' :
'=';
353 $bool_op = $this->nsInvert ?
'AND' :
'OR';
355 if ( !$this->associated ) {
356 return [
"page_namespace $eq_op $selectedNS" ];
359 $associatedNS = $this->mDb->addQuotes(
360 MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociated( $this->
namespace )
364 "page_namespace $eq_op $selectedNS " .
366 " page_namespace $eq_op $associatedNS"
381 if ( !$this->isQueryableRange( $ip ) ) {
387 return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) .
' AND ' . $db->addQuotes( $end );
398 $limits = $this->getConfig()->get(
'RangeContributionsCIDRLimit' );
402 ( $bits ===
false ) ||
403 (
IP::isIPv4( $ipRange ) && $bits < $limits[
'IPv4'] ) ||
404 (
IP::isIPv6( $ipRange ) && $bits < $limits[
'IPv6'] )
420 $target = $this->getTargetTable();
423 return 'rev_timestamp';
425 return 'ipc_rev_timestamp';
426 case 'revision_actor_temp':
427 return 'revactor_timestamp';
430 __METHOD__ .
": Unknown value '$target' from " . static::class .
'::getTargetTable()', 0
432 return 'rev_timestamp';
440 return $this->tagFilter;
447 return $this->target;
454 return $this->newOnly;
461 return $this->namespace;
472 $target = $this->getTargetTable();
477 return [
'ipc_rev_id' ];
478 case 'revision_actor_temp':
479 return [
'revactor_rev' ];
482 __METHOD__ .
": Unknown value '$target' from " . static::class .
'::getTargetTable()', 0
489 # Do a link batch query
490 $this->mResult->seek( 0 );
492 $this->mParentLens = [];
494 $isIpRange = $this->isQueryableRange( $this->target );
495 # Give some pointers to make (last) links
496 foreach ( $this->mResult as $row ) {
497 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
498 $parentRevIds[] = $row->rev_parent_id;
500 if ( isset( $row->rev_id ) ) {
501 $this->mParentLens[$row->rev_id] = $row->rev_len;
506 $batch->add( $row->page_namespace, $row->page_title );
509 # Fetch rev_len for revisions not already scanned above
512 array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
515 $this->mResult->seek( 0 );
522 return "<ul class=\"mw-contributions-list\">\n";
548 Wikimedia\suppressWarnings();
551 $validRevision = (bool)$rev->getId();
552 }
catch ( Exception $e ) {
553 $validRevision =
false;
555 Wikimedia\restoreWarnings();
556 return $validRevision ? $rev :
null;
576 $linkRenderer = $this->getLinkRenderer();
577 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
582 if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
585 $rev = $this->tryToCreateValidRevision( $row, $page );
587 $attribs[
'data-mw-revid'] = $rev->getId();
589 $link = $linkRenderer->makeLink(
591 $page->getPrefixedText(),
592 [
'class' =>
'mw-contributions-title' ],
593 $page->isRedirect() ? [
'redirect' =>
'no' ] : []
595 # Mark current revisions
599 if ( $row->rev_id === $row->page_latest ) {
600 $topmarktext .=
'<span class="mw-uctop">' . $this->messages[
'uctop'] .
'</span>';
601 $classes[] =
'mw-contributions-current';
603 if ( !$row->page_is_new &&
604 $permissionManager->quickUserCan(
'rollback', $user, $page ) &&
605 $permissionManager->quickUserCan(
'edit', $user, $page )
607 $this->preventClickjacking();
612 # Is there a visible previous revision?
613 if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) {
614 $difftext = $linkRenderer->makeKnownLink(
616 new HtmlArmor( $this->messages[
'diff'] ),
617 [
'class' =>
'mw-changeslist-diff' ],
620 'oldid' => $row->rev_id
624 $difftext = $this->messages[
'diff'];
626 $histlink = $linkRenderer->makeKnownLink(
628 new HtmlArmor( $this->messages[
'hist'] ),
629 [
'class' =>
'mw-changeslist-history' ],
630 [
'action' =>
'history' ]
633 if ( $row->rev_parent_id ===
null ) {
637 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
639 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
642 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
643 $parentLen = $this->mParentLens[$row->rev_parent_id];
646 $chardiff =
' <span class="mw-changeslist-separator"></span> ';
652 $chardiff .=
' <span class="mw-changeslist-separator"></span> ';
655 $lang = $this->getLanguage();
659 # When querying for an IP range, we want to always show user and user talk links.
661 if ( $this->isQueryableRange( $this->target ) ) {
662 $userlink =
' <span class="mw-changeslist-separator"></span> '
663 .
$lang->getDirMark()
665 $userlink .=
' ' . $this->msg(
'parentheses' )->rawParams(
670 if ( $rev->getParentId() === 0 ) {
674 if ( $rev->isMinor() ) {
687 $diffHistLinks = Html::rawElement(
'span',
688 [
'class' =>
'mw-changeslist-links' ],
691 Html::rawElement(
'span', [], $difftext ) .
693 Html::rawElement(
'span', [], $histlink )
702 $classes = array_merge( $classes, $newClasses );
709 'diffHistLinks' => $diffHistLinks,
710 'charDifference' => $chardiff,
712 'articleLink' => $link,
713 'userlink' => $userlink,
714 'logText' => $comment,
715 'topmarktext' => $topmarktext,
716 'tagSummary' => $tagSummary,
719 # Denote if username is redacted for this edit
720 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
721 $templateParams[
'rev-deleted-user-contribs'] =
722 $this->msg(
'rev-deleted-user-contribs' )->escaped();
725 $ret = $this->templateParser->processTemplate(
726 'SpecialContributionsLine',
732 Hooks::run(
'ContributionsLineEnding', [ $this, &$ret, $row, &$classes, &$attribs ] );
733 $attribs = array_filter( $attribs,
734 [ Sanitizer::class,
'isReservedDataAttribute' ],
741 if ( $classes === [] && $attribs === [] && $ret ===
'' ) {
742 wfDebug(
"Dropping Special:Contribution row that could not be formatted\n" );
743 return "<!-- Could not format Special:Contribution row. -->\n";
745 $attribs[
'class'] = $classes;
749 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";
757 if ( $this->
namespace || $this->deletedOnly ) {
759 return 'contributions page filtered for namespace or RevisionDeleted edits';
761 return 'contributions page unfiltered';
766 $this->preventClickjacking =
true;
773 return $this->preventClickjacking;
783 $start = $opts[
'start'] ??
'';
784 $end = $opts[
'end'] ??
'';
785 $year = $opts[
'year'] ??
'';
786 $month = $opts[
'month'] ??
'';
788 if ( $start !==
'' && $end !==
'' && $start > $end ) {
795 if ( $year !==
'' || $month !==
'' ) {
800 $legacyDateTime =
new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
801 $legacyDateTime = $legacyDateTime->modify(
'-1 day' );
806 $end = $legacyDateTime->format(
'Y-m-d' );
809 $opts[
'start'] = $start;