}
$this->getDateRangeCond( $startTimestamp, $endTimestamp );
+ // This property on IndexPager is set by $this->getIndexField() in parent::__construct().
+ // We need to reassign it here so that it is used when the actual query is ran.
+ $this->mIndexField = $this->getIndexField();
+
// Most of this code will use the 'contributions' group DB, which can map to replica DBs
// with extra user based indexes or partioning by user. The additional metadata
// queries should use a regular replica DB since the lookup pattern is not all by user.
'join_conds' => $join_cond
];
+ // For IPv6, we use ipc_rev_timestamp on ip_changes as the index field,
+ // which will be referenced when parsing the results of a query.
+ if ( self::isQueryableRange( $this->target ) ) {
+ $queryInfo['fields'][] = 'ipc_rev_timestamp';
+ }
+
ChangeTags::modifyDisplayQuery(
$queryInfo['tables'],
$queryInfo['fields'],
$condition['rev_user'] = $uid;
$index = 'user_timestamp';
} else {
- $condition['rev_user_text'] = $this->target;
- $index = 'usertext_timestamp';
+ $ipRangeConds = $this->getIpRangeConds( $this->mDb, $this->target );
+
+ if ( $ipRangeConds ) {
+ $tables[] = 'ip_changes';
+ $join_conds['ip_changes'] = [
+ 'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
+ ];
+ $condition[] = $ipRangeConds;
+ } else {
+ $condition['rev_user_text'] = $this->target;
+ $index = 'usertext_timestamp';
+ }
}
}
return [];
}
- function getIndexField() {
- return 'rev_timestamp';
+ /**
+ * Get SQL conditions for an IP range, if applicable
+ * @param IDatabase $db
+ * @param string $ip The IP address or CIDR
+ * @return string|false SQL for valid IP ranges, false if invalid
+ */
+ private function getIpRangeConds( $db, $ip ) {
+ // First make sure it is a valid range and they are not outside the CIDR limit
+ if ( !$this->isQueryableRange( $ip ) ) {
+ return false;
+ }
+
+ list( $start, $end ) = IP::parseRange( $ip );
+
+ return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
+ }
+
+ /**
+ * Is the given IP a range and within the CIDR limit?
+ *
+ * @param string $ipRange
+ * @return bool True if it is valid
+ * @since 1.30
+ */
+ public function isQueryableRange( $ipRange ) {
+ $limits = $this->getConfig()->get( 'RangeContributionsCIDRLimit' );
+
+ $bits = IP::parseCIDR( $ipRange )[1];
+ if (
+ ( $bits === false ) ||
+ ( IP::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
+ ( IP::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Override of getIndexField() in IndexPager.
+ * For IP ranges, it's faster to use the replicated ipc_rev_timestamp
+ * on the `ip_changes` table than the rev_timestamp on the `revision` table.
+ * @return string Name of field
+ */
+ public function getIndexField() {
+ if ( $this->isQueryableRange( $this->target ) ) {
+ return 'ipc_rev_timestamp';
+ } else {
+ return 'rev_timestamp';
+ }
}
function doBatchLookups() {
$parentRevIds = [];
$this->mParentLens = [];
$batch = new LinkBatch();
+ $isIpRange = $this->isQueryableRange( $this->target );
# Give some pointers to make (last) links
foreach ( $this->mResult as $row ) {
if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
if ( $this->contribs === 'newbie' ) { // multiple users
$batch->add( NS_USER, $row->user_name );
$batch->add( NS_USER_TALK, $row->user_name );
+ } elseif ( $isIpRange ) {
+ // If this is an IP range, batch the IP's talk page
+ $batch->add( NS_USER_TALK, $row->rev_user_text );
}
$batch->add( $row->page_namespace, $row->page_title );
}
* @return string
*/
function formatRow( $row ) {
-
$ret = '';
$classes = [];
+ $attribs = [];
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
MediaWiki\restoreWarnings();
if ( $validRevision ) {
- $classes = [];
+ $attribs['data-mw-revid'] = $rev->getId();
$page = Title::newFromRow( $row );
$link = $linkRenderer->makeLink(
# Mark current revisions
$topmarktext = '';
$user = $this->getUser();
+
if ( $row->rev_id === $row->page_latest ) {
$topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
$classes[] = 'mw-contributions-current';
# Show user names for /newbies as there may be different users.
# Note that only unprivileged users have rows with hidden user names excluded.
+ # When querying for an IP range, we want to always show user and user talk links.
$userlink = '';
- if ( $this->contribs == 'newbie' && !$rev->isDeleted( Revision::DELETED_USER ) ) {
+ if ( ( $this->contribs == 'newbie' && !$rev->isDeleted( Revision::DELETED_USER ) )
+ || $this->isQueryableRange( $this->target ) ) {
$userlink = ' . . ' . $lang->getDirMark()
. Linker::userLink( $rev->getUser(), $rev->getUserText() );
$userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
}
// Let extensions add data
- Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes ] );
+ Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes, &$attribs ] );
+ $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] );
// TODO: Handle exceptions in the catch block above. Do any extensions rely on
// receiving empty rows?
- if ( $classes === [] && $ret === '' ) {
+ if ( $classes === [] && $attribs === [] && $ret === '' ) {
wfDebug( "Dropping Special:Contribution row that could not be formatted\n" );
return "<!-- Could not format Special:Contribution row. -->\n";
}
+ $attribs['class'] = $classes;
// FIXME: The signature of the ContributionsLineEnding hook makes it
// very awkward to move this LI wrapper into the template.
- return Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n";
+ return Html::rawElement( 'li', $attribs, $ret ) . "\n";
}
/**
* @return array Options array with processed start and end date filter options
*/
public static function processDateFilter( $opts ) {
- $start = $opts['start'] ?: '';
- $end = $opts['end'] ?: '';
- $year = $opts['year'] ?: '';
- $month = $opts['month'] ?: '';
+ $start = isset( $opts['start'] ) ? $opts['start'] : '';
+ $end = isset( $opts['end'] ) ? $opts['end'] : '';
+ $year = isset( $opts['year'] ) ? $opts['year'] : '';
+ $month = isset( $opts['month'] ) ? $opts['month'] : '';
if ( $start !== '' && $end !== '' && $start > $end ) {
$temp = $start;