isAllowed( 'checkuser' ) || !$wgUser->isAllowed( 'checkuser-log' ) ) {
parent::__construct( 'CheckUser', 'checkuser' );
} else {
parent::__construct( 'CheckUser', 'checkuser-log' );
}
$this->sk = $wgUser->getSkin();
wfLoadExtensionMessages( 'CheckUser' );
}
public function execute( $subpage ) {
global $wgRequest, $wgOut, $wgUser, $wgContLang;
$this->setHeaders();
$this->sk = $wgUser->getSkin();
// This is horribly shitty.
// Lacking formal aliases, it's tough to ensure we have compatibility.
// Links may break, which sucks.
// Language fallbacks will not always be properly utilized.
$logMatches = array(
wfMsgForContent( 'checkuser-log-subpage' ),
'Log'
);
foreach ( $logMatches as $log ) {
if ( str_replace( '_', ' ', $wgContLang->lc( $subpage ) )
== str_replace( '_ ', ' ', $wgContLang->lc( $log ) ) ) {
if ( !$wgUser->isAllowed( 'checkuser-log' ) ) {
$wgOut->permissionRequired( 'checkuser-log' );
return;
}
$this->showLog();
return;
}
}
if ( !$wgUser->isAllowed( 'checkuser' ) ) {
if ( $wgUser->isAllowed( 'checkuser-log' ) ) {
$wgOut->addWikiText( wfMsg( 'checkuser-summary' ) .
"\n\n[[" . $this->getLogSubpageTitle()->getPrefixedText() .
'|' . wfMsg( 'checkuser-showlog' ) . ']]'
);
return;
}
$wgOut->permissionRequired( 'checkuser' );
return;
}
$user = $wgRequest->getText( 'user' ) ?
$wgRequest->getText( 'user' ) : $wgRequest->getText( 'ip' );
$user = trim( $user );
$reason = $wgRequest->getText( 'reason' );
$blockreason = $wgRequest->getText( 'blockreason' );
$checktype = $wgRequest->getVal( 'checktype' );
$period = $wgRequest->getInt( 'period' );
$users = $wgRequest->getArray( 'users' );
$tag = $wgRequest->getBool( 'usetag' ) ? trim( $wgRequest->getVal( 'tag' ) ) : '';
$talkTag = $wgRequest->getBool( 'usettag' ) ? trim( $wgRequest->getVal( 'talktag' ) ) : '';
# An IPv4? An IPv6? CIDR included?
if ( IP::isIPAddress( $user ) ) {
$ip = IP::sanitizeIP( $user );
$name = '';
$xff = '';
# An IPv4/IPv6 XFF string? CIDR included?
} elseif ( preg_match( '/^(.+)\/xff$/', $user, $m ) && IP::isIPAddress( $m[1] ) ) {
$ip = '';
$name = '';
$xff = IP::sanitizeIP( $m[1] );
# A user?
} else {
$ip = '';
$name = $user;
$xff = '';
}
$this->doForm( $user, $reason, $checktype, $ip, $xff, $name, $period );
# Perform one of the various submit operations...
if ( $wgRequest->wasPosted() ) {
if ( $wgRequest->getVal( 'action' ) === 'block' ) {
$this->doMassUserBlock( $users, $blockreason, $tag, $talkTag );
} elseif ( !$this->checkReason( $reason ) ) {
$wgOut->addWikiMsg( 'checkuser-noreason' );
} elseif ( $checktype == 'subuserips' ) {
$this->doUserIPsRequest( $name, $reason, $period );
} elseif ( $xff && $checktype == 'subipedits' ) {
$this->doIPEditsRequest( $xff, true, $reason, $period );
} elseif ( $checktype == 'subipedits' ) {
$this->doIPEditsRequest( $ip, false, $reason, $period );
} elseif ( $xff && $checktype == 'subipusers' ) {
$this->doIPUsersRequest( $xff, true, $reason, $period, $tag, $talkTag );
} elseif ( $checktype == 'subipusers' ) {
$this->doIPUsersRequest( $ip, false, $reason, $period, $tag, $talkTag );
} elseif ( $checktype == 'subuseredits' ) {
$this->doUserEditsRequest( $user, $reason, $period );
}
}
# Add CIDR calculation convenience form
$this->addJsCIDRForm();
$this->addStyles();
}
/**
* As we use the same small set of messages in various methods and that
* they are called often, we call them once and save them in $this->message
*/
protected function preCacheMessages() {
// Precache various messages
if ( !isset( $this->message ) ) {
foreach ( explode( ' ', 'diff hist minoreditletter newpageletter blocklink log' ) as $msg ) {
$this->message[$msg] = wfMsgExt( $msg, array( 'escape' ) );
}
}
}
public function getLogSubpageTitle() {
if ( !isset( $this->logSubpageTitle ) ) {
$this->logSubpageTitle = $this->getTitle( wfMsgForContent( 'checkuser-log-subpage' ) );
}
return $this->logSubpageTitle;
}
protected function doForm( $user, $reason, $checktype, $ip, $xff, $name, $period ) {
global $wgOut, $wgUser;
$action = $this->getTitle()->escapeLocalUrl();
# Fill in requested type if it makes sense
$encipusers = $encipedits = $encuserips = $encuseredits = 0;
if ( $checktype == 'subipusers' && ( $ip || $xff ) ) {
$encipusers = 1;
} elseif ( $checktype == 'subipedits' && ( $ip || $xff ) ) {
$encipedits = 1;
} elseif ( $checktype == 'subuserips' && $name ) {
$encuserips = 1;
} elseif ( $checktype == 'subuseredits' && $name ) {
$encuseredits = 1;
# Defaults otherwise
} elseif ( $ip || $xff ) {
$encipedits = 1;
} else {
$encuserips = 1;
}
# Compile our nice form
# User box length should fit things like "2001:0db8:85a3:08d3:1319:8a2e:0370:7344/100/xff"
if ( $wgUser->isAllowed( 'checkuser-log' ) ) {
$wgOut->addWikiText( wfMsg( 'checkuser-summary' ) .
"\n\n[[" . $this->getLogSubpageTitle()->getPrefixedText() .
'|' . wfMsg( 'checkuser-showlog' ) . ']]'
);
}
// FIXME: use more Xml/Html methods
$form = "
';
# Output form
$wgOut->addHTML( $form );
}
/**
* Add CSS/JS
*/
protected function addStyles() {
global $wgScriptPath, $wgCheckUserStyleVersion, $wgOut;
// FIXME, use Html::
$encJSFile = htmlspecialchars( "$wgScriptPath/extensions/CheckUser/checkuser.js?$wgCheckUserStyleVersion" );
$wgOut->addScript( "" );
}
/**
* Get a selector of time period options
* @param int $selected, selected level
*/
protected function getPeriodMenu( $selected = null ) {
$s = '' . wfMsgHtml( 'checkuser-period' ) . ' ';
$s .= Xml::openElement( 'select', array( 'name' => 'period', 'id' => 'period', 'style' => 'margin-top:.2em;' ) );
$s .= Xml::option( wfMsg( 'checkuser-week-1' ), 7, $selected === 7 );
$s .= Xml::option( wfMsg( 'checkuser-week-2' ), 14, $selected === 14 );
$s .= Xml::option( wfMsg( 'checkuser-month' ), 31, $selected === 31 );
$s .= Xml::option( wfMsg( 'checkuser-all' ), 0, $selected === 0 );
$s .= Xml::closeElement( 'select' ) . "\n";
return $s;
}
/**
* Make a quick JS form for admins to calculate block ranges
*/
protected function addJsCIDRForm() {
global $wgOut;
$s = '' .
'' . wfMsgHtml( 'checkuser-cidr-label' ) . ' ';
$s .= ' ';
$s .= wfMsgHtml( 'checkuser-cidr-res' ) . ' ' .
Xml::input( 'mw-checkuser-cidr-res', 35, '', array( 'id' => 'mw-checkuser-cidr-res' ) ) .
' ';
$s .= ' ';
$wgOut->addHTML( $s );
}
/**
* FIXME: documentation incomplete
* Block a list of selected users
* @param array $users
* @param string $reason
* @param string $tag
*/
protected function doMassUserBlock( $users, $reason = '', $tag = '', $talkTag = '' ) {
global $wgOut, $wgUser, $wgCheckUserMaxBlocks, $wgLang;
if ( empty( $users ) || $wgUser->isBlocked( false ) ) {
$wgOut->addWikiMsg( 'checkuser-block-failure' );
return;
} elseif ( count( $users ) > $wgCheckUserMaxBlocks ) {
$wgOut->addWikiMsg( 'checkuser-block-limit' );
return;
} elseif ( !$reason ) {
$wgOut->addWikiMsg( 'checkuser-block-noreason' );
return;
}
$safeUsers = IPBlockForm::doMassUserBlock( $users, $reason, $tag, $talkTag );
if ( !empty( $safeUsers ) ) {
$n = count( $safeUsers );
$ulist = $wgLang->listToText( $safeUsers );
$wgOut->addWikiMsg( 'checkuser-block-success', $ulist, $wgLang->formatNum( $n ) );
} else {
$wgOut->addWikiMsg( 'checkuser-block-failure' );
}
}
protected function noMatchesMessage( $userName ) {
global $wgLang;
$dbr = wfGetDB( DB_SLAVE );
$user_id = User::idFromName( $userName );
if ( $user_id ) {
$revEdit = $dbr->selectField( 'revision',
'rev_timestamp',
array( 'rev_user' => $user_id ),
__METHOD__,
array( 'ORDER BY' => 'rev_timestamp DESC' )
);
} else {
$revEdit = $dbr->selectField( 'revision',
'rev_timestamp',
array( 'rev_user_text' => $userName ),
__METHOD__,
array( 'ORDER BY' => 'rev_timestamp DESC' )
);
}
$logEdit = 0;
if ( $user_id ) {
$logEdit = $dbr->selectField( 'logging',
'log_timestamp',
array( 'log_user' => $user_id ),
__METHOD__,
array( 'ORDER BY' => 'log_timestamp DESC' )
);
}
$lastEdit = max( $revEdit, $logEdit );
if ( $lastEdit ) {
$lastEditDate = $wgLang->date( wfTimestamp( TS_MW, $lastEdit ), true );
$lastEditTime = $wgLang->time( wfTimestamp( TS_MW, $lastEdit ), true );
// FIXME: don't pass around parsed messages
return wfMsgExt( 'checkuser-nomatch-edits', 'parse', $lastEditDate, $lastEditTime );
}
return wfMsgExt( 'checkuser-nomatch', 'parse' );
}
protected function checkReason( $reason ) {
global $wgCheckUserForceSummary;
return ( !$wgCheckUserForceSummary || strlen( $reason ) );
}
/**
* FIXME: documentation out of date
* @param string $ip ??
* @param bool $xfor ??
* @param string $reason
* Get all IPs used by a user
* Shows first and last date and number of edits
*/
protected function doUserIPsRequest( $user , $reason = '', $period = 0 ) {
global $wgOut, $wgLang;
$userTitle = Title::newFromText( $user, NS_USER );
if ( !is_null( $userTitle ) ) {
// normalize the username
$user = $userTitle->getText();
}
# IPs are passed in as a blank string
if ( !$user ) {
$wgOut->addWikiMsg( 'nouserspecified' );
return;
}
# Get ID, works better than text as user may have been renamed
$user_id = User::idFromName( $user );
# If user is not IP or nonexistent
if ( !$user_id ) {
// FIXME: addWikiMsg
$s = wfMsgExt( 'nosuchusershort', array( 'parse' ), $user );
$wgOut->addHTML( $s );
return;
}
# Record check...
if ( !$this->addLogEntry( 'userips', 'user', $user, $reason, $user_id ) ) {
// FIXME: addWikiMsg
$wgOut->addHTML( '' . wfMsgHtml( 'checkuser-log-fail' ) . '
' );
}
$dbr = wfGetDB( DB_SLAVE );
$time_conds = $this->getTimeConds( $period );
# Ordering by the latest timestamp makes a small filesort on the IP list
$cu_changes = $dbr->tableName( 'cu_changes' );
$use_index = $dbr->useIndexClause( 'cuc_user_ip_time' );
$sql = "SELECT cuc_ip,cuc_ip_hex, COUNT(*) AS count,
MIN(cuc_timestamp) AS first, MAX(cuc_timestamp) AS last
FROM $cu_changes $use_index WHERE cuc_user = $user_id AND $time_conds
GROUP BY cuc_ip,cuc_ip_hex ORDER BY last DESC LIMIT 5001";
$ret = $dbr->query( $sql, __METHOD__ );
if ( !$dbr->numRows( $ret ) ) {
$s = $this->noMatchesMessage( $user ) . "\n";
} else {
$blockip = SpecialPage::getTitleFor( 'Blockip' );
$ips_edits = array();
$counter = 0;
while ( $row = $dbr->fetchObject( $ret ) ) {
if ( $counter >= 5000 ) {
// FIXME: addWikiMSG
$wgOut->addHTML( wfMsgExt( 'checkuser-limited', array( 'parse' ) ) );
break;
}
$ips_edits[$row->cuc_ip] = $row->count;
$ips_first[$row->cuc_ip] = $row->first;
$ips_last[$row->cuc_ip] = $row->last;
$ips_hex[$row->cuc_ip] = $row->cuc_ip_hex;
++$counter;
}
// Count pinging might take some time...make sure it is there
wfSuppressWarnings();
set_time_limit( 60 );
wfRestoreWarnings();
$logs = SpecialPage::getTitleFor( 'Log' );
$s = '';
}
$wgOut->addHTML( $s );
$dbr->freeResult( $ret );
}
protected function getIPBlockInfo( $ip ) {
static $blocklist;
$blocklist = SpecialPage::getTitleFor( 'Ipblocklist' );
$block = new Block();
$block->fromMaster( false ); // use slaves
if ( $block->load( $ip, 0 ) ) {
if ( IP::isIPAddress( $block->mAddress ) && strpos( $block->mAddress, '/' ) ) {
$userpage = Title::makeTitle( NS_USER, $block->mAddress );
$blocklog = $this->sk->makeKnownLinkObj( $logs, wfMsgHtml( 'checkuser-blocked' ),
'type=block&page=' . urlencode( $userpage->getPrefixedText() ) );
return ' (' . $blocklog . ' - ' . $block->mAddress . ') ';
} elseif ( $block->mAuto ) {
$blocklog = $this->sk->makeKnownLinkObj( $blocklist, wfMsgHtml( 'checkuser-blocked' ),
'ip=' . urlencode( "#$block->mId" ) );
return ' (' . $blocklog . ') ';
} else {
$userpage = Title::makeTitle( NS_USER, $ip );
$blocklog = $this->sk->makeKnownLinkObj( $logs, wfMsgHtml( 'checkuser-blocked' ),
'type=block&page=' . urlencode( $userpage->getPrefixedText() ) );
return ' (' . $blocklog . ') ';
}
}
return '';
}
/**
* @param string $ip
* @param bool $xfor
* @param string $reason
* FIXME: $period ???
* Shows all edits in Recent Changes by this IP (or range) and who made them
*/
protected function doIPEditsRequest( $ip, $xfor = false, $reason = '', $period = 0 ) {
global $wgOut, $wgLang;
$dbr = wfGetDB( DB_SLAVE );
# Invalid IPs are passed in as a blank string
$ip_conds = $this->getIpConds( $dbr, $ip, $xfor );
if ( !$ip || $ip_conds === false ) {
$wgOut->addWikiMsg( 'badipaddress' );
return;
}
$logType = 'ipedits';
if ( $xfor ) {
$logType .= '-xff';
}
# Record check...
if ( !$this->addLogEntry( $logType, 'ip', $ip, $reason ) ) {
$wgOut->addWikiMsg( 'checkuser-log-fail' );
}
$ip_conds = $dbr->makeList( $ip_conds, LIST_AND );
$time_conds = $this->getTimeConds( $period );
$cu_changes = $dbr->tableName( 'cu_changes' );
# Ordered in descent by timestamp. Can cause large filesorts on range scans.
# Check how many rows will need sorting ahead of time to see if this is too big.
# Also, if we only show 5000, too many will be ignored as well.
$index = $xfor ? 'cuc_xff_hex_time' : 'cuc_ip_hex_time';
if ( strpos( $ip, '/' ) !== false ) {
# Quick index check only OK if no time constraint
if ( $period ) {
$rangecount = $dbr->selectField( 'cu_changes', 'COUNT(*)',
array( $ip_conds, $time_conds ),
__METHOD__,
array( 'USE INDEX' => $index ) );
} else {
$rangecount = $dbr->estimateRowCount( 'cu_changes', '*',
array( $ip_conds ),
__METHOD__,
array( 'USE INDEX' => $index ) );
}
// Sorting might take some time...make sure it is there
wfSuppressWarnings();
set_time_limit( 60 );
wfRestoreWarnings();
}
$counter = 0;
# See what is best to do after testing the waters...
if ( isset( $rangecount ) && $rangecount > 5000 ) {
$use_index = $dbr->useIndexClause( $index );
$sql = "SELECT cuc_ip_hex, COUNT(*) AS count,
MIN(cuc_timestamp) AS first, MAX(cuc_timestamp) AS last
FROM $cu_changes $use_index
WHERE $ip_conds AND $time_conds
GROUP BY cuc_ip_hex ORDER BY cuc_ip_hex LIMIT 5001";
$ret = $dbr->query( $sql, __METHOD__ );
# List out each IP that has edits
$s = wfMsgExt( 'checkuser-too-many', array( 'parse' ) );
$s .= '';
while ( $row = $ret->fetchObject() ) {
if ( $counter >= 5000 ) {
// FIXME: addWikiMsg
$wgOut->addHTML( wfMsgExt( 'checkuser-limited', array( 'parse' ) ) );
break;
}
# Convert the IP hexes into normal form
if ( strpos( $row->cuc_ip_hex, 'v6-' ) !== false ) {
$ip = substr( $row->cuc_ip_hex, 3 );
$ip = IP::HextoOctet( $ip );
} else {
$ip = long2ip( wfBaseConvert( $row->cuc_ip_hex, 16, 10, 8 ) );
}
$s .= '' . $ip . ' ';
if ( $row->first == $row->last ) {
$s .= ' (' . $wgLang->timeanddate( wfTimestamp( TS_MW, $row->first ), true ) . ') ';
} else {
$s .= ' (' . $wgLang->timeanddate( wfTimestamp( TS_MW, $row->first ), true ) .
' -- ' . $wgLang->timeanddate( wfTimestamp( TS_MW, $row->last ), true ) . ') ';
}
$s .= ' [' . $row->count . " ] \n";
++$counter;
}
$s .= ' ';
$dbr->freeResult( $ret );
$wgOut->addHTML( $s );
return;
} elseif ( isset( $rangecount ) && !$rangecount ) {
$s = $this->noMatchesMessage( $ip ) . "\n";
$wgOut->addHTML( $s );
return;
}
# OK, do the real query...
$use_index = $dbr->useIndexClause( $index );
$sql = "SELECT cuc_namespace,cuc_title,cuc_user,cuc_user_text,cuc_comment,cuc_actiontext,
cuc_timestamp,cuc_minor,cuc_page_id,cuc_type,cuc_this_oldid,cuc_last_oldid,cuc_ip,cuc_xff,cuc_agent
FROM $cu_changes $use_index WHERE $ip_conds AND $time_conds ORDER BY cuc_timestamp DESC LIMIT 5001";
$ret = $dbr->query( $sql, __METHOD__ );
if ( !$dbr->numRows( $ret ) ) {
$s = $this->noMatchesMessage( $ip ) . "\n";
} else {
# Cache common messages
$this->preCacheMessages();
# Try to optimize this query
$lb = new LinkBatch;
while ( $row = $ret->fetchObject() ) {
$userText = str_replace( ' ', '_', $row->cuc_user_text );
$lb->add( $row->cuc_namespace, $row->cuc_title );
$lb->add( NS_USER, $userText );
$lb->add( NS_USER_TALK, $userText );
}
$lb->execute();
$ret->seek( 0 );
# List out the edits
$s = '';
while ( $row = $ret->fetchObject() ) {
if ( $counter >= 5000 ) {
// FIXME: addWikiMsg
$wgOut->addHTML( wfMsgExt( 'checkuser-limited', array( 'parse' ) ) );
break;
}
$s .= $this->CUChangesLine( $row, $reason );
++$counter;
}
$s .= '
';
$dbr->freeResult( $ret );
}
$wgOut->addHTML( $s );
}
/**
* @param string $user
* @param string $reason
* Shows all edits in Recent Changes by this user
*/
protected function doUserEditsRequest( $user, $reason = '', $period = 0 ) {
global $wgOut;
$userTitle = Title::newFromText( $user, NS_USER );
if ( !is_null( $userTitle ) ) {
// normalize the username
$user = $userTitle->getText();
}
# IPs are passed in as a blank string
if ( !$user ) {
$wgOut->addWikiMsg( 'nouserspecified' );
return;
}
# Get ID, works better than text as user may have been renamed
$user_id = User::idFromName( $user );
# If user is not IP or nonexistent
if ( !$user_id ) {
$s = wfMsgExt( 'nosuchusershort', array( 'parse' ), $user );
$wgOut->addHTML( $s );
return;
}
# Record check...
if ( !$this->addLogEntry( 'useredits', 'user', $user, $reason, $user_id ) ) {
$wgOut->addHTML( '' . wfMsgHtml( 'checkuser-log-fail' ) . '
' );
}
$dbr = wfGetDB( DB_SLAVE );
$user_cond = "cuc_user = '$user_id'";
$time_conds = $this->getTimeConds( $period );
$cu_changes = $dbr->tableName( 'cu_changes' );
# Ordered in descent by timestamp. Causes large filesorts if there are many edits.
# Check how many rows will need sorting ahead of time to see if this is too big.
# If it is, sort by IP,time to avoid the filesort.
if ( $period ) {
$count = $dbr->selectField( 'cu_changes', 'COUNT(*)',
array( $user_cond, $time_conds ),
__METHOD__,
array( 'USE INDEX' => 'cuc_user_ip_time' ) );
} else {
$count = $dbr->estimateRowCount( 'cu_changes', '*',
array( $user_cond, $time_conds ),
__METHOD__,
array( 'USE INDEX' => 'cuc_user_ip_time' ) );
}
# Cache common messages
$this->preCacheMessages();
# See what is best to do after testing the waters...
if ( $count > 5000 ) {
$wgOut->addHTML( wfMsgExt( 'checkuser-limited', array( 'parse' ) ) );
$use_index = $dbr->useIndexClause( 'cuc_user_ip_time' );
$sql = "SELECT * FROM $cu_changes $use_index
WHERE $user_cond AND $time_conds
ORDER BY cuc_ip ASC, cuc_timestamp DESC LIMIT 5000";
$ret = $dbr->query( $sql, __METHOD__ );
# Try to optimize this query
$lb = new LinkBatch;
while ( $row = $ret->fetchObject() ) {
$lb->add( $row->cuc_namespace, $row->cuc_title );
}
$lb->execute();
$ret->seek( 0 );
$s = '';
while ( $row = $ret->fetchObject() ) {
if ( !$ip = htmlspecialchars( $row->cuc_ip ) ) {
continue;
}
if ( !isset( $lastIP ) ) {
$lastIP = $row->cuc_ip;
$s .= "\n$ip \n";
} elseif ( $lastIP != $row->cuc_ip ) {
$s .= "
\n$ip \n";
$lastIP = $row->cuc_ip;
unset( $this->lastdate ); // start over
}
$s .= $this->CUChangesLine( $row, $reason );
}
$s .= '
';
$dbr->freeResult( $ret );
$wgOut->addHTML( $s );
return;
}
// Sorting might take some time...make sure it is there
wfSuppressWarnings();
set_time_limit( 60 );
wfRestoreWarnings();
# OK, do the real query...
$use_index = $dbr->useIndexClause( 'cuc_user_ip_time' );
$sql = "SELECT * FROM $cu_changes $use_index
WHERE $user_cond AND $time_conds ORDER BY cuc_timestamp DESC LIMIT 5000";
$ret = $dbr->query( $sql, __METHOD__ );
if ( !$dbr->numRows( $ret ) ) {
$s = $this->noMatchesMessage( $user ) . "\n";
} else {
# Try to optimize this query
$lb = new LinkBatch;
while ( $row = $ret->fetchObject() ) {
$lb->add( $row->cuc_namespace, $row->cuc_title );
}
$lb->execute();
$ret->seek( 0 );
# List out the edits
$s = '';
while ( $row = $ret->fetchObject() ) {
$s .= $this->CUChangesLine( $row, $reason );
}
$s .= '
';
$dbr->freeResult( $ret );
}
$wgOut->addHTML( $s );
}
/**
* @param string $ip
* @param bool $xfor
* @param string $reason
* @param int $period
* @param string $tag
* @param string $talkTag
* Lists all users in recent changes who used an IP, newest to oldest down
* Outputs usernames, latest and earliest found edit date, and count
* List unique IPs used for each user in time order, list corresponding user agent
*/
protected function doIPUsersRequest( $ip, $xfor = false, $reason = '', $period = 0, $tag = '', $talkTag = '' ) {
global $wgUser, $wgOut, $wgLang;
$dbr = wfGetDB( DB_SLAVE );
# Invalid IPs are passed in as a blank string
$ip_conds = $this->getIpConds( $dbr, $ip, $xfor );
if ( !$ip || $ip_conds === false ) {
$wgOut->addWikiMsg( 'badipaddress' );
return;
}
$logType = 'ipusers';
if ( $xfor ) {
$logType .= '-xff';
}
# Log the check...
if ( !$this->addLogEntry( $logType, 'ip', $ip, $reason ) ) {
$wgOut->addHTML( '' . wfMsgHtml( 'checkuser-log-fail' ) . '
' );
}
$ip_conds = $dbr->makeList( $ip_conds, LIST_AND );
$time_conds = $this->getTimeConds( $period );
$cu_changes = $dbr->tableName( 'cu_changes' );
$index = $xfor ? 'cuc_xff_hex_time' : 'cuc_ip_hex_time';
# Ordered in descent by timestamp. Can cause large filesorts on range scans.
# Check how many rows will need sorting ahead of time to see if this is too big.
if ( strpos( $ip, '/' ) !== false ) {
# Quick index check only OK if no time constraint
if ( $period ) {
$rangecount = $dbr->selectField( 'cu_changes', 'COUNT(*)',
array( $ip_conds, $time_conds ),
__METHOD__,
array( 'USE INDEX' => $index ) );
} else {
$rangecount = $dbr->estimateRowCount( 'cu_changes', '*',
array( $ip_conds ),
__METHOD__,
array( 'USE INDEX' => $index ) );
}
// Sorting might take some time...make sure it is there
wfSuppressWarnings();
set_time_limit( 120 );
wfRestoreWarnings();
}
// Are there too many edits?
if ( isset( $rangecount ) && $rangecount > 10000 ) {
$use_index = $dbr->useIndexClause( $index );
$sql = "SELECT cuc_ip_hex, COUNT(*) AS count,
MIN(cuc_timestamp) AS first, MAX(cuc_timestamp) AS last
FROM $cu_changes $use_index WHERE $ip_conds AND $time_conds
GROUP BY cuc_ip_hex ORDER BY cuc_ip_hex LIMIT 5001";
$ret = $dbr->query( $sql, __METHOD__ );
# List out each IP that has edits
$s = '' . wfMsg( 'checkuser-too-many' ) . ' ';
$s .= '';
$counter = 0;
while ( $row = $ret->fetchObject() ) {
if ( $counter >= 5000 ) {
$wgOut->addHTML( wfMsgExt( 'checkuser-limited', array( 'parse' ) ) );
break;
}
# Convert the IP hexes into normal form
if ( strpos( $row->cuc_ip_hex, 'v6-' ) !== false ) {
$ip = substr( $row->cuc_ip_hex, 3 );
$ip = IP::HextoOctet( $ip );
} else {
$ip = long2ip( wfBaseConvert( $row->cuc_ip_hex, 16, 10, 8 ) );
}
$s .= '' . $ip . ' ';
if ( $row->first == $row->last ) {
$s .= ' (' . $wgLang->timeanddate( wfTimestamp( TS_MW, $row->first ), true ) . ') ';
} else {
$s .= ' (' . $wgLang->timeanddate( wfTimestamp( TS_MW, $row->first ), true ) .
' -- ' . $wgLang->timeanddate( wfTimestamp( TS_MW, $row->last ), true ) . ') ';
}
$s .= ' [' . $row->count . " ] \n";
++$counter;
}
$s .= ' ';
$dbr->freeResult( $ret );
$wgOut->addHTML( $s );
return;
} elseif ( isset( $rangecount ) && !$rangecount ) {
$s = $this->noMatchesMessage( $ip ) . "\n";
$wgOut->addHTML( $s );
return;
}
global $wgMemc;
# OK, do the real query...
$use_index = $dbr->useIndexClause( $index );
$sql = "SELECT cuc_user_text, cuc_timestamp, cuc_user, cuc_ip, cuc_agent, cuc_xff
FROM $cu_changes $use_index WHERE $ip_conds AND $time_conds
ORDER BY cuc_timestamp DESC LIMIT 10000";
$ret = $dbr->query( $sql, __METHOD__ );
$users_first = $users_last = $users_edits = $users_ids = array();
if ( !$dbr->numRows( $ret ) ) {
$s = $this->noMatchesMessage( $ip ) . "\n";
} else {
global $wgAuth;
while ( ( $row = $dbr->fetchObject( $ret ) ) != false ) {
if ( !array_key_exists( $row->cuc_user_text, $users_edits ) ) {
$users_last[$row->cuc_user_text] = $row->cuc_timestamp;
$users_edits[$row->cuc_user_text] = 0;
$users_ids[$row->cuc_user_text] = $row->cuc_user;
$users_infosets[$row->cuc_user_text] = array();
$users_agentsets[$row->cuc_user_text] = array();
}
$users_edits[$row->cuc_user_text] += 1;
$users_first[$row->cuc_user_text] = $row->cuc_timestamp;
# Treat blank or NULL xffs as empty strings
$xff = empty( $row->cuc_xff ) ? null : $row->cuc_xff;
$xff_ip_combo = array( $row->cuc_ip, $xff );
# Add this IP/XFF combo for this username if it's not already there
if ( !in_array( $xff_ip_combo, $users_infosets[$row->cuc_user_text] ) ) {
$users_infosets[$row->cuc_user_text][] = $xff_ip_combo;
}
# Add this agent string if it's not already there; 10 max.
if ( count( $users_agentsets[$row->cuc_user_text] ) < 10 ) {
if ( !in_array( $row->cuc_agent, $users_agentsets[$row->cuc_user_text] ) ) {
$users_agentsets[$row->cuc_user_text][] = $row->cuc_agent;
}
}
}
$dbr->freeResult( $ret );
$action = $this->getTitle()->escapeLocalURL( 'action=block' );
$s = "';
}
$wgOut->addHTML( $s );
}
protected function userBlockFlags( $ip, $userId, $user ) {
static $logs, $blocklist;
$logs = SpecialPage::getTitleFor( 'Log' );
$blocklist = SpecialPage::getTitleFor( 'Ipblocklist' );
$block = new Block();
$block->fromMaster( false ); // use slaves
$flags = array();
if ( $block->load( $ip, $userId ) ) {
// Range blocked?
if ( IP::isIPAddress( $block->mAddress ) && strpos( $block->mAddress, '/' ) ) {
$userpage = Title::makeTitle( NS_USER, $block->mAddress );
$blocklog = $this->sk->makeKnownLinkObj( $logs, wfMsgHtml( 'checkuser-blocked' ),
'type=block&page=' . urlencode( $userpage->getPrefixedText() ) );
$flags[] = '(' . $blocklog . ' - ' . $block->mAddress . ') ';
// Auto blocked?
} elseif ( $block->mAuto ) {
$blocklog = $this->sk->makeKnownLinkObj( $blocklist,
wfMsgHtml( 'checkuser-blocked' ), 'ip=' . urlencode( "#{$block->mId}" ) );
$flags[] = '(' . $blocklog . ') ';
} else {
$userpage = $user->getUserPage();
$blocklog = $this->sk->makeKnownLinkObj( $logs, wfMsgHtml( 'checkuser-blocked' ),
'type=block&page=' . urlencode( $userpage->getPrefixedText() ) );
$flags[] = '(' . $blocklog . ') ';
}
// IP that is blocked on all wikis?
} elseif ( $ip == $user->getName() && $user->isBlockedGlobally( $ip ) ) {
$flags[] = '(' . wfMsgHtml( 'checkuser-gblocked' ) . ') ';
} elseif ( self::userWasBlocked( $user->getName() ) ) {
$userpage = $user->getUserPage();
$blocklog = $this->sk->makeKnownLinkObj( $logs, wfMsgHtml( 'checkuser-wasblocked' ),
'type=block&page=' . urlencode( $userpage->getPrefixedText() ) );
$flags[] = '(' . $blocklog . ') ';
}
return $flags;
}
/**
* @param Row $row
* @param string $reason
* @return a streamlined recent changes line with IP data
*/
protected function CUChangesLine( $row, $reason ) {
global $wgLang;
static $cuTitle, $flagCache;
$cuTitle = SpecialPage::getTitleFor( 'CheckUser' );
# Add date headers as needed
$date = $wgLang->date( wfTimestamp( TS_MW, $row->cuc_timestamp ), true, true );
if ( !isset( $this->lastdate ) ) {
$this->lastdate = $date;
$line = "\n$date \n";
} elseif ( $date != $this->lastdate ) {
$line = " \n$date \n";
$this->lastdate = $date;
} else {
$line = '';
}
$line .= '';
# Create diff/hist/page links
$line .= $this->getLinksFromRow( $row );
# Show date
$line .= ' . . ' . $wgLang->time( wfTimestamp( TS_MW, $row->cuc_timestamp ), true, true ) . ' . . ';
# Userlinks
$line .= $this->sk->userLink( $row->cuc_user, $row->cuc_user_text );
$line .= $this->sk->userToolLinks( $row->cuc_user, $row->cuc_user_text );
# Get block info
if ( isset( $flagCache[$row->cuc_user_text] ) ) {
$flags = $flagCache[$row->cuc_user_text];
} else {
$user = User::newFromName( $row->cuc_user_text, false );
$ip = IP::isIPAddress( $row->cuc_user_text ) ? $row->cuc_user_text : '';
$flags = $this->userBlockFlags( $ip, $row->cuc_user, $user );
$flagCache[$row->cuc_user_text] = $flags;
}
# Add any block information
if ( count( $flags ) ) {
$line .= ' ' . implode( ' ', $flags );
}
# Action text, hackish ...
if ( $row->cuc_actiontext ) {
$line .= ' ' . $this->sk->formatComment( $row->cuc_actiontext ) . ' ';
}
# Comment
$line .= $this->sk->commentBlock( $row->cuc_comment );
$line .= ' ';
# IP
$line .= ' IP : ' . $this->sk->makeKnownLinkObj( $cuTitle,
htmlspecialchars( $row->cuc_ip ), 'user=' . urlencode( $row->cuc_ip ) . '&reason=' . urlencode( $reason ) );
# XFF
if ( $row->cuc_xff != null ) {
# Flag our trusted proxies
list( $client, $trusted ) = efGetClientIPfromXFF( $row->cuc_xff, $row->cuc_ip );
$c = $trusted ? '#F0FFF0' : '#FFFFCC';
$line .= ' ' .
'XFF : ';
$line .= $this->sk->makeKnownLinkObj( $cuTitle,
htmlspecialchars( $row->cuc_xff ),
'user=' . urlencode( $client ) . '/xff&reason=' . urlencode( $reason ) ) . ' ';
}
# User agent
$line .= ' ' .
htmlspecialchars( $row->cuc_agent ) . ' ';
$line .= " \n";
return $line;
}
/**
* @param $row
* @create diff/hist/page link
*/
protected function getLinksFromRow( $row ) {
// Log items (old format) and events to logs
if ( $row->cuc_type == RC_LOG && $row->cuc_namespace == NS_SPECIAL ) {
list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $row->cuc_title );
$logname = LogPage::logName( $logtype );
$title = Title::makeTitle( $row->cuc_namespace, $row->cuc_title );
$links = '(' . $this->sk->makeKnownLinkObj( $title, $logname ) . ')';
// Log items
} elseif ( $row->cuc_type == RC_LOG ) {
$title = Title::makeTitle( $row->cuc_namespace, $row->cuc_title );
$links = '(' . $this->sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), $this->message['log'],
wfArrayToCGI( array( 'page' => $title->getPrefixedText() ) ) ) . ')';
} else {
$title = Title::makeTitle( $row->cuc_namespace, $row->cuc_title );
# New pages
if ( $row->cuc_type == RC_NEW ) {
$links = '(' . $this->message['diff'] . ') ';
} else {
# Diff link
$links = ' (' . $this->sk->makeKnownLinkObj( $title, $this->message['diff'],
wfArrayToCGI( array(
'curid' => $row->cuc_page_id,
'diff' => $row->cuc_this_oldid,
'oldid' => $row->cuc_last_oldid ) ) ) . ') ';
}
# History link
$links .= ' (' . $this->sk->makeKnownLinkObj( $title, $this->message['hist'],
wfArrayToCGI( array(
'curid' => $row->cuc_page_id,
'action' => 'history' ) ) ) . ') . . ';
# Some basic flags
if ( $row->cuc_type == RC_NEW ) {
$links .= '' . $this->message['newpageletter'] . ' ';
}
if ( $row->cuc_minor ) {
$links .= '' . $this->message['minoreditletter'] . ' ';
}
# Page link
$links .= ' ' . $this->sk->makeLinkObj( $title );
}
return $links;
}
protected static function userWasBlocked( $name ) {
$userpage = Title::makeTitle( NS_USER, $name );
return wfGetDB( DB_SLAVE )->selectField( 'logging', '1',
array( 'log_type' => array( 'block', 'suppress' ),
'log_action' => 'block',
'log_namespace' => $userpage->getNamespace(),
'log_title' => $userpage->getDBkey() ),
__METHOD__,
array( 'USE INDEX' => 'page_time' ) );
}
/**
* Format a link to a group description page
*
* @param string $group
* @return string
*/
protected static function buildGroupLink( $group ) {
static $cache = array();
if ( !isset( $cache[$group] ) ) {
$cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
}
return $cache[$group];
}
/**
* @param Database $db
* @param string $ip
* @param string $xfor
* @return mixed array/false conditions
*/
protected function getIpConds( $db, $ip, $xfor = false ) {
$type = ( $xfor ) ? 'xff' : 'ip';
// IPv4 CIDR, 16-32 bits
if ( preg_match( '#^(\d+\.\d+\.\d+\.\d+)/(\d+)$#', $ip, $matches ) ) {
if ( $matches[2] < 16 || $matches[2] > 32 ) {
return false; // invalid
}
list( $start, $end ) = IP::parseRange( $ip );
return array( 'cuc_' . $type . '_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end ) );
} elseif ( preg_match( '#^\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}/(\d+)$#', $ip, $matches ) ) {
// IPv6 CIDR, 96-128 bits
if ( $matches[1] < 96 || $matches[1] > 128 ) {
return false; // invalid
}
list( $start, $end ) = IP::parseRange6( $ip );
return array( 'cuc_' . $type . '_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end ) );
} elseif ( preg_match( '#^(\d+)\.(\d+)\.(\d+)\.(\d+)$#', $ip ) ) {
// 32 bit IPv4
$ip_hex = IP::toHex( $ip );
return array( 'cuc_' . $type . '_hex' => $ip_hex );
} elseif ( preg_match( '#^\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}$#', $ip ) ) {
// 128 bit IPv6
$ip_hex = IP::toHex( $ip );
return array( 'cuc_' . $type . '_hex' => $ip_hex );
}
// throw away this query, incomplete IP, these don't get through the entry point anyway
return false; // invalid
}
protected function getTimeConds( $period ) {
if ( !$period ) {
return '1 = 1';
}
$dbr = wfGetDB( DB_SLAVE );
$cutoff_unixtime = time() - ( $period * 24 * 3600 );
$cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
$cutoff = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
return "cuc_timestamp > $cutoff";
}
protected function showLog() {
global $wgRequest, $wgOut;
$type = $wgRequest->getVal( 'cuSearchType' );
$target = $wgRequest->getVal( 'cuSearch' );
$year = $wgRequest->getIntOrNull( 'year' );
$month = $wgRequest->getIntOrNull( 'month' );
$error = false;
$dbr = wfGetDB( DB_SLAVE );
$searchConds = false;
$wgOut->setPageTitle( wfMsg( 'checkuser-log' ) );
$wgOut->addHTML( $this->sk->makeKnownLinkObj( $this->getTitle(), wfMsgHtml( 'checkuser-log-return' ) ) );
if ( $type === null ) {
$type = 'target';
} elseif ( $type == 'initiator' ) {
$user = User::newFromName( $target );
if ( !$user || !$user->getID() ) {
$error = 'checkuser-user-nonexistent';
} else {
$searchConds = array( 'cul_user' => $user->getID() );
}
} else /* target */ {
$type = 'target';
// Is it an IP?
list( $start, $end ) = IP::parseRange( $target );
if ( $start !== false ) {
if ( $start == $end ) {
$searchConds = array( 'cul_target_hex = ' . $dbr->addQuotes( $start ) . ' OR ' .
'(cul_range_end >= ' . $dbr->addQuotes( $start ) . ' AND ' .
'cul_range_start <= ' . $dbr->addQuotes( $end ) . ')'
);
} else {
$searchConds = array(
'(cul_target_hex >= ' . $dbr->addQuotes( $start ) . ' AND ' .
'cul_target_hex <= ' . $dbr->addQuotes( $end ) . ') OR ' .
'(cul_range_end >= ' . $dbr->addQuotes( $start ) . ' AND ' .
'cul_range_start <= ' . $dbr->addQuotes( $end ) . ')'
);
}
} else {
// Is it a user?
$user = User::newFromName( $target );
if ( $user && $user->getID() ) {
$searchConds = array(
'cul_type' => array( 'userips', 'useredits' ),
'cul_target_id' => $user->getID(),
);
} elseif ( $target ) {
$error = 'checkuser-user-nonexistent';
}
}
}
$searchTypes = array( 'initiator', 'target' );
$select = "\n";
foreach ( $searchTypes as $searchType ) {
if ( $type == $searchType ) {
$checked = 'selected="selected"';
} else {
$checked = '';
}
$caption = wfMsgHtml( 'checkuser-search-' . $searchType );
$select .= "$caption \n";
}
$select .= ' ';
$encTarget = htmlspecialchars( $target );
$msgSearch = wfMsgHtml( 'checkuser-search' );
$input = " ";
$msgSearchForm = wfMsgHtml( 'checkuser-search-form', $select, $input );
$formAction = $this->getLogSubpageTitle()->escapeLocalURL();
$msgSearchSubmit = ' ' . wfMsgHtml( 'checkuser-search-submit' ) . ' ';
$s = " \n";
$wgOut->addHTML( $s );
if ( $error !== false ) {
$wgOut->addWikiText( '' . wfMsg( $error ) . '
' );
return;
}
$pager = new CheckUserLogPager( $this, $searchConds, $year, $month );
$wgOut->addHTML(
$pager->getNavigationBar() .
$pager->getBody() .
$pager->getNavigationBar()
);
}
/**
* @return string Formatted HTML
* @param int $year
* @param int $month
*/
protected function getDateMenu( $year, $month ) {
# Offset overrides year/month selection
if ( $month && $month !== - 1 ) {
$encMonth = intval( $month );
} else {
$encMonth = '';
}
if ( $year ) {
$encYear = intval( $year );
} elseif ( $encMonth ) {
$thisMonth = intval( gmdate( 'n' ) );
$thisYear = intval( gmdate( 'Y' ) );
if ( intval( $encMonth ) > $thisMonth ) {
$thisYear--;
}
$encYear = $thisYear;
} else {
$encYear = '';
}
return Xml::label( wfMsg( 'year' ), 'year' ) . ' ' .
Xml::input( 'year', 4, $encYear, array( 'id' => 'year', 'maxlength' => 4 ) ) .
' ' .
Xml::label( wfMsg( 'month' ), 'month' ) . ' ' .
Xml::monthSelector( $encMonth, - 1 );
}
protected function addLogEntry( $logType, $targetType, $target, $reason, $targetID = 0 ) {
global $wgUser;
if ( $targetType == 'ip' ) {
list( $rangeStart, $rangeEnd ) = IP::parseRange( $target );
$targetHex = $rangeStart;
if ( $rangeStart == $rangeEnd ) {
$rangeStart = $rangeEnd = '';
}
} else {
$targetHex = $rangeStart = $rangeEnd = '';
}
$dbw = wfGetDB( DB_MASTER );
$cul_id = $dbw->nextSequenceValue( 'cu_log_cul_id_seq' );
$dbw->insert( 'cu_log',
array(
'cul_id' => $cul_id,
'cul_timestamp' => $dbw->timestamp(),
'cul_user' => $wgUser->getID(),
'cul_user_text' => $wgUser->getName(),
'cul_reason' => $reason,
'cul_type' => $logType,
'cul_target_id' => $targetID,
'cul_target_text' => $target,
'cul_target_hex' => $targetHex,
'cul_range_start' => $rangeStart,
'cul_range_end' => $rangeEnd,
), __METHOD__ );
return true;
}
}
class CheckUserLogPager extends ReverseChronologicalPager {
var $searchConds, $specialPage, $y, $m;
function __construct( $specialPage, $searchConds, $y, $m ) {
parent::__construct();
/*
$this->messages = array_map( 'wfMsg',
array( 'comma-separator', 'checkuser-log-userips', 'checkuser-log-ipedits', 'checkuser-log-ipusers',
'checkuser-log-ipedits-xff', 'checkuser-log-ipusers-xff' ) );*/
$this->getDateCond( $y, $m );
$this->searchConds = $searchConds ? $searchConds : array();
$this->specialPage = $specialPage;
}
function formatRow( $row ) {
global $wgLang;
$skin = $this->getSkin();
if ( $row->cul_reason === '' ) {
$comment = '';
} else {
$comment = $skin->commentBlock( $row->cul_reason );
}
$user = $skin->userLink( $row->cul_user, $row->user_name );
if ( $row->cul_type == 'userips' || $row->cul_type == 'useredits' ) {
$target = $skin->userLink( $row->cul_target_id, $row->cul_target_text ) .
$skin->userToolLinks( $row->cul_target_id, $row->cul_target_text );
} else {
$target = $row->cul_target_text;
}
return '' .
$wgLang->timeanddate( wfTimestamp( TS_MW, $row->cul_timestamp ), true ) .
wfMsg( 'comma-separator' ) .
wfMsg(
'checkuser-log-' . $row->cul_type,
$user,
$target
) .
$comment .
' ';
}
function getStartBody() {
if ( $this->getNumRows() ) {
return '';
} else {
return '';
}
}
function getEndBody() {
if ( $this->getNumRows() ) {
return ' ';
} else {
return '';
}
}
function getEmptyBody() {
return '' . wfMsgHtml( 'checkuser-empty' ) . '
';
}
function getQueryInfo() {
$this->searchConds[] = 'user_id = cul_user';
return array(
'tables' => array( 'cu_log', 'user' ),
'fields' => $this->selectFields(),
'conds' => $this->searchConds
);
}
function getIndexField() {
return 'cul_timestamp';
}
function getTitle() {
return $this->specialPage->getLogSubpageTitle();
}
function selectFields() {
return array(
'cul_id', 'cul_timestamp', 'cul_user', 'cul_reason', 'cul_type',
'cul_target_id', 'cul_target_text', 'user_name'
);
}
}