diff options
Diffstat (limited to 'extensions/CheckUser/CheckUser_body.php')
-rw-r--r-- | extensions/CheckUser/CheckUser_body.php | 1432 |
1 files changed, 1432 insertions, 0 deletions
diff --git a/extensions/CheckUser/CheckUser_body.php b/extensions/CheckUser/CheckUser_body.php new file mode 100644 index 00000000..ee9bd7d5 --- /dev/null +++ b/extensions/CheckUser/CheckUser_body.php @@ -0,0 +1,1432 @@ +<?php + +if ( !defined( 'MEDIAWIKI' ) ) { + echo "CheckUser extension\n"; + exit( 1 ); +} + +class CheckUser extends SpecialPage { + /** + * Constructor -- set up the new special page + */ + public function __construct() { + global $wgUser; + if ( $wgUser->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 = "<form name='checkuserform' id='checkuserform' action=\"$action\" method='post'>"; + $form .= '<fieldset><legend>' . wfMsgHtml( 'checkuser-query' ) . '</legend>'; + $form .= '<table border="0" cellpadding="2"><tr>'; + $form .= '<td>' . wfMsgHtml( 'checkuser-target' ) . '</td>'; + $form .= '<td>' . Xml::input( 'user', 46, $user, array( 'id' => 'checktarget' ) ); + $form .= ' ' . $this->getPeriodMenu( $period ) . '</td>'; + $form .= '</tr><tr>'; + $form .= '<td></td><td class="checkuserradios"><table border="0" cellpadding="3"><tr>'; + $form .= '<td>' . Xml::radio( 'checktype', 'subuserips', $encuserips, array( 'id' => 'subuserips' ) ); + $form .= ' ' . Xml::label( wfMsg( 'checkuser-ips' ), 'subuserips' ) . '</td>'; + $form .= '<td>' . Xml::radio( 'checktype', 'subipedits', $encipedits, array( 'id' => 'subipedits' ) ); + $form .= ' ' . Xml::label( wfMsg( 'checkuser-edits' ), 'subipedits' ) . '</td>'; + $form .= '<td>' . Xml::radio( 'checktype', 'subipusers', $encipusers, array( 'id' => 'subipusers' ) ); + $form .= ' ' . Xml::label( wfMsg( 'checkuser-users' ), 'subipusers' ) . '</td>'; + $form .= '<td>' . Xml::radio( 'checktype', 'subuseredits', $encuseredits, array( 'id' => 'subuseredits' ) ); + $form .= ' ' . Xml::label( wfMsg( 'checkuser-account' ), 'subuseredits' ) . '</td>'; + $form .= '</tr></table></td>'; + $form .= '</tr><tr>'; + $form .= '<td>' . wfMsgHtml( 'checkuser-reason' ) . '</td>'; + $form .= '<td>' . Xml::input( 'reason', 46, $reason, array( 'maxlength' => '150', 'id' => 'checkreason' ) ); + $form .= '   ' . Xml::submitButton( wfMsg( 'checkuser-check' ), + array( 'id' => 'checkusersubmit', 'name' => 'checkusersubmit' ) ) . '</td>'; + $form .= '</tr></table></fieldset></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( "<script type=\"text/javascript\" src=\"$encJSFile\"></script>" ); + } + + /** + * Get a selector of time period options + * @param int $selected, selected level + */ + protected function getPeriodMenu( $selected = null ) { + $s = '<label for="period">' . wfMsgHtml( 'checkuser-period' ) . '</label> '; + $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 = '<fieldset id="mw-checkuser-cidrform" style="display:none; clear:both;">' . + '<legend>' . wfMsgHtml( 'checkuser-cidr-label' ) . '</legend>'; + $s .= '<textarea id="mw-checkuser-iplist" rows="5" cols="50" onkeyup="updateCIDRresult()" onclick="updateCIDRresult()"></textarea><br />'; + $s .= wfMsgHtml( 'checkuser-cidr-res' ) . ' ' . + Xml::input( 'mw-checkuser-cidr-res', 35, '', array( 'id' => 'mw-checkuser-cidr-res' ) ) . + ' <strong id="mw-checkuser-ipnote"></strong>'; + $s .= '</fieldset>'; + $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( '<p>' . wfMsgHtml( 'checkuser-log-fail' ) . '</p>' ); + } + $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 = '<div id="checkuserresults"><ul>'; + foreach ( $ips_edits as $ip => $edits ) { + $s .= '<li>'; + $s .= '<a href="' . + $this->getTitle()->escapeLocalURL( 'user=' . urlencode( $ip ) . '&reason=' . urlencode( $reason ) ) . '">' . + htmlspecialchars( $ip ) . '</a>'; + $s .= ' (<a href="' . $blockip->escapeLocalURL( 'ip=' . urlencode( $ip ) ) . '">' . + wfMsgHtml( 'blocklink' ) . '</a>)'; + if ( $ips_first[$ip] == $ips_last[$ip] ) { + $s .= ' (' . $wgLang->timeanddate( wfTimestamp( TS_MW, $ips_first[$ip] ), true ) . ') '; + } else { + $s .= ' (' . $wgLang->timeanddate( wfTimestamp( TS_MW, $ips_first[$ip] ), true ) . + ' -- ' . $wgLang->timeanddate( wfTimestamp( TS_MW, $ips_last[$ip] ), true ) . ') '; + } + $s .= ' <strong>[' . $edits . ']</strong>'; + + # If we get some results, it helps to know if the IP in general + # has a lot more edits, e.g. "tip of the iceberg"... + $ipedits = $dbr->estimateRowCount( 'cu_changes', '*', + array( 'cuc_ip_hex' => $ips_hex[$ip], $time_conds ), + __METHOD__ ); + # If small enough, get a more accurate count + if ( $ipedits <= 1000 ) { + $ipedits = $dbr->selectField( 'cu_changes', 'COUNT(*)', + array( 'cuc_ip_hex' => $ips_hex[$ip], $time_conds ), + __METHOD__ ); + } + if ( $ipedits > $ips_edits[$ip] ) { + $s .= ' <i>(' . wfMsgHtml( 'checkuser-ipeditcount', $ipedits ) . ')</i>'; + } + + # If this IP is blocked, give a link to the block log + $s .= $this->getIPBlockInfo( $ip ); + $s .= '<div style="margin-left:5%">'; + $s .= '<small>' . wfMsgExt( 'checkuser-toollinks', array( 'parseinline' ), urlencode( $ip ) ) . '</small>'; + $s .= '</div>'; + $s .= "</li>\n"; + } + $s .= '</ul></div>'; + } + $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 ' <strong>(' . $blocklog . ' - ' . $block->mAddress . ')</strong>'; + } elseif ( $block->mAuto ) { + $blocklog = $this->sk->makeKnownLinkObj( $blocklist, wfMsgHtml( 'checkuser-blocked' ), + 'ip=' . urlencode( "#$block->mId" ) ); + return ' <strong>(' . $blocklog . ')</strong>'; + } else { + $userpage = Title::makeTitle( NS_USER, $ip ); + $blocklog = $this->sk->makeKnownLinkObj( $logs, wfMsgHtml( 'checkuser-blocked' ), + 'type=block&page=' . urlencode( $userpage->getPrefixedText() ) ); + return ' <strong>(' . $blocklog . ')</strong>'; + } + } + 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 .= '<ol>'; + 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 .= '<li><a href="' . + $this->getTitle()->escapeLocalURL( 'user=' . urlencode( $ip ) . '&reason=' . urlencode( $reason ) . '&checktype=subipusers' ) . + '">' . $ip . '</a>'; + 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 .= ' [<strong>' . $row->count . "</strong>]</li>\n"; + ++$counter; + } + $s .= '</ol>'; + $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 = '<div id="checkuserresults">'; + while ( $row = $ret->fetchObject() ) { + if ( $counter >= 5000 ) { + // FIXME: addWikiMsg + $wgOut->addHTML( wfMsgExt( 'checkuser-limited', array( 'parse' ) ) ); + break; + } + $s .= $this->CUChangesLine( $row, $reason ); + ++$counter; + } + $s .= '</ul></div>'; + $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( '<p>' . wfMsgHtml( 'checkuser-log-fail' ) . '</p>' ); + } + + $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<h2>$ip</h2>\n<div class=\"special\">"; + } elseif ( $lastIP != $row->cuc_ip ) { + $s .= "</ul></div>\n<h2>$ip</h2>\n<div class=\"special\">"; + $lastIP = $row->cuc_ip; + unset( $this->lastdate ); // start over + } + $s .= $this->CUChangesLine( $row, $reason ); + } + $s .= '</ul></div>'; + $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 = '<div id="checkuserresults">'; + while ( $row = $ret->fetchObject() ) { + $s .= $this->CUChangesLine( $row, $reason ); + } + $s .= '</ul></div>'; + $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( '<p>' . wfMsgHtml( 'checkuser-log-fail' ) . '</p>' ); + } + + $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 = '<h5>' . wfMsg( 'checkuser-too-many' ) . '</h5>'; + $s .= '<ol>'; + $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 .= '<li><a href="' . + $this->getTitle()->escapeLocalURL( 'user=' . urlencode( $ip ) . '&reason=' . urlencode( $reason ) . '&checktype=subipusers' ) . + '">' . $ip . '</a>'; + 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 .= ' [<strong>' . $row->count . "</strong>]</li>\n"; + ++$counter; + } + $s .= '</ol>'; + $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 = "<form name='checkuserblock' id='checkuserblock' action=\"$action\" method='post'>"; + $s .= '<div id="checkuserresults"><ul>'; + foreach ( $users_edits as $name => $count ) { + $s .= '<li>'; + $s .= Xml::check( 'users[]', false, array( 'value' => $name ) ) . ' '; + # Load user object + $user = User::newFromName( $name, false ); + # Add user tool links + $s .= $this->sk->userLink( - 1 , $name ) . $this->sk->userToolLinks( - 1 , $name ); + # Add CheckUser link + $s .= ' (<a href="' . $this->getTitle()->escapeLocalURL( 'user=' . urlencode( $name ) . + '&reason=' . urlencode( $reason ) ) . '">' . wfMsgHtml( 'checkuser-check' ) . '</a>)'; + # Show edit time range + if ( $users_first[$name] == $users_last[$name] ) { + $s .= ' (' . $wgLang->timeanddate( wfTimestamp( TS_MW, $users_first[$name] ), true ) . ') '; + } else { + $s .= ' (' . $wgLang->timeanddate( wfTimestamp( TS_MW, $users_first[$name] ), true ) . + ' -- ' . $wgLang->timeanddate( wfTimestamp( TS_MW, $users_last[$name] ), true ) . ') '; + } + # Total edit count + $s .= ' [<strong>' . $count . '</strong>]<br />'; + # Check if this user or IP is blocked. If so, give a link to the block log... + $ip = IP::isIPAddress( $name ) ? $name : ''; + $flags = $this->userBlockFlags( $ip, $users_ids[$name], $user ); + # Show if account is local only + $authUser = $wgAuth->getUserInstance( $user ); + if ( $user->getId() && $authUser->getId() === 0 ) { + $flags[] = '<strong>(' . wfMsgHtml( 'checkuser-localonly' ) . ')</strong>'; + } + # Check for extra user rights... + if ( $users_ids[$name] ) { + if ( $user->isLocked() ) { + $flags[] = '<b>(' . wfMsgHtml( 'checkuser-locked' ) . ')</b>'; + } + $list = array(); + foreach ( $user->getGroups() as $group ) { + $list[] = self::buildGroupLink( $group ); + } + $groups = $wgLang->commaList( $list ); + if ( $groups ) { + $flags[] = '<i>(' . $groups . ')</i>'; + } + } + # Check how many accounts the user made recently? + if ( $ip ) { + $key = wfMemcKey( 'acctcreate', 'ip', $ip ); + $count = intval( $wgMemc->get( $key ) ); + if ( $count ) { + $flags[] = '<strong>[' . wfMsgExt( 'checkuser-accounts', 'parsemag', $wgLang->formatNum( $count ) ) . ']</strong>'; + } + } + $s .= implode( ' ', $flags ); + $s .= '<ol>'; + # List out each IP/XFF combo for this username + for ( $i = ( count( $users_infosets[$name] ) - 1 ); $i >= 0; $i-- ) { + $set = $users_infosets[$name][$i]; + # IP link + $s .= '<li>'; + $s .= '<a href="' . $this->getTitle()->escapeLocalURL( 'user=' . urlencode( $set[0] ) ) . '">' . htmlspecialchars( $set[0] ) . '</a>'; + # XFF string, link to /xff search + if ( $set[1] ) { + # Flag our trusted proxies + list( $client, $trusted ) = efGetClientIPfromXFF( $set[1], $set[0] ); + $c = $trusted ? '#F0FFF0' : '#FFFFCC'; + $s .= '   <span style="background-color: ' . $c . '"><strong>XFF</strong>: '; + $s .= $this->sk->makeKnownLinkObj( $this->getTitle(), + htmlspecialchars( $set[1] ), + 'user=' . urlencode( $client ) . '/xff' ) . '</span>'; + } + $s .= "</li>\n"; + } + $s .= '</ol><br /><ol>'; + # List out each agent for this username + for ( $i = ( count( $users_agentsets[$name] ) - 1 ); $i >= 0; $i-- ) { + $agent = $users_agentsets[$name][$i]; + $s .= '<li><i>' . htmlspecialchars( $agent ) . "</i></li>\n"; + } + $s .= '</ol>'; + $s .= '</li>'; + } + $s .= "</ul></div>\n"; + if ( $wgUser->isAllowed( 'block' ) && !$wgUser->isBlocked() ) { + $s .= "<fieldset>\n"; + $s .= '<legend>' . wfMsgHtml( 'checkuser-massblock' ) . "</legend>\n"; + $s .= '<p>' . wfMsgExt( 'checkuser-massblock-text', array( 'parseinline' ) ) . "</p>\n"; + $s .= '<table><tr>' . + '<td>' . Xml::check( 'usetag', false, array( 'id' => 'usetag' ) ) . '</td>' . + '<td>' . Xml::label( wfMsgHtml( 'checkuser-blocktag' ), 'usetag' ) . '</td>' . + '<td>' . Xml::input( 'tag', 46, $tag, array( 'id' => 'blocktag' ) ) . '</td>' . + '</tr><tr>' . + '<td>' . Xml::check( 'usettag', false, array( 'id' => 'usettag' ) ) . '</td>' . + '<td>' . Xml::label( wfMsgHtml( 'checkuser-blocktag-talk' ), 'usettag' ) . '</td>' . + '<td>' . Xml::input( 'talktag', 46, $talkTag, array( 'id' => 'talktag' ) ) . '</td>' . + '</tr></table>'; + $s .= '<p>' . wfMsgHtml( 'checkuser-reason' ) . ' '; + $s .= Xml::input( 'blockreason', 46, '', array( 'maxlength' => '150', 'id' => 'blockreason' ) ); + $s .= ' ' . Xml::submitButton( wfMsgHtml( 'checkuser-massblock-commit' ), + array( 'id' => 'checkuserblocksubmit', 'name' => 'checkuserblock' ) ) . "</p>\n"; + $s .= "</fieldset>\n"; + } + $s .= '</form>'; + } + + $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[] = '<strong>(' . $blocklog . ' - ' . $block->mAddress . ')</strong>'; + // Auto blocked? + } elseif ( $block->mAuto ) { + $blocklog = $this->sk->makeKnownLinkObj( $blocklist, + wfMsgHtml( 'checkuser-blocked' ), 'ip=' . urlencode( "#{$block->mId}" ) ); + $flags[] = '<strong>(' . $blocklog . ')</strong>'; + } else { + $userpage = $user->getUserPage(); + $blocklog = $this->sk->makeKnownLinkObj( $logs, wfMsgHtml( 'checkuser-blocked' ), + 'type=block&page=' . urlencode( $userpage->getPrefixedText() ) ); + $flags[] = '<strong>(' . $blocklog . ')</strong>'; + } + // IP that is blocked on all wikis? + } elseif ( $ip == $user->getName() && $user->isBlockedGlobally( $ip ) ) { + $flags[] = '<strong>(' . wfMsgHtml( 'checkuser-gblocked' ) . ')</strong>'; + } elseif ( self::userWasBlocked( $user->getName() ) ) { + $userpage = $user->getUserPage(); + $blocklog = $this->sk->makeKnownLinkObj( $logs, wfMsgHtml( 'checkuser-wasblocked' ), + 'type=block&page=' . urlencode( $userpage->getPrefixedText() ) ); + $flags[] = '<strong>(' . $blocklog . ')</strong>'; + } + 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<h4>$date</h4>\n<ul class=\"special\">"; + } elseif ( $date != $this->lastdate ) { + $line = "</ul>\n<h4>$date</h4>\n<ul class=\"special\">"; + $this->lastdate = $date; + } else { + $line = ''; + } + $line .= '<li>'; + # 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 .= '<br />        <small>'; + # IP + $line .= ' <strong>IP</strong>: ' . $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 .= '   <span class="mw-checkuser-xff" style="background-color: ' . $c . '">' . + '<strong>XFF</strong>: '; + $line .= $this->sk->makeKnownLinkObj( $cuTitle, + htmlspecialchars( $row->cuc_xff ), + 'user=' . urlencode( $client ) . '/xff&reason=' . urlencode( $reason ) ) . '</span>'; + } + # User agent + $line .= '   <span class="mw-checkuser-agent" style="color:#888;">' . + htmlspecialchars( $row->cuc_agent ) . '</span>'; + + $line .= "</small></li>\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 .= '<span class="newpage">' . $this->message['newpageletter'] . '</span>'; + } + if ( $row->cuc_minor ) { + $links .= '<span class="minor">' . $this->message['minoreditletter'] . '</span>'; + } + # 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 = "<select name=\"cuSearchType\" style='margin-top:.2em;'>\n"; + foreach ( $searchTypes as $searchType ) { + if ( $type == $searchType ) { + $checked = 'selected="selected"'; + } else { + $checked = ''; + } + $caption = wfMsgHtml( 'checkuser-search-' . $searchType ); + $select .= "<option value=\"$searchType\" $checked>$caption</option>\n"; + } + $select .= '</select>'; + + $encTarget = htmlspecialchars( $target ); + $msgSearch = wfMsgHtml( 'checkuser-search' ); + $input = "<input type=\"text\" name=\"cuSearch\" value=\"$encTarget\" size=\"40\"/>"; + $msgSearchForm = wfMsgHtml( 'checkuser-search-form', $select, $input ); + $formAction = $this->getLogSubpageTitle()->escapeLocalURL(); + $msgSearchSubmit = '  ' . wfMsgHtml( 'checkuser-search-submit' ) . '  '; + + $s = "<form method='get' action=\"$formAction\">\n" . + "<fieldset><legend>$msgSearch</legend>\n" . + "<p>$msgSearchForm</p>\n" . + "<p>" . $this->getDateMenu( $year, $month ) . "   \n" . + "<input type=\"submit\" name=\"cuSearchSubmit\" value=\"$msgSearchSubmit\"/></p>\n" . + "</fieldset></form>\n"; + $wgOut->addHTML( $s ); + + if ( $error !== false ) { + $wgOut->addWikiText( '<div class="errorbox">' . wfMsg( $error ) . '</div>' ); + 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 '<li>' . + $wgLang->timeanddate( wfTimestamp( TS_MW, $row->cul_timestamp ), true ) . + wfMsg( 'comma-separator' ) . + wfMsg( + 'checkuser-log-' . $row->cul_type, + $user, + $target + ) . + $comment . + '</li>'; + } + + function getStartBody() { + if ( $this->getNumRows() ) { + return '<ul>'; + } else { + return ''; + } + } + + function getEndBody() { + if ( $this->getNumRows() ) { + return '</ul>'; + } else { + return ''; + } + } + + function getEmptyBody() { + return '<p>' . wfMsgHtml( 'checkuser-empty' ) . '</p>'; + } + + 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' + ); + } +} |