diff options
Diffstat (limited to 'CheckUser/includes/CheckUserHooks.php')
-rw-r--r-- | CheckUser/includes/CheckUserHooks.php | 280 |
1 files changed, 211 insertions, 69 deletions
diff --git a/CheckUser/includes/CheckUserHooks.php b/CheckUser/includes/CheckUserHooks.php index 9a8b4256..4facc308 100644 --- a/CheckUser/includes/CheckUserHooks.php +++ b/CheckUser/includes/CheckUserHooks.php @@ -1,9 +1,68 @@ <?php -use MediaWiki\MediaWikiServices; use MediaWiki\Auth\AuthenticationResponse; +use MediaWiki\Block\DatabaseBlock; +use MediaWiki\CheckUser\SpecialInvestigate; +use MediaWiki\CheckUser\SpecialInvestigateBlock; +use MediaWiki\CheckUser\SpecialInvestigateLog; +use MediaWiki\MediaWikiServices; +use Wikimedia\IPUtils; +use Wikimedia\Rdbms\IDatabase; class CheckUserHooks { + + /** + * The maximum number of bytes that fit in CheckUser's text fields + * (cuc_agent,cuc_actiontext,cuc_comment,cuc_xff) + */ + private const TEXT_FIELD_LENGTH = 255; + + /** + * @param array &$list + * @return bool + */ + public static function onSpecialPage_initList( &$list ) { + global $wgCheckUserEnableSpecialInvestigate; + + if ( $wgCheckUserEnableSpecialInvestigate ) { + $list['Investigate'] = [ + 'class' => SpecialInvestigate::class, + 'services' => [ + 'LinkRenderer', + 'ContentLanguage', + 'UserOptionsManager', + 'CheckUserPreliminaryCheckPagerFactory', + 'CheckUserComparePagerFactory', + 'CheckUserTimelinePagerFactory', + 'CheckUserTokenQueryManager', + 'CheckUserDurationManager', + 'CheckUserEventLogger', + 'CheckUserGuidedTourLauncher', + 'CheckUserHookRunner', + ], + ]; + + $list['InvestigateLog'] = [ + 'class' => SpecialInvestigateLog::class, + 'services' => [ + 'CheckUserInvestigateLogPagerFactory', + ], + ]; + + $list['InvestigateBlock'] = [ + 'class' => SpecialInvestigateBlock::class, + 'services' => [ + 'PermissionManager', + 'TitleFormatter', + 'UserFactory', + 'CheckUserEventLogger', + ] + ]; + } + + return true; + } + /** * Hook function for RecentChange_save * Saves user data into the cu_changes table @@ -13,14 +72,14 @@ class CheckUserHooks { * @return bool */ public static function updateCheckUserData( RecentChange $rc ) { - global $wgRequest; + global $wgRequest, $wgCheckUserLogAdditionalRights; /** * RC_CATEGORIZE recent changes are generally triggered by other edits. * Thus there is no reason to store checkuser data about them. * @see https://phabricator.wikimedia.org/T125209 */ - if ( defined( 'RC_CATEGORIZE' ) && $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE ) { + if ( $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE ) { return true; } /** @@ -28,7 +87,7 @@ class CheckUserHooks { * Thus there is no reason to store checkuser data about them. * @see https://phabricator.wikimedia.org/T125664 */ - if ( defined( 'RC_EXTERNAL' ) && $rc->getAttribute( 'rc_type' ) == RC_EXTERNAL ) { + if ( $rc->getAttribute( 'rc_type' ) == RC_EXTERNAL ) { return true; } @@ -45,17 +104,33 @@ class CheckUserHooks { // BC: check if log_type and log_action exists // If not, then $rc_comment is the actiontext and comment if ( isset( $attribs['rc_log_type'] ) && $attribs['rc_type'] == RC_LOG ) { + $pm = MediaWikiServices::getInstance()->getPermissionManager(); $target = Title::makeTitle( $attribs['rc_namespace'], $attribs['rc_title'] ); $context = RequestContext::newExtraneousContext( $target ); + $scope = $pm->addTemporaryUserRights( $context->getUser(), $wgCheckUserLogAdditionalRights ); + $formatter = LogFormatter::newFromRow( $rc->getAttributes() ); $formatter->setContext( $context ); $actionText = $formatter->getPlainActionText(); + + \Wikimedia\ScopedCallback::consume( $scope ); } else { $actionText = ''; } - $dbw = wfGetDB( DB_MASTER ); + $comment = $rc->getAttribute( 'rc_comment' ); + + $services = MediaWikiServices::getInstance(); + $contLang = $services->getContentLanguage(); + + // (T199323) Truncate text fields prior to database insertion + // Attempting to insert too long text will cause an error in MariaDB/MySQL strict mode + $actionText = $contLang->truncateForDatabase( $actionText, self::TEXT_FIELD_LENGTH ); + $agent = $contLang->truncateForDatabase( $agent, self::TEXT_FIELD_LENGTH ); + $xff = $contLang->truncateForDatabase( $xff, self::TEXT_FIELD_LENGTH ); + $comment = $contLang->truncateForDatabase( $comment, self::TEXT_FIELD_LENGTH ); + $rcRow = [ 'cuc_namespace' => $attribs['rc_namespace'], 'cuc_title' => $attribs['rc_title'], @@ -63,15 +138,15 @@ class CheckUserHooks { 'cuc_user' => $attribs['rc_user'], 'cuc_user_text' => $attribs['rc_user_text'], 'cuc_actiontext' => $actionText, - 'cuc_comment' => $rc->getAttribute( 'rc_comment' ), + 'cuc_comment' => $comment, 'cuc_this_oldid' => $attribs['rc_this_oldid'], 'cuc_last_oldid' => $attribs['rc_last_oldid'], 'cuc_type' => $attribs['rc_type'], 'cuc_timestamp' => $attribs['rc_timestamp'], - 'cuc_ip' => IP::sanitizeIP( $ip ), - 'cuc_ip_hex' => $ip ? IP::toHex( $ip ) : null, + 'cuc_ip' => IPUtils::sanitizeIP( $ip ), + 'cuc_ip_hex' => $ip ? IPUtils::toHex( $ip ) : null, 'cuc_xff' => !$isSquidOnly ? $xff : '', - 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IP::toHex( $xff_ip ) : null, + 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IPUtils::toHex( $xff_ip ) : null, 'cuc_agent' => $agent ]; # On PG, MW unsets cur_id due to schema incompatibilites. So it may not be set! @@ -80,6 +155,8 @@ class CheckUserHooks { } Hooks::run( 'CheckUserInsertForRecentChange', [ $rc, &$rcRow ] ); + + $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER ); $dbw->insert( 'cu_changes', $rcRow, __METHOD__ ); return true; @@ -102,24 +179,36 @@ class CheckUserHooks { list( $xff_ip, $isSquidOnly ) = self::getClientIPfromXFF( $xff ); // Get agent $agent = $wgRequest->getHeader( 'User-Agent' ); - $dbw = wfGetDB( DB_MASTER ); + + $actionText = wfMessage( 'checkuser-reset-action', $account->getName() ) + ->inContentLanguage()->text(); + + $services = MediaWikiServices::getInstance(); + $contLang = $services->getContentLanguage(); + + // (T199323) Truncate comment fields prior to database insertion + // Attempting to insert too long text will cause an error in MariaDB/MySQL strict mode + $actionText = $contLang->truncateForDatabase( $actionText, self::TEXT_FIELD_LENGTH ); + $agent = $contLang->truncateForDatabase( $agent, self::TEXT_FIELD_LENGTH ); + $xff = $contLang->truncateForDatabase( $xff, self::TEXT_FIELD_LENGTH ); + + $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER ); $rcRow = [ 'cuc_namespace' => NS_USER, 'cuc_title' => '', 'cuc_minor' => 0, 'cuc_user' => $user->getId(), 'cuc_user_text' => $user->getName(), - 'cuc_actiontext' => wfMessage( 'checkuser-reset-action', $account->getName() ) - ->inContentLanguage()->text(), + 'cuc_actiontext' => $actionText, 'cuc_comment' => '', 'cuc_this_oldid' => 0, 'cuc_last_oldid' => 0, 'cuc_type' => RC_LOG, 'cuc_timestamp' => $dbw->timestamp( wfTimestampNow() ), - 'cuc_ip' => IP::sanitizeIP( $ip ), - 'cuc_ip_hex' => $ip ? IP::toHex( $ip ) : null, + 'cuc_ip' => IPUtils::sanitizeIP( $ip ), + 'cuc_ip_hex' => $ip ? IPUtils::toHex( $ip ) : null, 'cuc_xff' => !$isSquidOnly ? $xff : '', - 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IP::toHex( $xff_ip ) : null, + 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IPUtils::toHex( $xff_ip ) : null, 'cuc_agent' => $agent ]; $dbw->insert( 'cu_changes', $rcRow, __METHOD__ ); @@ -160,24 +249,36 @@ class CheckUserHooks { // Get agent $agent = $wgRequest->getHeader( 'User-Agent' ); - $dbr = wfGetDB( DB_REPLICA ); + $actionText = wfMessage( 'checkuser-email-action', $hash )->inContentLanguage()->text(); + + $services = MediaWikiServices::getInstance(); + $contLang = $services->getContentLanguage(); + + // (T199323) Truncate text fields prior to database insertion + // Attempting to insert too long text will cause an error in MariaDB/MySQL strict mode + $actionText = $contLang->truncateForDatabase( $actionText, self::TEXT_FIELD_LENGTH ); + $agent = $contLang->truncateForDatabase( $agent, self::TEXT_FIELD_LENGTH ); + $xff = $contLang->truncateForDatabase( $xff, self::TEXT_FIELD_LENGTH ); + + $lb = $services->getDBLoadBalancer(); + $dbr = $lb->getConnectionRef( DB_REPLICA ); + $rcRow = [ 'cuc_namespace' => NS_USER, 'cuc_title' => '', 'cuc_minor' => 0, 'cuc_user' => $userFrom->getId(), 'cuc_user_text' => $userFrom->getName(), - 'cuc_actiontext' => - wfMessage( 'checkuser-email-action', $hash )->inContentLanguage()->text(), + 'cuc_actiontext' => $actionText, 'cuc_comment' => '', 'cuc_this_oldid' => 0, 'cuc_last_oldid' => 0, 'cuc_type' => RC_LOG, 'cuc_timestamp' => $dbr->timestamp( wfTimestampNow() ), - 'cuc_ip' => IP::sanitizeIP( $ip ), - 'cuc_ip_hex' => $ip ? IP::toHex( $ip ) : null, + 'cuc_ip' => IPUtils::sanitizeIP( $ip ), + 'cuc_ip_hex' => $ip ? IPUtils::toHex( $ip ) : null, 'cuc_xff' => !$isSquidOnly ? $xff : '', - 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IP::toHex( $xff_ip ) : null, + 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IPUtils::toHex( $xff_ip ) : null, 'cuc_agent' => $agent ]; if ( trim( $wgCUPublicKey ) != '' ) { @@ -187,8 +288,8 @@ class CheckUserHooks { } $fname = __METHOD__; - DeferredUpdates::addCallableUpdate( function () use ( $rcRow, $fname ) { - $dbw = wfGetDB( DB_MASTER ); + DeferredUpdates::addCallableUpdate( function () use ( $lb, $rcRow, $fname ) { + $dbw = $lb->getConnectionRef( DB_MASTER ); $dbw->insert( 'cu_changes', $rcRow, $fname ); } ); @@ -225,7 +326,19 @@ class CheckUserHooks { list( $xff_ip, $isSquidOnly ) = self::getClientIPfromXFF( $xff ); // Get agent $agent = $wgRequest->getHeader( 'User-Agent' ); - $dbw = wfGetDB( DB_MASTER ); + $services = MediaWikiServices::getInstance(); + $contLang = $services->getContentLanguage(); + + $actiontext = wfMessage( $actiontext )->inContentLanguage()->text(); + + // (T199323) Truncate text fields prior to database insertion + // Attempting to insert too long text will cause an error in MariaDB/MySQL strict mode + $actionText = $contLang->truncateForDatabase( $actiontext, self::TEXT_FIELD_LENGTH ); + $agent = $contLang->truncateForDatabase( $agent, self::TEXT_FIELD_LENGTH ); + $xff = $contLang->truncateForDatabase( $xff, self::TEXT_FIELD_LENGTH ); + + $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER ); + $rcRow = [ 'cuc_page_id' => 0, 'cuc_namespace' => NS_USER, @@ -233,16 +346,16 @@ class CheckUserHooks { 'cuc_minor' => 0, 'cuc_user' => $user->getId(), 'cuc_user_text' => $user->getName(), - 'cuc_actiontext' => wfMessage( $actiontext )->inContentLanguage()->text(), + 'cuc_actiontext' => $actionText, 'cuc_comment' => '', 'cuc_this_oldid' => 0, 'cuc_last_oldid' => 0, 'cuc_type' => RC_LOG, 'cuc_timestamp' => $dbw->timestamp( wfTimestampNow() ), - 'cuc_ip' => IP::sanitizeIP( $ip ), - 'cuc_ip_hex' => $ip ? IP::toHex( $ip ) : null, + 'cuc_ip' => IPUtils::sanitizeIP( $ip ), + 'cuc_ip_hex' => $ip ? IPUtils::toHex( $ip ) : null, 'cuc_xff' => !$isSquidOnly ? $xff : '', - 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IP::toHex( $xff_ip ) : null, + 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IPUtils::toHex( $xff_ip ) : null, 'cuc_agent' => $agent ]; $dbw->insert( 'cu_changes', $rcRow, __METHOD__ ); @@ -258,7 +371,7 @@ class CheckUserHooks { public static function onAuthManagerLoginAuthenticateAudit( AuthenticationResponse $ret, $user, $username ) { - global $wgRequest, $wgCheckUserLogLogins; + global $wgRequest, $wgCheckUserLogLogins, $wgCheckUserLogSuccessfulBotLogins; if ( !$wgCheckUserLogLogins ) { return; @@ -272,42 +385,68 @@ class CheckUserHooks { return; } + if ( + $wgCheckUserLogSuccessfulBotLogins !== true && + $ret->status === AuthenticationResponse::PASS + ) { + $userGroups = MediaWikiServices::getInstance() + ->getUserGroupManager() + ->getUserGroups( $user ); + + if ( in_array( 'bot', $userGroups ) ) { + return; + } + } + + $ip = $wgRequest->getIP(); + $xff = $wgRequest->getHeader( 'X-Forwarded-For' ); + list( $xff_ip, $isSquidOnly ) = self::getClientIPfromXFF( $xff ); + $agent = $wgRequest->getHeader( 'User-Agent' ); + $userName = $user->getName(); + if ( $ret->status === AuthenticationResponse::FAIL ) { $msg = 'checkuser-login-failure'; + $cuc_user = 0; + $cuc_user_text = $ip; } elseif ( $ret->status === AuthenticationResponse::PASS ) { $msg = 'checkuser-login-success'; + $cuc_user = $user->getId(); + $cuc_user_text = $userName; } else { // Abstain, Redirect, etc. return; } - $ip = $wgRequest->getIP(); - $xff = $wgRequest->getHeader( 'X-Forwarded-For' ); - list( $xff_ip, $isSquidOnly ) = self::getClientIPfromXFF( $xff ); - $agent = $wgRequest->getHeader( 'User-Agent' ); - $userName = $user->getName(); $target = "[[User:$userName|$userName]]"; - $msg = wfMessage( $msg ); - $msg->params( $target ); + $actionText = wfMessage( $msg )->params( $target )->inContentLanguage()->text(); - $dbw = wfGetDB( DB_MASTER ); + $services = MediaWikiServices::getInstance(); + $contLang = $services->getContentLanguage(); + + // (T199323) Truncate text fields prior to database insertion + // Attempting to insert too long text will cause an error in MariaDB/MySQL strict mode + $actionText = $contLang->truncateForDatabase( $actionText, self::TEXT_FIELD_LENGTH ); + $agent = $contLang->truncateForDatabase( $agent, self::TEXT_FIELD_LENGTH ); + $xff = $contLang->truncateForDatabase( $xff, self::TEXT_FIELD_LENGTH ); + + $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER ); $rcRow = [ 'cuc_page_id' => 0, 'cuc_namespace' => NS_USER, 'cuc_title' => '', 'cuc_minor' => 0, - 'cuc_user' => 0, - 'cuc_user_text' => $ip, - 'cuc_actiontext' => $msg->inContentLanguage()->text(), + 'cuc_user' => $cuc_user, + 'cuc_user_text' => $cuc_user_text, + 'cuc_actiontext' => $actionText, 'cuc_comment' => '', 'cuc_this_oldid' => 0, 'cuc_last_oldid' => 0, 'cuc_type' => RC_LOG, 'cuc_timestamp' => $dbw->timestamp( wfTimestampNow() ), - 'cuc_ip' => IP::sanitizeIP( $ip ), - 'cuc_ip_hex' => $ip ? IP::toHex( $ip ) : null, + 'cuc_ip' => IPUtils::sanitizeIP( $ip ), + 'cuc_ip_hex' => $ip ? IPUtils::toHex( $ip ) : null, 'cuc_xff' => !$isSquidOnly ? $xff : '', - 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IP::toHex( $xff_ip ) : null, + 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IPUtils::toHex( $xff_ip ) : null, 'cuc_agent' => $agent ]; $dbw->insert( 'cu_changes', $rcRow, __METHOD__ ); @@ -315,16 +454,24 @@ class CheckUserHooks { /** * Hook function to prune data from the cu_changes table - * @return true */ public static function maybePruneIPData() { - # Every 50th edit, prune the checkuser changes table. - if ( 0 == mt_rand( 0, 49 ) ) { - $fname = __METHOD__; - DeferredUpdates::addCallableUpdate( function () use ( $fname ) { + if ( mt_rand( 0, 9 ) != 0 ) { + return; + } + + DeferredUpdates::addUpdate( new AutoCommitUpdate( + wfGetDB( DB_MASTER ), + __METHOD__, + function ( IDatabase $dbw, $fname ) { global $wgCUDMaxAge; - $dbw = wfGetDB( DB_MASTER ); + $key = "{$dbw->getDomainID()}:PruneCheckUserData"; // per-wiki + $scopedLock = $dbw->getScopedLockAndFlush( $key, $fname, 1 ); + if ( !$scopedLock ) { + return; + } + $encCutoff = $dbw->addQuotes( $dbw->timestamp( time() - $wgCUDMaxAge ) ); $ids = $dbw->selectFieldValues( 'cu_changes', 'cuc_id', @@ -336,10 +483,8 @@ class CheckUserHooks { if ( $ids ) { $dbw->delete( 'cu_changes', [ 'cuc_id' => $ids ], $fname ); } - } ); - } - - return true; + } + ) ); } /** @@ -366,12 +511,7 @@ class CheckUserHooks { $ipchain = array_map( 'trim', explode( ',', $xff ) ); $ipchain = array_reverse( $ipchain ); - if ( class_exists( ProxyLookup::class ) ) { // MW 1.28+ - $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup(); - } else { - // This is kind of sketch, but is good enough for back-compat - $proxyLookup = new IP(); - } + $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup(); $client = null; // best guess of the client IP $isSquidOnly = false; // all proxy servers where site Squid/Varnish servers? @@ -380,7 +520,7 @@ class CheckUserHooks { # unless the address is not sensible (e.g. private). However, prefer private # IP addresses over proxy servers controlled by this site (more sensible). foreach ( $ipchain as $i => $curIP ) { - $curIP = IP::canonicalize( $curIP ); + $curIP = IPUtils::canonicalize( $curIP ); if ( $curIP === null ) { break; // not a valid IP address } @@ -391,14 +531,14 @@ class CheckUserHooks { } if ( isset( $ipchain[$i + 1] ) && - IP::isIPAddress( $ipchain[$i + 1] ) && + IPUtils::isIPAddress( $ipchain[$i + 1] ) && ( - IP::isPublic( $ipchain[$i + 1] ) || + IPUtils::isPublic( $ipchain[$i + 1] ) || $wgUsePrivateIPs || $curIsSquid // bug 48919 ) ) { - $client = IP::canonicalize( $ipchain[$i + 1] ); + $client = IPUtils::canonicalize( $ipchain[$i + 1] ); $isSquidOnly = ( $isSquidOnly && $curIsSquid ); continue; } @@ -446,7 +586,7 @@ class CheckUserHooks { // First time so populate cu_changes with recentchanges data. // Note: We cannot completely rely on updatelog here for old entries // as populateCheckUserTable.php doesn't check for duplicates - $updater->addPostDatabaseUpdateMaintenance( 'PopulateCheckUserTable' ); + $updater->addPostDatabaseUpdateMaintenance( PopulateCheckUserTable::class ); } } @@ -487,7 +627,9 @@ class CheckUserHooks { ) { $user = $sp->getUser(); $linkRenderer = $sp->getLinkRenderer(); - if ( $user->isAllowed( 'checkuser' ) ) { + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + + if ( $permissionManager->userHasRight( $user, 'checkuser' ) ) { $links['checkuser'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'CheckUser' ), $sp->msg( 'checkuser-contribs' )->text(), @@ -495,7 +637,7 @@ class CheckUserHooks { [ 'user' => $nt->getText() ] ); } - if ( $user->isAllowed( 'checkuser-log' ) ) { + if ( $permissionManager->userHasRight( $user, 'checkuser-log' ) ) { $links['checkuser-log'] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'CheckUserLog' ), $sp->msg( 'checkuser-contribs-log' )->text(), @@ -510,13 +652,13 @@ class CheckUserHooks { /** * Retroactively autoblocks the last IP used by the user (if it is a user) - * blocked by this Block. + * blocked by this block. * - * @param Block $block + * @param DatabaseBlock $block * @param array &$blockIds * @return bool */ - public static function doRetroactiveAutoblock( Block $block, array &$blockIds ) { + public static function doRetroactiveAutoblock( DatabaseBlock $block, array &$blockIds ) { $dbr = wfGetDB( DB_REPLICA ); $user = User::newFromName( (string)$block->getTarget(), false ); |