diff options
Diffstat (limited to 'CheckUser/specials/SpecialCheckUser.php')
-rw-r--r-- | CheckUser/specials/SpecialCheckUser.php | 1417 |
1 files changed, 791 insertions, 626 deletions
diff --git a/CheckUser/specials/SpecialCheckUser.php b/CheckUser/specials/SpecialCheckUser.php index 048a0d20..135d8254 100644 --- a/CheckUser/specials/SpecialCheckUser.php +++ b/CheckUser/specials/SpecialCheckUser.php @@ -2,22 +2,47 @@ class CheckUser extends SpecialPage { /** - * Constructor -- set up the new special page + * @var null|array $message Used to cache frequently used messages */ + protected $message = null; + public function __construct() { parent::__construct( 'CheckUser', 'checkuser' ); } + public function doesWrites() { + return true; // logging + } + public function execute( $subpage ) { + $this->setHeaders(); + $this->checkPermissions(); + // Logging and blocking requires writing so stop from here if read-only mode + $this->checkReadOnly(); + + // Blocked users are not allowed to run checkuser queries (bug T157883) + $callingUser = $this->getUser(); + if ( $callingUser->isBlocked() ) { + throw new UserBlockedError( $callingUser->getBlock() ); + } + + $out = $this->getOutput(); $request = $this->getRequest(); - $this->checkPermissions(); - $this->setHeaders(); + if ( $this->getUser()->isAllowed( 'checkuser-log' ) ) { + $subtitleLink = $this->getLinkRenderer()->makeKnownLink( + SpecialPage::getTitleFor( 'CheckUserLog' ), + $this->msg( 'checkuser-showlog' )->text() + ); + $out->addSubtitle( $subtitleLink ); + } $user = $request->getText( 'user', $request->getText( 'ip', $subpage ) ); $user = trim( $user ); $reason = $request->getText( 'reason' ); - $blockreason = $request->getText( 'blockreason' ); + $blockreason = $request->getText( 'blockreason', '' ); + $disableUserTalk = $request->getBool( 'blocktalk', false ); + $disableEmail = $request->getBool( 'blockemail', false ); $checktype = $request->getVal( 'checktype' ); $period = $request->getInt( 'period' ); $users = $request->getArray( 'users' ); @@ -26,34 +51,36 @@ class CheckUser extends SpecialPage { $talkTag = $request->getBool( 'usettag' ) ? trim( $request->getVal( 'talktag' ) ) : ''; - $m = array(); - # An IPv4? An IPv6? CIDR included? + $blockParams = [ + 'reason' => $blockreason, + 'talk' => $disableUserTalk, + 'email' => $disableEmail, + ]; + + $ip = $name = $xff = ''; + $m = []; if ( IP::isIPAddress( $user ) ) { + // A single IP address or an IP range $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 = ''; + // A single IP address or range with XFF string included $xff = IP::sanitizeIP( $m[1] ); - # A user? } else { - $ip = ''; + // A user? $name = $user; - $xff = ''; } + $this->showIntroductoryText(); $this->showForm( $user, $reason, $checktype, $ip, $xff, $name, $period ); - # Perform one of the various submit operations... + // Perform one of the various submit operations... if ( $request->wasPosted() ) { if ( !$this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) { - $this->getOutput()->wrapWikiMsg( '<div class="error">$1</div>', 'checkuser-token-fail' ); + $out->wrapWikiMsg( '<div class="error">$1</div>', 'checkuser-token-fail' ); } elseif ( $request->getVal( 'action' ) === 'block' ) { - $this->doMassUserBlock( $users, $blockreason, $tag, $talkTag ); + $this->doMassUserBlock( $users, $blockParams, $tag, $talkTag ); } elseif ( !$this->checkReason( $reason ) ) { - $this->getOutput()->addWikiMsg( 'checkuser-noreason' ); + $out->addWikiMsg( 'checkuser-noreason' ); } elseif ( $checktype == 'subuserips' ) { $this->doUserIPsRequest( $name, $reason, $period ); } elseif ( $xff && $checktype == 'subedits' ) { @@ -68,57 +95,35 @@ class CheckUser extends SpecialPage { $this->doIPUsersRequest( $ip, false, $reason, $period, $tag, $talkTag ); } } - # Add CIDR calculation convenience form + // Add CIDR calculation convenience JS form $this->addJsCIDRForm(); - $this->getOutput()->addModules( 'ext.checkUser' ); // JS + $out->addModules( 'ext.checkUser' ); } - /** - * 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 ( array( 'diff', 'hist', 'minoreditletter', 'newpageletter', 'blocklink', 'log' ) as $msg ) { - $this->message[$msg] = $this->msg( $msg )->escaped(); - } - } - } - - /** - * @return Title - */ - public function getCheckUserLogTitle() { - if ( !isset( $this->checkUserLogTitle ) ) { - $this->checkUserLogTitle = SpecialPage::getTitleFor( 'CheckUserLog' ); - } - return $this->checkUserLogTitle; - } - - protected function showGuide() { + protected function showIntroductoryText() { global $wgCheckUserCIDRLimit; $this->getOutput()->addWikiText( $this->msg( 'checkuser-summary', $wgCheckUserCIDRLimit['IPv4'], - $wgCheckUserCIDRLimit['IPv6'] )->text() . - "\n\n[[" . $this->getCheckUserLogTitle()->getPrefixedText() . - '|' . $this->msg( 'checkuser-showlog' )->text() . ']]' + $wgCheckUserCIDRLimit['IPv6'] + )->text() ); } /** - * @param $user - * @param $reason - * @param $checktype - * @param $ip - * @param $xff - * @param $name - * @param $period + * Show the CheckUser query form + * + * @param string $user + * @param string $reason + * @param string $checktype + * @param string $ip + * @param string $xff + * @param string $name + * @param int $period */ protected function showForm( $user, $reason, $checktype, $ip, $xff, $name, $period ) { $action = htmlspecialchars( $this->getPageTitle()->getLocalUrl() ); - # Fill in requested type if it makes sense + // Fill in requested type if it makes sense $encipusers = $encedits = $encuserips = 0; if ( $checktype == 'subipusers' && ( $ip || $xff ) ) { $encipusers = 1; @@ -126,64 +131,67 @@ class CheckUser extends SpecialPage { $encuserips = 1; } elseif ( $checktype == 'subedits' ) { $encedits = 1; - # Defaults otherwise + // Defaults otherwise } elseif ( $ip || $xff ) { $encedits = 1; } else { $encuserips = 1; } - # Compile our nice form... - # Username field should fit things like "2001:0db8:85a3:08d3:1319:8a2e:0370:7344/100/xff" - $this->showGuide(); // explanation text - $form = Xml::openElement( 'form', array( 'action' => $action, - 'name' => 'checkuserform', 'id' => 'checkuserform', 'method' => 'post' ) ); + $form = Xml::openElement( 'form', [ 'action' => $action, + 'name' => 'checkuserform', 'id' => 'checkuserform', 'method' => 'post' ] ); $form .= '<fieldset><legend>' . $this->msg( 'checkuser-query' )->escaped() . '</legend>'; - $form .= Xml::openElement( 'table', array( 'style' => 'border:0' ) ); + $form .= Xml::openElement( 'table', [ 'style' => 'border:0' ] ); $form .= '<tr>'; $form .= '<td>' . $this->msg( 'checkuser-target' )->escaped() . '</td>'; - $form .= '<td>' . Xml::input( 'user', 46, $user, array( 'id' => 'checktarget' ) ); + // User field should fit things like "2001:0db8:85a3:08d3:1319:8a2e:0370:7344/100/xff" + $form .= '<td>' . Xml::input( 'user', 46, $user, [ 'id' => 'checktarget' ] ); $form .= ' ' . $this->getPeriodMenu( $period ) . '</td>'; $form .= '</tr><tr>'; $form .= '<td></td>'; - $form .= Xml::openElement( 'td', array( 'class' => 'checkuserradios' ) ); - $form .= Xml::openElement( 'table', array( 'style' => 'border:0' ) ); + $form .= Xml::openElement( 'td', [ 'class' => 'checkuserradios' ] ); + $form .= Xml::openElement( 'table', [ 'style' => 'border:0' ] ); $form .= '<tr>'; $form .= '<td>' . - Xml::radio( 'checktype', 'subuserips', $encuserips, array( 'id' => 'subuserips' ) ); + Xml::radio( 'checktype', 'subuserips', $encuserips, [ 'id' => 'subuserips' ] ); $form .= ' ' . Xml::label( $this->msg( 'checkuser-ips' )->text(), 'subuserips' ) . '</td>'; $form .= '<td>' . - Xml::radio( 'checktype', 'subedits', $encedits, array( 'id' => 'subedits' ) ); + Xml::radio( 'checktype', 'subedits', $encedits, [ 'id' => 'subedits' ] ); $form .= ' ' . Xml::label( $this->msg( 'checkuser-edits' )->text(), 'subedits' ) . '</td>'; $form .= '<td>' . - Xml::radio( 'checktype', 'subipusers', $encipusers, array( 'id' => 'subipusers' ) ); - $form .= ' ' . Xml::label( $this->msg( 'checkuser-users' )->text(), 'subipusers' ) . '</td>'; + Xml::radio( 'checktype', 'subipusers', $encipusers, [ 'id' => 'subipusers' ] ); + $form .= ' ' . + Xml::label( $this->msg( 'checkuser-users' )->text(), 'subipusers' ) . '</td>'; $form .= '</tr>'; $form .= Xml::closeElement( 'table' ); $form .= Xml::closeElement( 'td' ); $form .= '</tr><tr>'; $form .= '<td>' . $this->msg( 'checkuser-reason' )->escaped() . '</td>'; $form .= '<td>' . Xml::input( 'reason', 46, $reason, - array( 'maxlength' => '150', 'id' => 'checkreason' ) ); + [ 'maxlength' => '150', 'id' => 'checkreason' ] ); $form .= '   ' . Xml::submitButton( $this->msg( 'checkuser-check' )->text(), - array( 'id' => 'checkusersubmit', 'name' => 'checkusersubmit' ) ) . '</td>'; + [ 'id' => 'checkusersubmit', 'name' => 'checkusersubmit' ] ) . '</td>'; $form .= '</tr>'; $form .= Xml::closeElement( 'table' ); $form .= '</fieldset>'; $form .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); $form .= Xml::closeElement( 'form' ); - # Output form + $this->getOutput()->addHTML( $form ); } /** * Get a selector of time period options - * @param int $selected, selected level + * @param int $selected Currently selected option * @return string */ - protected function getPeriodMenu( $selected = null ) { - $s = '<label for="period">' . $this->msg( 'checkuser-period' )->escaped() . '</label> '; - $s .= Xml::openElement( 'select', array( 'name' => 'period', 'id' => 'period', 'style' => 'margin-top:.2em;' ) ); + protected function getPeriodMenu( $selected ) { + $s = '<label for="period">' . + $this->msg( 'checkuser-period' )->escaped() . '</label> '; + $s .= Xml::openElement( + 'select', + [ 'name' => 'period', 'id' => 'period', 'style' => 'margin-top:.2em;' ] + ); $s .= Xml::option( $this->msg( 'checkuser-week-1' )->text(), 7, $selected === 7 ); $s .= Xml::option( $this->msg( 'checkuser-week-2' )->text(), 14, $selected === 14 ); $s .= Xml::option( $this->msg( 'checkuser-month' )->text(), 31, $selected === 31 ); @@ -200,37 +208,65 @@ class CheckUser extends SpecialPage { '<legend>' . $this->msg( 'checkuser-cidr-label' )->escaped() . '</legend>'; $s .= '<textarea id="mw-checkuser-iplist" dir="ltr" rows="5" cols="50"></textarea><br />'; $s .= $this->msg( 'checkuser-cidr-res' )->escaped() . ' ' . - Xml::input( 'mw-checkuser-cidr-res', 35, '', array( 'id' => 'mw-checkuser-cidr-res' ) ) . + Xml::input( 'mw-checkuser-cidr-res', 35, '', [ 'id' => 'mw-checkuser-cidr-res' ] ) . ' <strong id="mw-checkuser-ipnote"></strong>'; $s .= '</fieldset>'; $this->getOutput()->addHTML( $s ); } /** + * @param string $reason + * @return bool + */ + protected function checkReason( $reason ) { + global $wgCheckUserForceSummary; + return ( !$wgCheckUserForceSummary || strlen( $reason ) ); + } + + /** + * 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() { + if ( $this->message === null ) { + $msgKeys = [ 'diff', 'hist', 'minoreditletter', 'newpageletter', 'blocklink', 'log' ]; + foreach ( $msgKeys as $msg ) { + $this->message[$msg] = $this->msg( $msg )->escaped(); + } + } + } + + /** * Block a list of selected users * @param array $users - * @param string $reason + * @param array $blockParams * @param string $tag * @param string $talkTag */ - protected function doMassUserBlock( $users, $reason = '', $tag = '', $talkTag = '' ) { + protected function doMassUserBlock( $users, $blockParams, $tag = '', $talkTag = '' ) { global $wgCheckUserMaxBlocks; - if ( empty( $users ) || $this->getUser()->isBlocked( false ) ) { + $usersCount = count( $users ); + if ( !$this->getUser()->isAllowed( 'block' ) || $this->getUser()->isBlocked() + || !$usersCount + ) { $this->getOutput()->addWikiMsg( 'checkuser-block-failure' ); return; - } elseif ( count( $users ) > $wgCheckUserMaxBlocks ) { + } elseif ( $usersCount > $wgCheckUserMaxBlocks ) { $this->getOutput()->addWikiMsg( 'checkuser-block-limit' ); return; - } elseif ( !$reason ) { + } elseif ( !$blockParams['reason'] ) { $this->getOutput()->addWikiMsg( 'checkuser-block-noreason' ); return; } - $safeUsers = self::doMassUserBlockInternal( $users, $reason, $tag, $talkTag ); - if ( !empty( $safeUsers ) ) { + + $blockedUsers = $this->doMassUserBlockInternal( $users, $blockParams, $tag, $talkTag ); + $blockedCount = count( $blockedUsers ); + if ( $blockedCount > 0 ) { $lang = $this->getLanguage(); - $n = count( $safeUsers ); - $ulist = $lang->listToText( $safeUsers ); - $this->getOutput()->addWikiMsg( 'checkuser-block-success', $ulist, $lang->formatNum( $n ) ); + $this->getOutput()->addWikiMsg( 'checkuser-block-success', + $lang->listToText( $blockedUsers ), + $lang->formatNum( $blockedCount ) + ); } else { $this->getOutput()->addWikiMsg( 'checkuser-block-failure' ); } @@ -239,150 +275,183 @@ class CheckUser extends SpecialPage { /** * Block a list of selected users * - * @param $users Array - * @param $reason String - * @param $tag String: replaces user pages - * @param $talkTag String: replaces user talk pages - * @return Array: list of html-safe usernames + * @param string[] $users + * @param array $blockParams + * @param string $tag replaces user pages + * @param string $talkTag replaces user talk pages + * @return string[] List of html-safe usernames which were actually were blocked */ - public static function doMassUserBlockInternal( $users, $reason = '', $tag = '', $talkTag = '' ) { - global $wgUser; + protected function doMassUserBlockInternal( $users, array $blockParams, + $tag = '', $talkTag = '' ) { + global $wgBlockAllowsUTEdit; - $counter = $blockSize = 0; - $safeUsers = array(); - $log = new LogPage( 'block' ); + $currentUser = $this->getUser(); + $safeUsers = []; foreach ( $users as $name ) { - # Enforce limits - $counter++; - $blockSize++; - # Lets not go *too* fast - if ( $blockSize >= 20 ) { - $blockSize = 0; - wfWaitForSlaves( 5 ); - } $u = User::newFromName( $name, false ); - // If user doesn't exist, it ought to be an IP then - if ( is_null( $u ) || ( !$u->getId() && !IP::isIPAddress( $u->getName() ) ) ) { + // Do some checks to make sure we can block this user first + if ( $u === null ) { + // Invalid user + continue; + } + $isIP = IP::isIPAddress( $u->getName() ); + if ( !$u->getId() && !$isIP ) { + // Not a registered user or an IP + continue; + } + + if ( $u->isBlocked() ) { + // If the user is already blocked, just leave it as is continue; } + $userTitle = $u->getUserPage(); $userTalkTitle = $u->getTalkPage(); - $userpage = WikiPage::factory( $userTitle ); - $usertalk = WikiPage::factory( $userTalkTitle ); - $safeUsers[] = '[[' . $userTitle->getPrefixedText() . '|' . $userTitle->getText() . ']]'; - $expirestr = $u->getId() ? 'indefinite' : '1 week'; + $safeUsers[] = "[[{$userTitle->getPrefixedText()}|{$userTitle->getText()}]]"; + $expirestr = $isIP ? '1 week' : 'indefinite'; $expiry = SpecialBlock::parseExpiryInput( $expirestr ); - $anonOnly = IP::isIPAddress( $u->getName() ) ? 1 : 0; // Create the block $block = new Block(); $block->setTarget( $u ); - $block->setBlocker( $wgUser ); - $block->mReason = $reason; + $block->setBlocker( $currentUser ); + $block->mReason = $blockParams['reason']; $block->mExpiry = $expiry; - $block->isHardblock( !IP::isIPAddress( $u->getName() ) ); + $block->isHardblock( !$isIP ); $block->isAutoblocking( true ); $block->prevents( 'createaccount', true ); - $block->prevents( 'sendemail', false ); - $block->prevents( 'editownusertalk', false ); - - $oldblock = Block::newFromTarget( $u->getName() ); - if ( !$oldblock ) { - $block->insert(); - # Prepare log parameters - $logParams = array(); - $logParams[] = $expirestr; - $flags = array( 'nocreate' ); - if ( $anonOnly ) { - $flags[] = 'anononly'; - } - $logParams[] = implode( ',', $flags ); - # Add log entry - $log->addEntry( 'block', $userTitle, $reason, $logParams ); - } - # Tag userpage! (check length to avoid mistakes) - if ( strlen( $tag ) > 2 ) { - $flags = 0; - if ( $userpage->exists() ) { - $flags |= EDIT_MINOR; - } - $userpage->doEditContent( new WikitextContent( $tag ), $reason, $flags ); - } - if ( strlen( $talkTag ) > 2 ) { - $flags = 0; - if ( $usertalk->exists() ) { - $flags |= EDIT_MINOR; - } - $usertalk->doEditContent( new WikitextContent( $talkTag ), $reason, $flags ); - } + $block->prevents( 'sendemail', + ( SpecialBlock::canBlockEmail( $currentUser ) && $blockParams['email'] ) + ); + $block->prevents( 'editownusertalk', ( !$wgBlockAllowsUTEdit || $blockParams['talk'] ) ); + $status = $block->insert(); + + // Prepare log parameters for the block + $logParams = []; + $logParams['5::duration'] = $expirestr; + $logParams['6::flags'] = self::userBlockLogFlags( $isIP, $blockParams ); + + $logEntry = new ManualLogEntry( 'block', 'block' ); + $logEntry->setTarget( $userTitle ); + $logEntry->setComment( $blockParams['reason'] ); + $logEntry->setPerformer( $currentUser ); + $logEntry->setParameters( $logParams ); + $blockIds = array_merge( [ $status['id'] ], $status['autoIds'] ); + $logEntry->setRelations( [ 'ipb_id' => $blockIds ] ); + $logEntry->publish( $logEntry->insert() ); + + // Tag user page and user talk page + $this->tagPage( $userTitle, $tag, $blockParams['reason'] ); + $this->tagPage( $userTalkTitle, $talkTag, $blockParams['reason'] ); } + return $safeUsers; } /** + * Return a comma-delimited list of "flags" to be passed to the block log. + * Flags are 'anononly', 'nocreate', 'noemail' and 'nousertalk'. + * @param bool $anonOnly + * @param array $blockParams + * @return string + */ + protected static function userBlockLogFlags( $anonOnly, array $blockParams ) { + global $wgBlockAllowsUTEdit; + $flags = []; + + if ( $anonOnly ) { + $flags[] = 'anononly'; + } + + $flags[] = 'nocreate'; + + if ( $blockParams['email'] ) { + $flags[] = 'noemail'; + } + + if ( $wgBlockAllowsUTEdit && $blockParams['talk'] ) { + $flags[] = 'nousertalk'; + } + + return implode( ',', $flags ); + } + + /** + * Make an edit to the given page with the tag provided + * + * @param Title $title + * @param string $tag + * @param string $summary + */ + protected function tagPage( Title $title, $tag, $summary ) { + // Check length to avoid mistakes + if ( strlen( $tag ) > 2 ) { + $page = WikiPage::factory( $title ); + $flags = 0; + if ( $page->exists() ) { + $flags |= EDIT_MINOR; + } + $page->doEditContent( new WikitextContent( $tag ), $summary, + $flags, false, $this->getUser() ); + } + } + + /** * Give a "no matches found for X" message. * If $checkLast, then mention the last edit by this user or IP. * - * @param $userName + * @param string $userName * @param bool $checkLast - * @return String + * @return string */ protected function noMatchesMessage( $userName, $checkLast = true ) { if ( $checkLast ) { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $user_id = User::idFromName( $userName ); if ( $user_id ) { $revEdit = $dbr->selectField( 'revision', 'rev_timestamp', - array( 'rev_user' => $user_id ), + [ 'rev_user' => $user_id ], __METHOD__, - array( 'ORDER BY' => 'rev_timestamp DESC' ) + [ 'ORDER BY' => 'rev_timestamp DESC' ] ); $logEdit = $dbr->selectField( 'logging', 'log_timestamp', - array( 'log_user' => $user_id ), + [ 'log_user' => $user_id ], __METHOD__, - array( 'ORDER BY' => 'log_timestamp DESC' ) + [ 'ORDER BY' => 'log_timestamp DESC' ] ); } else { $revEdit = $dbr->selectField( 'revision', 'rev_timestamp', - array( 'rev_user_text' => $userName ), + [ 'rev_user_text' => $userName ], __METHOD__, - array( 'ORDER BY' => 'rev_timestamp DESC' ) + [ 'ORDER BY' => 'rev_timestamp DESC' ] ); $logEdit = false; // no log_user_text index } $lastEdit = max( $revEdit, $logEdit ); if ( $lastEdit ) { + $lastEditTime = wfTimestamp( TS_MW, $lastEdit ); $lang = $this->getLanguage(); - $lastEditDate = $lang->date( wfTimestamp( TS_MW, $lastEdit ), true ); - $lastEditTime = $lang->time( wfTimestamp( TS_MW, $lastEdit ), true ); // FIXME: don't pass around parsed messages - return $this->msg( 'checkuser-nomatch-edits', $lastEditDate, $lastEditTime )->parseAsBlock(); + return $this->msg( 'checkuser-nomatch-edits', + $lang->date( $lastEditTime, true ), + $lang->time( $lastEditTime, true ) + )->parseAsBlock(); } } return $this->msg( 'checkuser-nomatch' )->parseAsBlock(); } /** - * @param $reason - * @return bool - */ - protected function checkReason( $reason ) { - global $wgCheckUserForceSummary; - return ( !$wgCheckUserForceSummary || strlen( $reason ) ); - } - - /** - * FIXME: documentation out of date - * @param string $ip <??? - * @param bool $xfor <??? + * Show all the IPs used by a user + * + * @param string $user * @param string $reason - * Get all IPs used by a user - * Shows first and last date and number of edits + * @param int $period */ - protected function doUserIPsRequest( $user , $reason = '', $period = 0 ) { + protected function doUserIPsRequest( $user, $reason = '', $period = 0 ) { $out = $this->getOutput(); $userTitle = Title::newFromText( $user, NS_USER ); @@ -390,54 +459,50 @@ class CheckUser extends SpecialPage { // normalize the username $user = $userTitle->getText(); } - # IPs are passed in as a blank string + // IPs are passed in as a blank string if ( !$user ) { $out->addWikiMsg( 'nouserspecified' ); return; } - # Get ID, works better than text as user may have been renamed + // 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 is not IP or nonexistent if ( !$user_id ) { - $out->addWikiMsgArray( 'nosuchusershort', $user ); + $out->addWikiMsg( 'nosuchusershort', $user ); return; } - # Record check... - if ( !self::addLogEntry( 'userips', 'user', $user, $reason, $user_id ) ) { - // FIXME: addWikiMsg - $out->addHTML( '<p>' . $this->msg( 'checkuser-log-fail' )->escaped() . '</p>' ); - } + // Record check... + self::addLogEntry( 'userips', 'user', $user, $reason, $user_id ); - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $time_conds = $this->getTimeConds( $period ); - # Ordering by the latest timestamp makes a small filesort on the IP list + // Ordering by the latest timestamp makes a small filesort on the IP list $ret = $dbr->select( 'cu_changes', - array( + [ 'cuc_ip', 'cuc_ip_hex', 'COUNT(*) AS count', 'MIN(cuc_timestamp) AS first', 'MAX(cuc_timestamp) AS last', - ), - array( 'cuc_user' => $user_id, $time_conds ), + ], + [ 'cuc_user' => $user_id, $time_conds ], __METHOD__, - array( + [ 'ORDER BY' => 'last DESC', 'GROUP BY' => 'cuc_ip,cuc_ip_hex', 'LIMIT' => 5001, 'USE INDEX' => 'cuc_user_ip_time', - ) + ] ); if ( !$dbr->numRows( $ret ) ) { $s = $this->noMatchesMessage( $user ) . "\n"; } else { - $blockip = SpecialPage::getTitleFor( 'Block' ); - $ips_edits = array(); + $ips_edits = []; $counter = 0; foreach ( $ret as $row ) { if ( $counter >= 5000 ) { @@ -455,46 +520,46 @@ class CheckUser extends SpecialPage { 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="' . - htmlspecialchars( $this->getPageTitle()->getLocalURL( array( + $s .= $this->getSelfLink( $ip, + [ 'user' => $ip, - 'reason' => $reason - ) ) ) . '">' . - htmlspecialchars( $ip ) . '</a>'; - $s .= ' (<a href="' . htmlspecialchars( $blockip->getLocalURL( 'ip=' . urlencode( $ip ) ) ) . '">' . - $this->msg( 'blocklink' )->escaped() . '</a>)'; - if ( $ips_first[$ip] == $ips_last[$ip] ) { - $s .= ' (' . $this->getLanguage()->timeanddate( wfTimestamp( TS_MW, $ips_first[$ip] ), true ) . ') '; - } else { - $lang = $this->getLanguage(); - $s .= ' (' . $lang->timeanddate( wfTimestamp( TS_MW, $ips_first[$ip] ), true ) . - ' -- ' . $lang->timeanddate( wfTimestamp( TS_MW, $ips_last[$ip] ), true ) . ') '; - } + 'reason' => $reason, + ] + ); + $s .= ' ' . $this->msg( 'parentheses' )->rawParams( + $this->getLinkRenderer()->makeKnownLink( + SpecialPage::getTitleFor( 'Block', $ip ), + $this->msg( 'blocklink' )->text() + ) + )->escaped(); + $s .= ' ' . $this->getTimeRangeString( $ips_first[$ip], $ips_last[$ip] ) . ' '; $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"... + // 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 ), + [ 'cuc_ip_hex' => $ips_hex[$ip], $time_conds ], __METHOD__ ); - # If small enough, get a more accurate count + // 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 ), + [ 'cuc_ip_hex' => $ips_hex[$ip], $time_conds ], __METHOD__ ); } if ( $ipedits > $ips_edits[$ip] ) { - $s .= ' <i>(' . $this->msg( 'checkuser-ipeditcount', $ipedits )->escaped() . ')</i>'; + $s .= ' <i>(' . + $this->msg( 'checkuser-ipeditcount' )->numParams( $ipedits )->escaped() . + ')</i>'; } - # If this IP is blocked, give a link to the block log + // If this IP is blocked, give a link to the block log $s .= $this->getIPBlockInfo( $ip ); $s .= '<div style="margin-left:5%">'; - $s .= '<small>' . $this->msg( 'checkuser-toollinks', urlencode( $ip ) )->parse() . '</small>'; + $s .= '<small>' . $this->msg( 'checkuser-toollinks', urlencode( $ip ) )->parse() . + '</small>'; $s .= '</div>'; $s .= "</li>\n"; } @@ -506,88 +571,90 @@ class CheckUser extends SpecialPage { protected function getIPBlockInfo( $ip ) { $block = Block::newFromTarget( null, $ip, false ); if ( $block instanceof Block ) { - if ( $block->getType() == Block::TYPE_RANGE ) { - $userpage = Title::makeTitle( NS_USER, $block->getTarget() ); - $blocklog = Linker::linkKnown( - SpecialPage::getTitleFor( 'Log' ), - $this->msg( 'checkuser-blocked' )->escaped(), - array(), - array( - 'type' => 'block', - 'page' => $userpage->getPrefixedText() - ) - ); - return ' <strong>(' . $blocklog . ' - ' . $block->getTarget() . ')</strong>'; - } elseif ( $block->getType() == Block::TYPE_AUTO ) { - $blocklog = Linker::linkKnown( - SpecialPage::getTitleFor( 'BlockList' ), - $this->msg( 'checkuser-blocked' )->escaped(), - array(), - array( 'ip' => "#{$block->getId()}" ) - ); - return ' <strong>(' . $blocklog . ')</strong>'; - } else { - $userpage = Title::makeTitle( NS_USER, $block->getTarget() ); - $blocklog = Linker::linkKnown( - SpecialPage::getTitleFor( 'Log' ), - $this->msg( 'checkuser-blocked' )->escaped(), - array(), - array( - 'type' => 'block', - 'page' => $userpage->getPrefixedText() - ) - ); - return ' <strong>(' . $blocklog . ')</strong>'; - } + return $this->getBlockFlag( $block ); } return ''; } /** + * Get a link to block information about the passed block for displaying to the user. + * + * @param Block $block + * @return string + */ + protected function getBlockFlag( Block $block ) { + if ( $block->getType() == Block::TYPE_AUTO ) { + $ret = $this->getLinkRenderer()->makeKnownLink( + SpecialPage::getTitleFor( 'BlockList' ), + $this->msg( 'checkuser-blocked' )->text(), + [], + [ 'wpTarget' => "#{$block->getId()}" ] + ); + } else { + $userPage = Title::makeTitle( NS_USER, $block->getTarget() ); + $ret = $this->getLinkRenderer()->makeKnownLink( + SpecialPage::getTitleFor( 'Log' ), + $this->msg( 'checkuser-blocked' )->text(), + [], + [ + 'type' => 'block', + 'page' => $userPage->getPrefixedText() + ] + ); + } + + // Add the blocked range if the block is on a range + if ( $block->getType() == Block::TYPE_RANGE ) { + $ret .= ' - ' . htmlspecialchars( $block->getTarget() ); + } + + return '<strong>' . + $this->msg( 'parentheses' )->rawParams( $ret )->escaped() + . '</strong>'; + } + + /** + * Shows all changes made by an IP address or range + * * @param string $ip - * @param bool $xfor + * @param bool $xfor if query is for XFF * @param string $reason - * FIXME: $period ??? - * Shows all edits in Recent Changes by this IP (or range) and who made them + * @param int $period */ protected function doIPEditsRequest( $ip, $xfor = false, $reason = '', $period = 0 ) { $out = $this->getOutput(); - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); - # Invalid IPs are passed in as a blank string + // Invalid IPs are passed in as a blank string $ip_conds = self::getIpConds( $dbr, $ip, $xfor ); if ( !$ip || $ip_conds === false ) { $out->addWikiMsg( 'badipaddress' ); return; } - $logType = 'ipedits'; - if ( $xfor ) { - $logType .= '-xff'; - } - # Record check... - if ( !self::addLogEntry( $logType, 'ip', $ip, $reason ) ) { - $out->addWikiMsg( 'checkuser-log-fail' ); - } + $logType = $xfor ? 'ipedits-xff' : 'ipedits'; + + // Record check in the logs + self::addLogEntry( $logType, 'ip', $ip, $reason ); $ip_conds = $dbr->makeList( $ip_conds, LIST_AND ); $time_conds = $this->getTimeConds( $period ); - # 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. + // 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 + // Quick index check only OK if no time constraint if ( $period ) { $rangecount = $dbr->selectField( 'cu_changes', 'COUNT(*)', - array( $ip_conds, $time_conds ), + [ $ip_conds, $time_conds ], __METHOD__, - array( 'USE INDEX' => $index ) ); + [ 'USE INDEX' => $index ] ); } else { $rangecount = $dbr->estimateRowCount( 'cu_changes', '*', - array( $ip_conds ), + [ $ip_conds ], __METHOD__, - array( 'USE INDEX' => $index ) ); + [ 'USE INDEX' => $index ] ); } // Sorting might take some time...make sure it is there wfSuppressWarnings(); @@ -595,20 +662,26 @@ class CheckUser extends SpecialPage { wfRestoreWarnings(); } $counter = 0; - # See what is best to do after testing the waters... + // See what is best to do after testing the waters... if ( isset( $rangecount ) && $rangecount > 5000 ) { - $ret = $dbr->select( 'cu_changes', - array( 'cuc_ip_hex', 'COUNT(*) AS count', 'MIN(cuc_timestamp) AS first', 'MAX(cuc_timestamp) AS last' ), - array( $ip_conds, $time_conds ), + $ret = $dbr->select( + 'cu_changes', + [ + 'cuc_ip_hex', + 'COUNT(*) AS count', + 'MIN(cuc_timestamp) AS first', + 'MAX(cuc_timestamp) AS last' + ], + [ $ip_conds, $time_conds ], __METHOD__, - array( + [ 'GROUP BY' => 'cuc_ip_hex', 'ORDER BY' => 'cuc_ip_hex', 'LIMIT' => 5001, 'USE INDEX' => $index, - ) + ] ); - # List out each IP that has edits + // List out each IP that has edits $s = $this->msg( 'checkuser-too-many' )->parseAsBlock(); $s .= '<ol>'; foreach ( $ret as $row ) { @@ -616,27 +689,22 @@ class CheckUser extends SpecialPage { $out->addWikiMsg( 'checkuser-limited' ); break; } - # Convert the IP hexes into normal form + // 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 ) ); + $ip = long2ip( Wikimedia\base_convert( $row->cuc_ip_hex, 16, 10, 8 ) ); } - $s .= '<li><a href="' . - htmlspecialchars( $this->getPageTitle()->getLocalURL( array( + $s .= '<li>'; + $s .= $this->getSelfLink( $ip, + [ 'user' => $ip, 'reason' => $reason, 'checktype' => 'subipusers' - ) ) ) . - '">' . $ip . '</a>'; - if ( $row->first == $row->last ) { - $s .= ' (' . $this->getLanguage()->timeanddate( wfTimestamp( TS_MW, $row->first ), true ) . ') '; - } else { - $lang = $this->getLanguage(); - $s .= ' (' . $lang->timeanddate( wfTimestamp( TS_MW, $row->first ), true ) . - ' -- ' . $lang->timeanddate( wfTimestamp( TS_MW, $row->last ), true ) . ') '; - } + ] + ); + $s .= ' ' . $this->getTimeRangeString( $row->first, $row->last ) . ' '; $s .= ' [<strong>' . $row->count . "</strong>]</li>\n"; ++$counter; } @@ -650,40 +718,42 @@ class CheckUser extends SpecialPage { return; } - # OK, do the real query... + // OK, do the real query... $ret = $dbr->select( 'cu_changes', - array( - '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' - ), - array( $ip_conds, $time_conds ), + [ + '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' + ], + [ $ip_conds, $time_conds ], __METHOD__, - array( + [ 'ORDER BY' => 'cuc_timestamp DESC', 'LIMIT' => 5001, 'USE INDEX' => $index, - ) + ] ); if ( !$dbr->numRows( $ret ) ) { $s = $this->noMatchesMessage( $ip, !$xfor ) . "\n"; } else { - # Cache common messages + // Cache common messages $this->preCacheMessages(); - # Try to optimize this query + // Try to optimize this query $lb = new LinkBatch; foreach ( $ret as $row ) { $userText = str_replace( ' ', '_', $row->cuc_user_text ); - $lb->add( $row->cuc_namespace, $row->cuc_title ); + if ( $row->cuc_title !== '' ) { + $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 + // List out the edits $s = '<div id="checkuserresults">'; foreach ( $ret as $row ) { if ( $counter >= 5000 ) { @@ -700,9 +770,26 @@ class CheckUser extends SpecialPage { } /** + * @param ResultWrapper $rows Results with cuc_namespace and cuc_title field + */ + protected function doLinkCache( $rows ) { + $lb = new LinkBatch(); + $lb->setCaller( __METHOD__ ); + foreach ( $rows as $row ) { + if ( $row->cuc_title !== '' ) { + $lb->add( $row->cuc_namespace, $row->cuc_title ); + } + } + $lb->execute(); + $rows->seek( 0 ); + } + + /** + * Shows all changes made by a particular user + * * @param string $user * @param string $reason - * Shows all edits in Recent Changes by this user + * @param int $period */ protected function doUserEditsRequest( $user, $reason = '', $period = 0 ) { $out = $this->getOutput(); @@ -712,70 +799,64 @@ class CheckUser extends SpecialPage { // normalize the username $user = $userTitle->getText(); } - # IPs are passed in as a blank string + // IPs are passed in as a blank string if ( !$user ) { $out->addWikiMsg( 'nouserspecified' ); return; } - # Get ID, works better than text as user may have been renamed + // 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 is not IP or nonexistent if ( !$user_id ) { $s = $this->msg( 'nosuchusershort', $user )->parseAsBlock(); $out->addHTML( $s ); return; } - # Record check... - if ( !self::addLogEntry( 'useredits', 'user', $user, $reason, $user_id ) ) { - $out->addHTML( '<p>' . $this->msg( 'checkuser-log-fail' )->escaped() . '</p>' ); - } + // Record check... + self::addLogEntry( 'useredits', 'user', $user, $reason, $user_id ); - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $user_cond = "cuc_user = '$user_id'"; $time_conds = $this->getTimeConds( $period ); - # 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. + // 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 ), + [ $user_cond, $time_conds ], __METHOD__, - array( 'USE INDEX' => 'cuc_user_ip_time' ) ); + [ 'USE INDEX' => 'cuc_user_ip_time' ] ); } else { $count = $dbr->estimateRowCount( 'cu_changes', '*', - array( $user_cond, $time_conds ), + [ $user_cond, $time_conds ], __METHOD__, - array( 'USE INDEX' => 'cuc_user_ip_time' ) ); + [ 'USE INDEX' => 'cuc_user_ip_time' ] ); } - # Cache common messages + // Cache common messages $this->preCacheMessages(); - # See what is best to do after testing the waters... + // See what is best to do after testing the waters... if ( $count > 5000 ) { $out->addHTML( $this->msg( 'checkuser-limited' )->parse() ); $ret = $dbr->select( 'cu_changes', '*', - array( $user_cond, $time_conds ), + [ $user_cond, $time_conds ], __METHOD__, - array( + [ 'ORDER BY' => 'cuc_ip ASC, cuc_timestamp DESC', 'LIMIT' => 5000, 'USE INDEX' => 'cuc_user_ip_time' - ) + ] ); - # Try to optimize this query - $lb = new LinkBatch; - foreach ( $ret as $row ) { - $lb->add( $row->cuc_namespace, $row->cuc_title ); - } - $lb->execute(); - $ret->seek( 0 ); + // Try to optimize this query + $this->doLinkCache( $ret ); $s = ''; foreach ( $ret as $row ) { - if ( !$ip = htmlspecialchars( $row->cuc_ip ) ) { + $ip = htmlspecialchars( $row->cuc_ip ); + if ( !$ip ) { continue; } if ( !isset( $lastIP ) ) { @@ -798,30 +879,24 @@ class CheckUser extends SpecialPage { set_time_limit( 60 ); wfRestoreWarnings(); - # OK, do the real query... + // OK, do the real query... $ret = $dbr->select( 'cu_changes', '*', - array( $user_cond, $time_conds ), + [ $user_cond, $time_conds ], __METHOD__, - array( + [ 'ORDER BY' => 'cuc_timestamp DESC', 'LIMIT' => 5000, 'USE INDEX' => 'cuc_user_ip_time' - ) + ] ); if ( !$dbr->numRows( $ret ) ) { $s = $this->noMatchesMessage( $user ) . "\n"; } else { - # Try to optimize this query - $lb = new LinkBatch; - foreach ( $ret as $row ) { - $lb->add( $row->cuc_namespace, $row->cuc_title ); - } - $lb->execute(); - $ret->seek( 0 ); - # List out the edits + $this->doLinkCache( $ret ); + // List out the edits $s = '<div id="checkuserresults">'; foreach ( $ret as $row ) { $s .= $this->CUChangesLine( $row, $reason ); @@ -833,53 +908,53 @@ class CheckUser extends SpecialPage { } /** + * 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 + * * @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 = '' ) { + protected function doIPUsersRequest( + $ip, $xfor = false, $reason = '', $period = 0, $tag = '', $talkTag = '' + ) { + global $wgMemc; $out = $this->getOutput(); + $dbr = wfGetDB( DB_REPLICA ); - $dbr = wfGetDB( DB_SLAVE ); - # Invalid IPs are passed in as a blank string + // Invalid IPs are passed in as a blank string $ip_conds = self::getIpConds( $dbr, $ip, $xfor ); if ( !$ip || $ip_conds === false ) { $out->addWikiMsg( 'badipaddress' ); return; } - $logType = 'ipusers'; - if ( $xfor ) { - $logType .= '-xff'; - } - # Log the check... - if ( !self::addLogEntry( $logType, 'ip', $ip, $reason ) ) { - $out->addHTML( '<p>' . $this->msg( 'checkuser-log-fail' )->escaped() . '</p>' ); - } + $logType = $xfor ? 'ipusers-xff' : 'ipusers'; + + // Log the check... + self::addLogEntry( $logType, 'ip', $ip, $reason ); $ip_conds = $dbr->makeList( $ip_conds, LIST_AND ); $time_conds = $this->getTimeConds( $period ); $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. + // 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 + // Quick index check only OK if no time constraint if ( $period ) { $rangecount = $dbr->selectField( 'cu_changes', 'COUNT(*)', - array( $ip_conds, $time_conds ), + [ $ip_conds, $time_conds ], __METHOD__, - array( 'USE INDEX' => $index ) ); + [ 'USE INDEX' => $index ] ); } else { $rangecount = $dbr->estimateRowCount( 'cu_changes', '*', - array( $ip_conds ), + [ $ip_conds ], __METHOD__, - array( 'USE INDEX' => $index ) ); + [ 'USE INDEX' => $index ] ); } // Sorting might take some time...make sure it is there wfSuppressWarnings(); @@ -890,20 +965,20 @@ class CheckUser extends SpecialPage { if ( isset( $rangecount ) && $rangecount > 10000 ) { $ret = $dbr->select( 'cu_changes', - array( + [ 'cuc_ip_hex', 'COUNT(*) AS count', 'MIN(cuc_timestamp) AS first', 'MAX(cuc_timestamp) AS last' - ), - array( $ip_conds, $time_conds ), + ], + [ $ip_conds, $time_conds ], __METHOD__, - array( + [ 'GROUP BY' => 'cuc_ip_hex', 'ORDER BY' => 'cuc_ip_hex', 'LIMIT' => 5001, 'USE INDEX' => $index, - ) + ] ); - # List out each IP that has edits + // List out each IP that has edits $s = '<h5>' . $this->msg( 'checkuser-too-many' )->escaped() . '</h5>'; $s .= '<ol>'; $counter = 0; @@ -912,27 +987,22 @@ class CheckUser extends SpecialPage { $out->addHTML( $this->msg( 'checkuser-limited' )->parseAsBlock() ); break; } - # Convert the IP hexes into normal form + // 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 ); + $ip = IP::hexToOctet( $ip ); } else { - $ip = long2ip( wfBaseConvert( $row->cuc_ip_hex, 16, 10, 8 ) ); + $ip = long2ip( Wikimedia\base_convert( $row->cuc_ip_hex, 16, 10, 8 ) ); } - $s .= '<li><a href="' . - htmlspecialchars( $this->getPageTitle()->getLocalURL( array( + $s .= '<li>'; + $s .= $this->getSelfLink( $ip, + [ 'user' => $ip, 'reason' => $reason, 'checktype' => 'subipusers' - ) ) ) . - '">' . $ip . '</a>'; - if ( $row->first == $row->last ) { - $s .= ' (' . $this->getLanguage()->timeanddate( wfTimestamp( TS_MW, $row->first ), true ) . ') '; - } else { - $lang = $this->getLanguage(); - $s .= ' (' . $lang->timeanddate( wfTimestamp( TS_MW, $row->first ), true ) . - ' -- ' . $lang->timeanddate( wfTimestamp( TS_MW, $row->last ), true ) . ') '; - } + ] + ); + $s .= ' ' . $this->getTimeRangeString( $row->first, $row->last ) . ' '; // @todo FIXME: Hard coded brackets. $s .= ' [<strong>' . $row->count . "</strong>]</li>\n"; ++$counter; @@ -947,24 +1017,27 @@ class CheckUser extends SpecialPage { return; } - global $wgMemc; - # OK, do the real query... - + // OK, do the real query... $ret = $dbr->select( 'cu_changes', - array( + [ 'cuc_user_text', 'cuc_timestamp', 'cuc_user', 'cuc_ip', 'cuc_agent', 'cuc_xff' - ), - array( $ip_conds, $time_conds ), + ], + [ $ip_conds, $time_conds ], __METHOD__, - array( + [ 'ORDER BY' => 'cuc_timestamp DESC', 'LIMIT' => 10000, 'USE INDEX' => $index, - ) + ] ); - $users_first = $users_last = $users_edits = $users_ids = array(); + $users_first = []; + $users_last = []; + $users_edits = []; + $users_ids = []; + $users_agentsets = []; + $users_infosets = []; if ( !$dbr->numRows( $ret ) ) { $s = $this->noMatchesMessage( $ip, !$xfor ) . "\n"; } else { @@ -973,19 +1046,19 @@ class CheckUser extends SpecialPage { $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_infosets[$row->cuc_user_text] = []; + $users_agentsets[$row->cuc_user_text] = []; } $users_edits[$row->cuc_user_text] += 1; $users_first[$row->cuc_user_text] = $row->cuc_timestamp; - # Treat blank or NULL xffs as empty strings + // 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 + $xff_ip_combo = [ $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. + // 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; @@ -993,75 +1066,72 @@ class CheckUser extends SpecialPage { } } + // @todo FIXME: This form (and checkboxes) shouldn't be initiated for users without 'block' right $action = htmlspecialchars( $this->getPageTitle()->getLocalURL( '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 + $s .= Xml::check( 'users[]', false, [ 'value' => $name ] ) . ' '; + // Load user object $user = User::newFromName( $name, false ); - # Add user tool links - $s .= Linker::userLink( - 1 , $name ) . Linker::userToolLinks( - 1 , $name ); - # Add CheckUser link - $s .= ' (<a href="' . htmlspecialchars( $this->getPageTitle()->getLocalURL( array( - 'user' => $name, - 'reason' => $reason - ) ) ) . '">' . $this->msg( 'checkuser-check' )->escaped() . '</a>)'; - # Show edit time range - if ( $users_first[$name] == $users_last[$name] ) { - // @todo FIXME: Hard coded parentheses. - $s .= ' (' . $this->getLanguage()->timeanddate( wfTimestamp( TS_MW, $users_first[$name] ), true ) . ') '; - } else { - // @todo FIXME: Hard coded parentheses. - $lang = $this->getLanguage(); - $s .= ' (' . $lang->timeanddate( wfTimestamp( TS_MW, $users_first[$name] ), true ) . - ' -- ' . $lang->timeanddate( wfTimestamp( TS_MW, $users_last[$name] ), true ) . ') '; - } - # Total edit count + // Add user page and tool links + $s .= Linker::userLink( -1, $name ) . ' '; + $ip = IP::isIPAddress( $name ) ? $name : ''; + $linksMsgKey = $ip ? 'checkuser-userlinks-ip' : 'checkuser-userlinks'; + $s .= $this->msg( $linksMsgKey, $name )->parse(); + // Add CheckUser link + $s .= ' ' . $this->msg( 'parentheses' )->rawParams( + $this->getSelfLink( + $this->msg( 'checkuser-check' )->text(), + [ + 'user' => $name, + 'reason' => $reason + ] + ) + )->escaped(); + // Show edit time range + $s .= ' ' . $this->getTimeRangeString( $users_first[$name], $users_last[$name] ) . ' '; + // Total edit count // @todo FIXME: i18n issue: Hard coded brackets. $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 : ''; + // Check if this user or IP is blocked. If so, give a link to the block log... $flags = $this->userBlockFlags( $ip, $users_ids[$name], $user ); - # Check how many accounts the user made recently? + // Check how many accounts the user made recently if ( $ip ) { $key = wfMemcKey( 'acctcreate', 'ip', $ip ); $count = intval( $wgMemc->get( $key ) ); if ( $count ) { // @todo FIXME: i18n issue: Hard coded brackets. - $flags[] = '<strong>[' . $this->msg( 'checkuser-accounts' )->numParams( $count )->escaped() . ']</strong>'; + $flags[] = '<strong>[' . + $this->msg( 'checkuser-accounts' )->numParams( $count )->escaped() . + ']</strong>'; } } $s .= implode( ' ', $flags ); $s .= '<ol>'; - # List out each IP/XFF combo for this username + // 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 + // IP link $s .= '<li>'; - $s .= '<a href="' . - htmlspecialchars( $this->getPageTitle()->getLocalURL( 'user=' . urlencode( $set[0] ) ) ) . - '">' . htmlspecialchars( $set[0] ) . '</a>'; - # XFF string, link to /xff search + $s .= $this->getSelfLink( $set[0], [ 'user' => $set[0] ] ); + // XFF string, link to /xff search if ( $set[1] ) { - # Flag our trusted proxies + // Flag our trusted proxies list( $client ) = CheckUserHooks::getClientIPfromXFF( $set[1] ); // XFF was trusted if client came from it $trusted = ( $client === $row->cuc_ip ); $c = $trusted ? '#F0FFF0' : '#FFFFCC'; - $s .= '   <span style="background-color: ' . $c . '"><strong>XFF</strong>: '; - $s .= Linker::linkKnown( - $this->getPageTitle(), - htmlspecialchars( $set[1] ), - array(), - array( 'user' => $client . '/xff' ) - ) . '</span>'; + $s .= '   <span style="background-color: ' . $c . + '"><strong>XFF</strong>: '; + $s .= $this->getSelfLink( $set[1], [ 'user' => $client . '/xff' ] ) . + '</span>'; } $s .= "</li>\n"; } $s .= '</ol><br /><ol>'; - # List out each agent for this username + // 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"; @@ -1071,113 +1141,152 @@ class CheckUser extends SpecialPage { } $s .= "</ul></div>\n"; if ( $this->getUser()->isAllowed( 'block' ) && !$this->getUser()->isBlocked() ) { - $s .= "<fieldset>\n"; - $s .= '<legend>' . $this->msg( 'checkuser-massblock' )->escaped() . "</legend>\n"; - $s .= '<p>' . $this->msg( 'checkuser-massblock-text' )->parse() . "</p>\n"; - $s .= '<table><tr>' . - '<td>' . Xml::check( 'usetag', false, array( 'id' => 'usetag' ) ) . '</td>' . - '<td>' . Xml::label( $this->msg( 'checkuser-blocktag' )->escaped(), '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( $this->msg( 'checkuser-blocktag-talk' )->escaped(), 'usettag' ) . '</td>' . - '<td>' . Xml::input( 'talktag', 46, $talkTag, array( 'id' => 'talktag' ) ) . '</td>' . - '</tr></table>'; - $s .= '<p>' . $this->msg( 'checkuser-reason' )->escaped() . ' '; - $s .= Xml::input( 'blockreason', 46, '', array( 'maxlength' => '150', 'id' => 'blockreason' ) ); - $s .= ' ' . Xml::submitButton( $this->msg( 'checkuser-massblock-commit' )->escaped(), - array( 'id' => 'checkuserblocksubmit', 'name' => 'checkuserblock' ) ) . "</p>\n"; - $s .= "</fieldset>\n"; + // FIXME: The block <form> is currently added for users without 'block' right + // - only the user-visible form is shown appropriately + $s .= $this->getBlockForm( $tag, $talkTag ); + $s .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); } - $s .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); - $s .= '</form>'; + $s .= "</form>\n"; } $out->addHTML( $s ); } /** - * @param $ip - * @param $userId - * @param $user User + * @param string $tag + * @param string $talkTag + * @return string + */ + protected function getBlockForm( $tag, $talkTag ) { + global $wgBlockAllowsUTEdit, $wgCheckUserCAMultiLock; + if ( $wgCheckUserCAMultiLock !== false ) { + if ( !class_exists( 'CentralAuthUser' ) ) { + // $wgCheckUserCAMultiLock shouldn't be enabled if CA is not loaded + throw new Exception( '$wgCheckUserCAMultiLock requires CentralAuth extension.' ); + } + + $caUserGroups = CentralAuthUser::getInstance( $this->getUser() )->getGlobalGroups(); + // Only load the script for users in the configured global group(s) + if ( count( array_intersect( $wgCheckUserCAMultiLock['groups'], $caUserGroups ) ) ) { + $out = $this->getOutput(); + $out->addModules( 'ext.checkUser.caMultiLock' ); + $centralMLUrl = WikiMap::getForeignURL( + $wgCheckUserCAMultiLock['centralDB'], + // Use canonical name instead of local name so that it works + // even if the local language is different from central wiki + 'Special:MultiLock' + ); + if ( $centralMLUrl === false ) { + throw new Exception( + "Could not retrieve URL for {$wgCheckUserCAMultiLock['centralDB']}" + ); + } + $out->addJsConfigVars( 'wgCUCAMultiLockCentral', $centralMLUrl ); + } + } + + $s = "<fieldset>\n"; + $s .= '<legend>' . $this->msg( 'checkuser-massblock' )->escaped() . "</legend>\n"; + $s .= $this->msg( 'checkuser-massblock-text' )->parseAsBlock() . "\n"; + $s .= '<table><tr>' . + '<td>' . Xml::check( 'usetag', false, [ 'id' => 'usetag' ] ) . '</td>' . + '<td>' . Xml::label( $this->msg( 'checkuser-blocktag' )->escaped(), 'usetag' ) . + '</td>' . + '<td>' . Xml::input( 'tag', 46, $tag, [ 'id' => 'blocktag' ] ) . '</td>' . + '</tr><tr>' . + '<td>' . Xml::check( 'usettag', false, [ 'id' => 'usettag' ] ) . '</td>' . + '<td>' . Xml::label( $this->msg( 'checkuser-blocktag-talk' )->escaped(), 'usettag' ) . + '</td>' . + '<td>' . Xml::input( 'talktag', 46, $talkTag, [ 'id' => 'talktag' ] ) . '</td>'; + if ( $wgBlockAllowsUTEdit ) { + $s .= '</tr><tr>' . + '<td>' . Xml::check( 'blocktalk', false, [ 'id' => 'blocktalk' ] ) . '</td>' . + '<td>' . Xml::label( $this->msg( 'checkuser-blocktalk' )->escaped(), 'blocktalk' ) . + '</td>'; + } + if ( SpecialBlock::canBlockEmail( $this->getUser() ) ) { + $s .= '</tr><tr>' . + '<td>' . Xml::check( 'blockemail', false, [ 'id' => 'blockemail' ] ) . '</td>' . + '<td>' . Xml::label( $this->msg( 'checkuser-blockemail' )->escaped(), 'blockemail' ) + . '</td>'; + } + $s .= '</tr></table>'; + $s .= '<p>' . $this->msg( 'checkuser-reason' )->escaped() . ' '; + $s .= Xml::input( 'blockreason', 46, '', [ 'maxlength' => '150', 'id' => 'blockreason' ] ); + $s .= ' ' . Xml::submitButton( $this->msg( 'checkuser-massblock-commit' )->escaped(), + [ 'id' => 'checkuserblocksubmit', 'name' => 'checkuserblock' ] ) . "</p>\n"; + $s .= "</fieldset>\n"; + + return $s; + } + + /** + * Get an HTML link (<a> element) to Special:CheckUser + * + * @param string $text content to use within <a> tag + * @param array $params query parameters to use in the URL + * @return string + */ + private function getSelfLink( $text, array $params ) { + static $title; + if ( $title === null ) { + $title = $this->getPageTitle(); + } + return $this->getLinkRenderer()->makeKnownLink( + $title, + $text, + [], + $params + ); + } + + /** + * @param string $ip + * @param int $userId + * @param User $user * @return array */ protected function userBlockFlags( $ip, $userId, $user ) { - global $wgAuth; - static $logs, $blocklist; - $logs = SpecialPage::getTitleFor( 'Log' ); - $blocklist = SpecialPage::getTitleFor( 'BlockList' ); + $flags = []; + $block = Block::newFromTarget( $user, $ip, false ); - $flags = array(); if ( $block instanceof Block ) { - // Range blocked? - if ( $block->getType() == Block::TYPE_RANGE ) { - $userpage = Title::makeTitle( NS_USER, $block->getTarget() ); - $blocklog = Linker::linkKnown( - $logs, - $this->msg( 'checkuser-blocked' )->escaped(), - array(), - array( - 'type' => 'block', - 'page' => $userpage->getPrefixedText() - ) - ); - $flags[] = '<strong>(' . $blocklog . ' - ' . $block->getTarget() . ')</strong>'; - // Auto blocked? - } elseif ( $block->getType() == Block::TYPE_AUTO ) { - $blocklog = Linker::linkKnown( - $blocklist, - $this->msg( 'checkuser-blocked' )->escaped(), - array(), - array( 'ip' => "#{$block->getId()}" ) - ); - // @todo FIXME: Hard coded parentheses. - $flags[] = '<strong>(' . $blocklog . ')</strong>'; - } else { - $userpage = $user->getUserPage(); - $blocklog =Linker::linkKnown( - $logs, - $this->msg( 'checkuser-blocked' )->escaped(), - array(), - array( - 'type' => 'block', - 'page' => $userpage->getPrefixedText() - ) - ); - // @todo FIXME: Hard coded parentheses. - $flags[] = '<strong>(' . $blocklog . ')</strong>'; - } - // IP that is blocked on all wikis? + // Locally blocked + $flags[] = $this->getBlockFlag( $block ); } elseif ( $ip == $user->getName() && $user->isBlockedGlobally( $ip ) ) { + // Globally blocked IP $flags[] = '<strong>(' . $this->msg( 'checkuser-gblocked' )->escaped() . ')</strong>'; } elseif ( self::userWasBlocked( $user->getName() ) ) { + // Previously blocked $userpage = $user->getUserPage(); - $blocklog = Linker::linkKnown( - $logs, - $this->msg( 'checkuser-wasblocked' )->escaped(), - array(), - array( - 'type'=> 'block', + $blocklog = $this->getLinkRenderer()->makeKnownLink( + SpecialPage::getTitleFor( 'Log' ), + $this->msg( 'checkuser-wasblocked' )->text(), + [], + [ + 'type' => 'block', 'page' => $userpage->getPrefixedText() - ) + ] ); // @todo FIXME: Hard coded parentheses. $flags[] = '<strong>(' . $blocklog . ')</strong>'; } - # Show if account is local only - $authUser = $wgAuth->getUserInstance( $user ); - if ( $user->getId() && $authUser->getId() === 0 ) { + // Show if account is local only + if ( $user->getId() && + CentralIdLookup::factory() + ->centralIdFromLocalUser( $user, CentralIdLookup::AUDIENCE_RAW ) === 0 + ) { // @todo FIXME: i18n issue: Hard coded parentheses. $flags[] = '<strong>(' . $this->msg( 'checkuser-localonly' )->escaped() . ')</strong>'; } - # Check for extra user rights... + // Check for extra user rights... if ( $userId ) { if ( $user->isLocked() ) { // @todo FIXME: i18n issue: Hard coded parentheses. $flags[] = '<b>(' . $this->msg( 'checkuser-locked' )->escaped() . ')</b>'; } - $list = array(); + $list = []; foreach ( $user->getGroups() as $group ) { $list[] = self::buildGroupLink( $group, $user->getName() ); } @@ -1192,14 +1301,15 @@ class CheckUser extends SpecialPage { } /** - * @param Row $row + * Get a streamlined recent changes line with IP data + * + * @param stdClass $row * @param string $reason - * @return a streamlined recent changes line with IP data + * @return string */ protected function CUChangesLine( $row, $reason ) { - static $cuTitle, $flagCache; - $cuTitle = SpecialPage::getTitleFor( 'CheckUser' ); - # Add date headers as needed + static $flagCache; + // Add date headers as needed $date = $this->getLanguage()->date( wfTimestamp( TS_MW, $row->cuc_timestamp ), true, true ); if ( !isset( $this->lastdate ) ) { $this->lastdate = $date; @@ -1211,14 +1321,16 @@ class CheckUser extends SpecialPage { $line = ''; } $line .= '<li>'; - # Create diff/hist/page links + // Create diff/hist/page links $line .= $this->getLinksFromRow( $row ); - # Show date - $line .= ' . . ' . $this->getLanguage()->time( wfTimestamp( TS_MW, $row->cuc_timestamp ), true, true ) . ' . . '; - # Userlinks + // Show date + $line .= ' . . ' . + $this->getLanguage()->time( wfTimestamp( TS_MW, $row->cuc_timestamp ), true, true ) + . ' . . '; + // Userlinks $line .= Linker::userLink( $row->cuc_user, $row->cuc_user_text ); $line .= Linker::userToolLinks( $row->cuc_user, $row->cuc_user_text ); - # Get block info + // Get block info if ( isset( $flagCache[$row->cuc_user_text] ) ) { $flags = $flagCache[$row->cuc_user_text]; } else { @@ -1227,46 +1339,43 @@ class CheckUser extends SpecialPage { $flags = $this->userBlockFlags( $ip, $row->cuc_user, $user ); $flagCache[$row->cuc_user_text] = $flags; } - # Add any block information + // Add any block information if ( count( $flags ) ) { $line .= ' ' . implode( ' ', $flags ); } - # Action text, hackish ... + // Action text, hackish ... if ( $row->cuc_actiontext ) { $line .= ' ' . Linker::formatComment( $row->cuc_actiontext ) . ' '; } - # Comment + // Comment $line .= Linker::commentBlock( $row->cuc_comment ); $line .= '<br />        <small>'; - # IP - $line .= ' <strong>IP</strong>: ' . Linker::linkKnown( - $cuTitle, - htmlspecialchars( $row->cuc_ip ), - array(), - array( + // IP + $line .= ' <strong>IP</strong>: '; + $line .= $this->getSelfLink( $row->cuc_ip, + [ 'user' => $row->cuc_ip, 'reason' => $reason - ) + ] ); - # XFF + // XFF if ( $row->cuc_xff != null ) { - # Flag our trusted proxies + // Flag our trusted proxies list( $client ) = CheckUserHooks::getClientIPfromXFF( $row->cuc_xff ); $trusted = ( $client === $row->cuc_ip ); // XFF was trusted if client came from it $c = $trusted ? '#F0FFF0' : '#FFFFCC'; - $line .= '   <span class="mw-checkuser-xff" style="background-color: ' . $c . '">' . + $line .= '   '; + $line .= '<span class="mw-checkuser-xff" style="background-color: ' . $c . '">' . '<strong>XFF</strong>: '; - $line .= Linker::linkKnown( - $cuTitle, - htmlspecialchars( $row->cuc_xff ), - array(), - array( + $line .= $this->getSelfLink( $row->cuc_xff, + [ 'user' => $client . '/xff', 'reason' => $reason - ) - ) . '</span>'; + ] + ); + $line .= '</span>'; } - # User agent + // User agent $line .= '   <span class="mw-checkuser-agent" style="color:#888;">' . htmlspecialchars( $row->cuc_agent ) . '</span>'; @@ -1276,80 +1385,118 @@ class CheckUser extends SpecialPage { } /** - * @param $row - * @create diff/hist/page link + * Get formatted timestamp(s) to show the time of first and last change. + * If both timestamps are the same, it will be shown only once. + * + * @param string $first Timestamp of the first change + * @param string $last Timestamp of the last change + * @return string + */ + protected function getTimeRangeString( $first, $last ) { + $s = $this->getFormattedTimestamp( $first ); + if ( $first !== $last ) { + // @todo i18n issue - hardcoded string + $s .= ' -- '; + $s .= $this->getFormattedTimestamp( $last ); + } + return $this->msg( 'parentheses' )->rawParams( $s )->escaped(); + } + + /** + * Get a formatted timestamp string in the current language + * for displaying to the user. + * + * @param string $timestamp + * @return string + */ + protected function getFormattedTimestamp( $timestamp ) { + return $this->getLanguage()->timeanddate( + wfTimestamp( TS_MW, $timestamp ), true + ); + } + + /** + * @param stdClass $row + * @return string diff, hist and page other links related to the change */ protected function getLinksFromRow( $row ) { - $links = array(); + $links = []; // Log items if ( $row->cuc_type == RC_LOG ) { $title = Title::makeTitle( $row->cuc_namespace, $row->cuc_title ); // @todo FIXME: Hard coded parentheses. - $links['log'] = '(' . Linker::linkKnown( + $links['log'] = '(' . $this->getLinkRenderer()->makeKnownLink( SpecialPage::getTitleFor( 'Log' ), $this->message['log'], - array(), - array( 'page' => $title->getPrefixedText() ) + [], + [ 'page' => $title->getPrefixedText() ] ) . ')'; } else { $title = Title::makeTitle( $row->cuc_namespace, $row->cuc_title ); - # New pages + // New pages if ( $row->cuc_type == RC_NEW ) { $links['diff'] = '(' . $this->message['diff'] . ') '; } else { - # Diff link + // Diff link // @todo FIXME: Hard coded parentheses. - $links['diff'] = ' (' . Linker::linkKnown( + $links['diff'] = ' (' . $this->getLinkRenderer()->makeKnownLink( $title, $this->message['diff'], - array(), - array( + [], + [ 'curid' => $row->cuc_page_id, 'diff' => $row->cuc_this_oldid, 'oldid' => $row->cuc_last_oldid - ) + ] ) . ') '; } - # History link + // History link // @todo FIXME: Hard coded parentheses. - $links['history'] = ' (' . Linker::linkKnown( + $links['history'] = ' (' . $this->getLinkRenderer()->makeKnownLink( $title, $this->message['hist'], - array(), - array( + [], + [ 'curid' => $row->cuc_page_id, 'action' => 'history' - ) + ] ) . ') . . '; - # Some basic flags + // Some basic flags if ( $row->cuc_type == RC_NEW ) { - $links['newpage'] = '<span class="newpage">' . $this->message['newpageletter'] . '</span>'; + $links['newpage'] = '<span class="newpage">' . $this->message['newpageletter'] . + '</span>'; } if ( $row->cuc_minor ) { - $links['minor'] = '<span class="minor">' . $this->message['minoreditletter'] . '</span>'; + $links['minor'] = '<span class="minor">' . $this->message['minoreditletter'] . + '</span>'; } - # Page link - $links['title'] = Linker::link( $title ); + // Page link + $links['title'] = $this->getLinkRenderer()->makeLink( $title ); } - Hooks::run( 'SpecialCheckUserGetLinksFromRow', array( $this, $row, &$links ) ); + Hooks::run( 'SpecialCheckUserGetLinksFromRow', [ $this, $row, &$links ] ); if ( is_array( $links ) ) { return implode( ' ', $links ); } else { - wfDebugLog( __CLASS__, __METHOD__ . ': Expected array from SpecialCheckUserGetLinksFromRow $links param, but received ' . gettype( $links ) ); + wfDebugLog( __CLASS__, + __METHOD__ . ': Expected array from SpecialCheckUserGetLinksFromRow $links param,' + . ' but received ' . gettype( $links ) + ); return ''; } } protected static function userWasBlocked( $name ) { $userpage = Title::makeTitle( NS_USER, $name ); - return wfGetDB( DB_SLAVE )->selectField( 'logging', '1', - array( 'log_type' => array( 'block', 'suppress' ), + return (bool)wfGetDB( DB_REPLICA )->selectField( 'logging', '1', + [ + 'log_type' => [ 'block', 'suppress' ], 'log_action' => 'block', 'log_namespace' => $userpage->getNamespace(), - 'log_title' => $userpage->getDBkey() ), + 'log_title' => $userpage->getDBkey() + ], __METHOD__, - array( 'USE INDEX' => 'page_time' ) ); + [ 'USE INDEX' => 'page_time' ] ); } /** @@ -1359,56 +1506,45 @@ class CheckUser extends SpecialPage { * @param string $username * @return string */ - protected static function buildGroupLink( $group, $username = '#' ) { - static $cache = array(); + protected static function buildGroupLink( $group, $username ) { + static $cache = []; if ( !isset( $cache[$group] ) ) { - $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group, $username ) ); + $cache[$group] = UserGroupMembership::getLink( + $group, RequestContext::getMain(), 'html' + ); } return $cache[$group]; } /** - * @param DatabaseBase $db - * @param string $ip + * @param IDatabase $db + * @param string $target an IP address or CIDR range * @param string|bool $xfor - * @return mixed array/false conditions + * @return array|false array for valid conditions, false if invalid */ - public static function getIpConds( $db, $ip, $xfor = false ) { + public static function getIpConds( $db, $target, $xfor = false ) { global $wgCheckUserCIDRLimit; - $type = ( $xfor ) ? 'xff' : 'ip'; - // IPv4 CIDR, 16-32 bits - $matches = array(); - if ( preg_match( '#^(\d+\.\d+\.\d+\.\d+)/(\d+)$#', $ip, $matches ) ) { - if ( $matches[2] < $wgCheckUserCIDRLimit['IPv4'] || $matches[2] > 32 ) { - return false; // invalid + $type = $xfor ? 'xff' : 'ip'; + if ( IP::isValidRange( $target ) ) { + list( $ip, $range ) = explode( '/', $target, 2 ); + list( $start, $end ) = IP::parseRange( $target ); + if ( ( IP::isIPv4( $ip ) && $range < $wgCheckUserCIDRLimit['IPv4'] ) || + ( IP::isIPv6( $ip ) && $range < $wgCheckUserCIDRLimit['IPv6'] ) ) { + return false; // range is too wide } - 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, 48-128 bits - if ( $matches[1] < $wgCheckUserCIDRLimit['IPv6'] || $matches[1] > 128 ) { - 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( '#^(\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 ); + return [ 'cuc_' . $type . '_hex BETWEEN ' . $db->addQuotes( $start ) . + ' AND ' . $db->addQuotes( $end ) ]; + } elseif ( IP::isValid( $target ) ) { + return [ "cuc_{$type}_hex" => IP::toHex( $target ) ]; } - // throw away this query, incomplete IP, these don't get through the entry point anyway - return false; // invalid + return false; // invalid IP } protected function getTimeConds( $period ) { if ( !$period ) { return '1 = 1'; } - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $cutoff_unixtime = time() - ( $period * 24 * 3600 ); $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 ); $cutoff = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) ); @@ -1416,7 +1552,7 @@ class CheckUser extends SpecialPage { } public static function addLogEntry( $logType, $targetType, $target, $reason, $targetID = 0 ) { - global $wgUser; + $user = RequestContext::getMain()->getUser(); if ( $targetType == 'ip' ) { list( $rangeStart, $rangeEnd ) = IP::parseRange( $target ); @@ -1428,27 +1564,56 @@ class CheckUser extends SpecialPage { $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; + $timestamp = time(); + $data = [ + 'cul_user' => $user->getId(), + 'cul_user_text' => $user->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 + ]; + + DeferredUpdates::addCallableUpdate( + function () use ( $data, $timestamp ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( + 'cu_log', + [ + 'cul_timestamp' => $dbw->timestamp( $timestamp ) + ] + $data, + __METHOD__ + ); + }, + DeferredUpdates::PRESEND // fail on error and show no output + ); + } + + /** + * Return an array of subpages beginning with $search that this special page will accept. + * + * @param string $search Prefix to search for + * @param int $limit Maximum number of results to return (usually 10) + * @param int $offset Number of results to skip (usually 0) + * @return string[] Matching subpages + */ + public function prefixSearchSubpages( $search, $limit, $offset ) { + if ( !class_exists( 'UserNamePrefixSearch' ) ) { // check for version 1.27 + return []; + } + $user = User::newFromName( $search ); + if ( !$user ) { + // No prefix suggestion for invalid user + return []; + } + // Autocomplete subpage as user list - public to allow caching + return UserNamePrefixSearch::search( 'public', $search, $limit, $offset ); } protected function getGroupName() { return 'users'; } } - |