Viewing File: /home/omtekel/www/wp-content/upgrade/backup/Watcher.tar

EmergencyWatcher.php000066600000012146151335021210010512 0ustar00<?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.php000066600000003073151335021210011473 0ustar00<?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.php000066600000000725151335021210006653 0ustar00<?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.php000066600000011354151335037130011362 0ustar00<?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