Page MenuHomeWickedGov Phorge

AbuseLogger.php
No OneTemporary

Size
12 KB
Referenced Files
None
Subscribers
None

AbuseLogger.php

<?php
namespace MediaWiki\Extension\AbuseFilter;
use InvalidArgumentException;
use MediaWiki\CheckUser\Hooks;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
use MediaWiki\Logging\ManualLogEntry;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use MediaWiki\User\UserIdentityValue;
use Profiler;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\ScopedCallback;
class AbuseLogger {
private Title $title;
private User $user;
private VariableHolder $vars;
private string $action;
/** @var string[][] A list of variable dumps generated by {@link self::storeVarDump} for de-duplication. */
private array $varDumps = [];
private CentralDBManager $centralDBManager;
private FilterLookup $filterLookup;
private VariablesBlobStore $varBlobStore;
private VariablesManager $varManager;
private EditRevUpdater $editRevUpdater;
private LBFactory $lbFactory;
private ServiceOptions $options;
private RuleCheckerFactory $ruleCheckerFactory;
private AbuseFilterPermissionManager $afPermissionManager;
private string $wikiID;
private string $requestIP;
/**
* @param CentralDBManager $centralDBManager
* @param FilterLookup $filterLookup
* @param VariablesBlobStore $varBlobStore
* @param VariablesManager $varManager
* @param EditRevUpdater $editRevUpdater
* @param LBFactory $lbFactory
* @param RuleCheckerFactory $ruleCheckerFactory
* @param AbuseFilterPermissionManager $afPermissionManager
* @param ServiceOptions $options
* @param string $wikiID
* @param string $requestIP
* @param Title $title
* @param User $user
* @param VariableHolder $vars
* @internal Use {@link AbuseLoggerFactory::newLogger} instead
*/
public function __construct(
CentralDBManager $centralDBManager,
FilterLookup $filterLookup,
VariablesBlobStore $varBlobStore,
VariablesManager $varManager,
EditRevUpdater $editRevUpdater,
LBFactory $lbFactory,
RuleCheckerFactory $ruleCheckerFactory,
AbuseFilterPermissionManager $afPermissionManager,
ServiceOptions $options,
string $wikiID,
string $requestIP,
Title $title,
User $user,
VariableHolder $vars
) {
if ( !$vars->varIsSet( 'action' ) ) {
throw new InvalidArgumentException( "The 'action' variable is not set." );
}
$this->centralDBManager = $centralDBManager;
$this->filterLookup = $filterLookup;
$this->varBlobStore = $varBlobStore;
$this->varManager = $varManager;
$this->editRevUpdater = $editRevUpdater;
$this->lbFactory = $lbFactory;
$this->afPermissionManager = $afPermissionManager;
$this->ruleCheckerFactory = $ruleCheckerFactory;
$this->options = $options;
$this->wikiID = $wikiID;
$this->requestIP = $requestIP;
$this->title = $title;
$this->user = $user;
$this->vars = $vars;
$this->action = $vars->getComputedVariable( 'action' )->toString();
}
/**
* Create and publish log entries for taken actions
*
* @param array[] $actionsTaken
* @return array Shape is [ 'local' => int[], 'global' => int[] ], IDs of logged filters
* @phan-return array{local:int[],global:int[]}
*/
public function addLogEntries( array $actionsTaken ): array {
$dbw = $this->lbFactory->getPrimaryDatabase();
$logTemplate = $this->buildLogTemplate();
$centralLogTemplate = [
'afl_wiki' => $this->wikiID,
];
$logRows = [];
$centralLogRows = [];
$loggedLocalFilters = [];
$loggedGlobalFilters = [];
foreach ( $actionsTaken as $filter => $actions ) {
[ $filterID, $global ] = GlobalNameUtils::splitGlobalName( $filter );
$thisLog = $logTemplate;
$thisLog['afl_filter_id'] = $filterID;
$thisLog['afl_global'] = (int)$global;
$thisLog['afl_actions'] = implode( ',', $actions );
// Don't log if we were only throttling.
// TODO This check should be removed or rewritten using Consequence objects
if ( $thisLog['afl_actions'] !== 'throttle' ) {
$logRows[] = $thisLog;
// Global logging
if ( $global ) {
$centralLog = $thisLog + $centralLogTemplate;
$centralLog['afl_filter_id'] = $filterID;
$centralLog['afl_global'] = 0;
$centralLog['afl_title'] = $this->title->getPrefixedText();
$centralLog['afl_namespace'] = 0;
$centralLogRows[] = $centralLog;
$loggedGlobalFilters[] = $filterID;
} else {
$loggedLocalFilters[] = $filterID;
}
}
}
if ( !count( $logRows ) ) {
return [ 'local' => [], 'global' => [] ];
}
$localLogIDs = $this->insertLocalLogEntries( $logRows, $dbw );
$globalLogIDs = [];
if ( count( $loggedGlobalFilters ) ) {
$fdb = $this->centralDBManager->getConnection( DB_PRIMARY );
$globalLogIDs = $this->insertCentralLogEntries( $centralLogRows, $fdb );
}
$this->editRevUpdater->setLogIdsForTarget(
$this->title,
[ 'local' => $localLogIDs, 'global' => $globalLogIDs ]
);
return [ 'local' => $loggedLocalFilters, 'global' => $loggedGlobalFilters ];
}
/**
* Creates a template to use for logging taken actions
*
* @return array
*/
private function buildLogTemplate(): array {
// If $this->user isn't safe to load (e.g. a failure during
// AbortAutoAccount), create a dummy anonymous user instead.
$user = $this->user->isSafeToLoad() ? $this->user : new User;
// Create a template
$logTemplate = [
'afl_user' => $user->getId(),
'afl_user_text' => $user->getName(),
'afl_timestamp' => $this->lbFactory->getReplicaDatabase()->timestamp(),
'afl_namespace' => $this->title->getNamespace(),
'afl_title' => $this->title->getDBkey(),
'afl_action' => $this->action,
'afl_ip' => $this->options->get( 'AbuseFilterLogIP' ) ? $this->requestIP : ''
];
// Hack to avoid revealing IPs of people creating accounts
if ( ( $this->action === 'createaccount' || $this->action === 'autocreateaccount' ) && !$user->getId() ) {
$logTemplate['afl_user_text'] = $this->vars->getComputedVariable( 'accountname' )->toString();
}
return $logTemplate;
}
/**
* @param array $data
* @return ManualLogEntry
*/
private function newLocalLogEntryFromData( array $data ): ManualLogEntry {
// Give grep a chance to find the usages:
// logentry-abusefilter-hit
$entry = new ManualLogEntry( 'abusefilter', 'hit' );
$user = new UserIdentityValue( $data['afl_user'], $data['afl_user_text'] );
$entry->setPerformer( $user );
$entry->setTarget( $this->title );
$filterName = GlobalNameUtils::buildGlobalName(
$data['afl_filter_id'],
$data['afl_global'] === 1
);
// Additional info
$entry->setParameters( [
'action' => $data['afl_action'],
'filter' => $filterName,
'actions' => $data['afl_actions'],
'log' => $data['afl_id'],
] );
return $entry;
}
/**
* @param array[] $logRows
* @param IDatabase $dbw
* @return int[]
*/
private function insertLocalLogEntries( array $logRows, IDatabase $dbw ): array {
$loggedIDs = [];
foreach ( $logRows as $data ) {
$data['afl_var_dump'] = $this->storeVarDump( $data['afl_filter_id'], (bool)$data['afl_global'], false );
$dbw->newInsertQueryBuilder()
->insertInto( 'abuse_filter_log' )
->row( $data )
->caller( __METHOD__ )
->execute();
$loggedIDs[] = $data['afl_id'] = $dbw->insertId();
// Send data to CheckUser if installed and we
// aren't already sending a notification to recentchanges
if ( ExtensionRegistry::getInstance()->isLoaded( 'CheckUser' )
&& !str_contains( $this->options->get( 'AbuseFilterNotifications' ) ?: '', 'rc' )
) {
$entry = $this->newLocalLogEntryFromData( $data );
$user = $entry->getPerformerIdentity();
// Invert the hack from ::buildLogTemplate because CheckUser attempts
// to assign an actor id to the non-existing user
if (
( $this->action === 'createaccount' || $this->action === 'autocreateaccount' )
&& !$user->getId()
) {
$entry->setPerformer( new UserIdentityValue( 0, $this->requestIP ) );
}
$rc = $entry->getRecentChange();
// We need to send the entries on POSTSEND to ensure that the user definitely exists, as a temporary
// account being created by this edit may not exist until after AbuseFilter processes the edit.
DeferredUpdates::addCallableUpdate( static function () use ( $rc ) {
// Silence the TransactionProfiler warnings for performing write queries (T359648).
$trxProfiler = Profiler::instance()->getTransactionProfiler();
$scope = $trxProfiler->silenceForScope( $trxProfiler::EXPECTATION_REPLICAS_ONLY );
Hooks::updateCheckUserData( $rc );
ScopedCallback::consume( $scope );
} );
}
if ( $this->options->get( 'AbuseFilterNotifications' ) !== false ) {
$filterID = $data['afl_filter_id'];
$global = $data['afl_global'];
if (
!$this->options->get( 'AbuseFilterNotificationsPrivate' ) &&
$this->filterLookup->getFilter( $filterID, $global )->isHidden()
) {
continue;
}
$entry = $this->newLocalLogEntryFromData( $data );
$this->publishEntry( $dbw, $entry );
}
}
return $loggedIDs;
}
/**
* @param array[] $centralLogRows
* @param IDatabase $fdb
* @return int[]
*/
private function insertCentralLogEntries( array $centralLogRows, IDatabase $fdb ): array {
$this->varManager->computeDBVars( $this->vars );
foreach ( $centralLogRows as $index => $data ) {
$centralLogRows[$index]['afl_var_dump'] = $this->storeVarDump(
$data['afl_filter_id'],
// All the filters logged centrally are global. Note, this must not use `afl_global`, because that is
// in the perspective of the central wiki, hence false: what we consider global on the current wiki is
// local to the central wiki.
true,
true
);
}
$loggedIDs = [];
foreach ( $centralLogRows as $row ) {
$fdb->newInsertQueryBuilder()
->insertInto( 'abuse_filter_log' )
->row( $row )
->caller( __METHOD__ )
->execute();
$loggedIDs[] = $fdb->insertId();
}
return $loggedIDs;
}
/**
* Returns the BlobStore address for use as the value of the afl_var_dump column for an AbuseFilter log entry.
*
* This method removes protected variables from the var dump that are not used in the filter
* associated with the AbuseFilter log to be created. It also de-duplicates var dumps where
* this is possible.
*
* @param int $filterId The filter associated with the AbuseFilter log entry
* @param bool $isGlobalFilter If the filter associated with the AbuseFilter log entry is global
* @param bool $useCentralDB Whether the dump should be stored in the central database
* @return string
*/
private function storeVarDump( int $filterId, bool $isGlobalFilter, bool $useCentralDB ): string {
// Generate a key for the varDumps instance cache used to de-duplicate var dumps where possible.
// The key for this cache is the protected variables used in the filter along with whether the
// var dump is global.
$filter = $this->filterLookup->getFilter( $filterId, $isGlobalFilter );
$usedVariables = $this->ruleCheckerFactory->newRuleChecker()->getUsedVars( $filter->getRules() );
$usedProtectedVariables = $this->afPermissionManager->getUsedProtectedVariables( $usedVariables );
if ( count( $usedProtectedVariables ) ) {
sort( $usedProtectedVariables );
$variablesKey = implode( ',', $usedProtectedVariables );
} else {
$variablesKey = 0;
}
$centralDBKey = (int)$useCentralDB;
// Create a new var dump if the instance cache does not have this key.
if (
!array_key_exists( $centralDBKey, $this->varDumps ) ||
!array_key_exists( $variablesKey, $this->varDumps[$centralDBKey] )
) {
// Filter out all protected variables that are not used in the current filter. Any other filter with
// the same list of protected filters will also use this var dump
$filteredVars = VariableHolder::newFromArray( $this->vars->getVars() );
$protectedVariables = $this->afPermissionManager->getProtectedVariables();
foreach ( array_keys( $filteredVars->getVars() ) as $varName ) {
if ( in_array( $varName, $protectedVariables ) && !in_array( $varName, $usedProtectedVariables ) ) {
$filteredVars->removeVar( $varName );
}
}
$this->varDumps[$centralDBKey][$variablesKey] = $this->varBlobStore->storeVarDump(
$filteredVars,
$useCentralDB
);
}
return $this->varDumps[$centralDBKey][$variablesKey];
}
/**
* Like ManualLogEntry::publish, but doesn't require an ID (which we don't have) and skips the
* tagging part
*
* @param IDatabase $dbw To cancel the callback if the log insertion fails
* @param ManualLogEntry $entry
*/
private function publishEntry( IDatabase $dbw, ManualLogEntry $entry ): void {
DeferredUpdates::addCallableUpdate(
function () use ( $entry ) {
$rc = $entry->getRecentChange();
$to = $this->options->get( 'AbuseFilterNotifications' );
if ( $to === 'rc' || $to === 'rcandudp' ) {
$rc->save( $rc::SEND_NONE );
}
if ( $to === 'udp' || $to === 'rcandudp' ) {
$rc->notifyRCFeeds();
}
},
DeferredUpdates::POSTSEND,
$dbw
);
}
}

File Metadata

Mime Type
text/x-php
Expires
Fri, Jul 3, 18:19 (1 d, 9 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
01/fc/4ed1bc460bb202af37f8187af3e0
Default Alt Text
AbuseLogger.php (12 KB)

Event Timeline