Viewing File: /home/omtekel/www/wp-content/upgrade/backup/Watcher.tar
EmergencyWatcher.php 0000666 00000012146 15133502121 0010512 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Watcher;
use AutoCommitUpdate;
use DeferredUpdates;
use InvalidArgumentException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\EchoNotifier;
use MediaWiki\Extension\AbuseFilter\EmergencyCache;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LBFactory;
/**
* Service for monitoring filters with restricted actions and preventing them
* from executing destructive actions ("throttling")
*
* @todo We should log throttling somewhere
*/
class EmergencyWatcher implements Watcher {
public const SERVICE_NAME = 'AbuseFilterEmergencyWatcher';
public const CONSTRUCTOR_OPTIONS = [
'AbuseFilterEmergencyDisableAge',
'AbuseFilterEmergencyDisableCount',
'AbuseFilterEmergencyDisableThreshold',
];
/** @var EmergencyCache */
private $cache;
/** @var LBFactory */
private $lbFactory;
/** @var FilterLookup */
private $filterLookup;
/** @var EchoNotifier */
private $notifier;
/** @var ServiceOptions */
private $options;
/**
* @param EmergencyCache $cache
* @param LBFactory $lbFactory
* @param FilterLookup $filterLookup
* @param EchoNotifier $notifier
* @param ServiceOptions $options
*/
public function __construct(
EmergencyCache $cache,
LBFactory $lbFactory,
FilterLookup $filterLookup,
EchoNotifier $notifier,
ServiceOptions $options
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->cache = $cache;
$this->lbFactory = $lbFactory;
$this->filterLookup = $filterLookup;
$this->notifier = $notifier;
$this->options = $options;
}
/**
* Determine which filters must be throttled, i.e. their potentially dangerous
* actions must be disabled.
*
* @param int[] $filters The filters to check
* @param string $group Group the filters belong to
* @return int[] Array of filters to be throttled
*/
public function getFiltersToThrottle( array $filters, string $group ): array {
$filters = array_intersect(
$filters,
$this->cache->getFiltersToCheckInGroup( $group )
);
if ( $filters === [] ) {
return [];
}
$threshold = $this->getEmergencyValue( 'threshold', $group );
$hitCountLimit = $this->getEmergencyValue( 'count', $group );
$maxAge = $this->getEmergencyValue( 'age', $group );
$time = (int)wfTimestamp( TS_UNIX );
$throttleFilters = [];
foreach ( $filters as $filter ) {
$filterObj = $this->filterLookup->getFilter( $filter, false );
// TODO: consider removing the filter from the group key
// after throttling
if ( $filterObj->isThrottled() ) {
continue;
}
$filterAge = (int)wfTimestamp( TS_UNIX, $filterObj->getTimestamp() );
$exemptTime = $filterAge + $maxAge;
// Optimize for the common case when filters are well-established
// This check somewhat duplicates the role of cache entry's TTL
// and could as well be removed
if ( $exemptTime <= $time ) {
continue;
}
// TODO: this value might be stale, there is no guarantee the match
// has actually been recorded now
$cacheValue = $this->cache->getForFilter( $filter );
if ( $cacheValue === false ) {
continue;
}
[ 'total' => $totalActions, 'matches' => $matchCount ] = $cacheValue;
if ( $matchCount > $hitCountLimit && ( $matchCount / $totalActions ) > $threshold ) {
// More than AbuseFilterEmergencyDisableCount matches, constituting more than
// AbuseFilterEmergencyDisableThreshold (a fraction) of last few edits.
// Disable it.
$throttleFilters[] = $filter;
}
}
return $throttleFilters;
}
/**
* Determine which a filters must be throttled and apply the throttling
*
* @inheritDoc
*/
public function run( array $localFilters, array $globalFilters, string $group ): void {
$throttleFilters = $this->getFiltersToThrottle( $localFilters, $group );
if ( !$throttleFilters ) {
return;
}
DeferredUpdates::addUpdate(
new AutoCommitUpdate(
$this->lbFactory->getPrimaryDatabase(),
__METHOD__,
static function ( IDatabase $dbw, $fname ) use ( $throttleFilters ) {
$dbw->update(
'abuse_filter',
[ 'af_throttled' => 1 ],
[ 'af_id' => $throttleFilters ],
$fname
);
}
)
);
DeferredUpdates::addCallableUpdate( function () use ( $throttleFilters ) {
foreach ( $throttleFilters as $filter ) {
$this->notifier->notifyForFilter( $filter );
}
} );
}
/**
* @param string $type The value to get, either "threshold", "count" or "age"
* @param string $group The filter's group (as defined in $wgAbuseFilterValidGroups)
* @return mixed
*/
private function getEmergencyValue( string $type, string $group ) {
switch ( $type ) {
case 'threshold':
$opt = 'AbuseFilterEmergencyDisableThreshold';
break;
case 'count':
$opt = 'AbuseFilterEmergencyDisableCount';
break;
case 'age':
$opt = 'AbuseFilterEmergencyDisableAge';
break;
default:
// @codeCoverageIgnoreStart
throw new InvalidArgumentException( '$type must be either "threshold", "count" or "age"' );
// @codeCoverageIgnoreEnd
}
$value = $this->options->get( $opt );
return $value[$group] ?? $value['default'];
}
}
UpdateHitCountWatcher.php 0000666 00000003073 15133502121 0011473 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Watcher;
use DeferredUpdates;
use MediaWiki\Extension\AbuseFilter\CentralDBManager;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LBFactory;
/**
* Watcher that updates hit counts of filters
*/
class UpdateHitCountWatcher implements Watcher {
public const SERVICE_NAME = 'AbuseFilterUpdateHitCountWatcher';
/** @var LBFactory */
private $lbFactory;
/** @var CentralDBManager */
private $centralDBManager;
/**
* @param LBFactory $lbFactory
* @param CentralDBManager $centralDBManager
*/
public function __construct(
LBFactory $lbFactory,
CentralDBManager $centralDBManager
) {
$this->lbFactory = $lbFactory;
$this->centralDBManager = $centralDBManager;
}
/**
* @inheritDoc
*/
public function run( array $localFilters, array $globalFilters, string $group ): void {
// Run in a DeferredUpdate to avoid primary database queries on raw/view requests (T274455)
DeferredUpdates::addCallableUpdate( function () use ( $localFilters, $globalFilters ) {
if ( $localFilters ) {
$this->updateHitCounts( $this->lbFactory->getPrimaryDatabase(), $localFilters );
}
if ( $globalFilters ) {
$fdb = $this->centralDBManager->getConnection( DB_PRIMARY );
$this->updateHitCounts( $fdb, $globalFilters );
}
} );
}
/**
* @param IDatabase $dbw
* @param array $loggedFilters
*/
private function updateHitCounts( IDatabase $dbw, array $loggedFilters ): void {
$dbw->update(
'abuse_filter',
[ 'af_hit_count=af_hit_count+1' ],
[ 'af_id' => $loggedFilters ],
__METHOD__
);
}
}
Watcher.php 0000666 00000000725 15133502121 0006653 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Watcher;
/**
* Classes inheriting this interface can be used to execute some actions after all filter have been checked.
*/
interface Watcher {
/**
* @param int[] $localFilters The local filters that matched the action
* @param int[] $globalFilters The global filters that matched the action
* @param string $group
*/
public function run( array $localFilters, array $globalFilters, string $group ): void;
}
EmergencyWatcherTest.php 0000666 00000011354 15133503713 0011362 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit\Watcher;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\EchoNotifier;
use MediaWiki\Extension\AbuseFilter\EmergencyCache;
use MediaWiki\Extension\AbuseFilter\Filter\ExistingFilter;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\AbuseFilter\Watcher\EmergencyWatcher;
use MediaWikiUnitTestCase;
use MWTimestamp;
use Wikimedia\Rdbms\LBFactory;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Watcher\EmergencyWatcher
*/
class EmergencyWatcherTest extends MediaWikiUnitTestCase {
private function getOptions(): ServiceOptions {
return new ServiceOptions(
EmergencyWatcher::CONSTRUCTOR_OPTIONS,
[
'AbuseFilterEmergencyDisableAge' => [
'default' => 86400,
'other' => 3600,
],
'AbuseFilterEmergencyDisableCount' => [
'default' => 2,
],
'AbuseFilterEmergencyDisableThreshold' => [
'default' => 0.05,
'other' => 0.01,
],
]
);
}
private function getEmergencyCache( array $cacheData, string $group ): EmergencyCache {
$cache = $this->createMock( EmergencyCache::class );
$cache->method( 'getForFilter' )
->with( 1 )
->willReturn( $cacheData );
$cache->method( 'getFiltersToCheckInGroup' )
->with( $group )
->willReturn( [ 1 ] );
return $cache;
}
private function getFilterLookup( array $filterData ): FilterLookup {
$lookup = $this->createMock( FilterLookup::class );
$lookup->method( 'getFilter' )
->with( 1, false )
->willReturnCallback( function () use ( $filterData ) {
$filterObj = $this->createMock( ExistingFilter::class );
$filterObj->method( 'getTimestamp' )->willReturn( $filterData['timestamp'] );
$filterObj->method( 'isThrottled' )->willReturn( $filterData['throttled'] ?? false );
return $filterObj;
} );
return $lookup;
}
public static function provideFiltersToThrottle(): array {
return [
'throttled, default group' => [
/* timestamp */ '20201016010000',
/* filterData */ [
'timestamp' => '20201016000000'
],
/* cacheData */ [
'total' => 100,
'matches' => 10
],
/* willThrottle */ true
],
'throttled, other group' => [
/* timestamp */ '20201016003000',
/* filterData */ [
'timestamp' => '20201016000000'
],
/* cacheData */ [
'total' => 100,
'matches' => 5
],
/* willThrottle */ true,
/* group */ 'other'
],
'not throttled, already is' => [
/* timestamp */ '20201016010000',
/* filterData */ [
'timestamp' => '20201016000000',
'throttled' => true,
],
/* cacheData */ [
'total' => 100,
'matches' => 10
],
/* willThrottle */ false
],
'not throttled, not enough actions' => [
/* timestamp */ '20201016010000',
/* filterData */ [
'timestamp' => '20201016000000'
],
/* cacheData */ [
'total' => 5,
'matches' => 2
],
/* willThrottle */ false
],
'not throttled, too few matches' => [
/* timestamp */ '20201016010000',
/* filterData */ [
'timestamp' => '20201016000000'
],
/* cacheData */ [
'total' => 100,
'matches' => 5
],
/* willThrottle */ false
],
'not throttled, too long period' => [
/* timestamp */ '20201017010000',
/* filterData */ [
'timestamp' => '20201016000000'
],
/* cacheData */ [
'total' => 1000,
'matches' => 100
],
/* willThrottle */ false
],
'not throttled, profiler reset' => [
/* timestamp */ '20201016010000',
/* filterData */ [
'timestamp' => '20201016000000'
],
/* cacheData */ [
'total' => 0,
'matches' => 0
],
/* willThrottle */ false
],
];
}
/**
* @covers ::getFiltersToThrottle
* @covers ::getEmergencyValue
* @dataProvider provideFiltersToThrottle
*/
public function testGetFiltersToThrottle(
string $timestamp,
array $filterData,
array $cacheData,
bool $willThrottle,
string $group = 'default'
) {
MWTimestamp::setFakeTime( $timestamp );
$watcher = new EmergencyWatcher(
$this->getEmergencyCache( $cacheData, $group ),
$this->createMock( LBFactory::class ),
$this->getFilterLookup( $filterData ),
$this->createMock( EchoNotifier::class ),
$this->getOptions()
);
$toThrottle = $watcher->getFiltersToThrottle(
[ 1 ],
$group
);
$this->assertSame(
$willThrottle ? [ 1 ] : [],
$toThrottle
);
}
/**
* @covers ::__construct
*/
public function testConstruct() {
$watcher = new EmergencyWatcher(
$this->createMock( EmergencyCache::class ),
$this->createMock( LBFactory::class ),
$this->createMock( FilterLookup::class ),
$this->createMock( EchoNotifier::class ),
$this->getOptions()
);
$this->assertInstanceOf( EmergencyWatcher::class, $watcher );
}
}
Back to Directory
File Manager