diff --git a/maintenance/SearchFilters.php b/maintenance/SearchFilters.php index 1964db0af..03e2240dd 100644 --- a/maintenance/SearchFilters.php +++ b/maintenance/SearchFilters.php @@ -3,6 +3,9 @@ namespace MediaWiki\Extension\AbuseFilter\Maintenance; use Maintenance; +use MediaWiki\MainConfigNames; +use Wikimedia\Rdbms\IExpression; +use Wikimedia\Rdbms\LikeValue; // @codeCoverageIgnoreStart $IP = getenv( 'MW_INSTALL_PATH' ); @@ -15,8 +18,11 @@ require_once "$IP/maintenance/Maintenance.php"; class SearchFilters extends Maintenance { public function __construct() { parent::__construct(); - $this->addDescription( 'Find all filters matching a regular expression pattern' ); - $this->addOption( 'pattern', 'Regular expression pattern', true, true ); + $this->addDescription( + 'Find all filters matching a regular expression pattern and/or that have given consequence' + ); + $this->addOption( 'pattern', 'Regular expression pattern', false, true ); + $this->addOption( 'consequence', 'The consequence that the filter should have', false, true ); $this->requireExtension( 'Abuse Filter' ); } @@ -25,22 +31,17 @@ class SearchFilters extends Maintenance { * @see Maintenance:execute() */ public function execute() { - global $wgConf, $wgDBtype; + global $wgConf; - if ( $wgDBtype !== 'mysql' ) { - // Code using exit() cannot be tested (T272241) - // @codeCoverageIgnoreStart + if ( $this->getConfig()->get( MainConfigNames::DBtype ) !== 'mysql' ) { $this->fatalError( 'This maintenance script only works with MySQL databases' ); - // @codeCoverageIgnoreEnd + } + + if ( !$this->getOption( 'pattern' ) && !$this->getOption( 'consequence' ) ) { + $this->fatalError( 'One of --consequence or --pattern should be specified.' ); } $this->output( "wiki\tfilter\n" ); - if ( $this->getOption( 'pattern' ) === '' ) { - // Code using exit() cannot be tested (T272241) - // @codeCoverageIgnoreStart - $this->fatalError( 'Pattern cannot be empty' ); - // @codeCoverageIgnoreEnd - } if ( count( $wgConf->wikis ) > 0 ) { foreach ( $wgConf->wikis as $dbname ) { @@ -57,16 +58,23 @@ class SearchFilters extends Maintenance { private function getMatchingFilters( $dbname = false ) { $dbr = $this->getDB( DB_REPLICA, [], $dbname ); $pattern = $dbr->addQuotes( $this->getOption( 'pattern' ) ); + $consequence = $this->getOption( 'consequence' ); if ( $dbr->tableExists( 'abuse_filter' ) ) { - $rows = $dbr->newSelectQueryBuilder() + $queryBuilder = $dbr->newSelectQueryBuilder() ->select( [ 'dbname' => 'DATABASE()', 'af_id' ] ) - ->from( 'abuse_filter' ) - ->where( [ - "af_pattern RLIKE $pattern" - ] ) - ->caller( __METHOD__ ) - ->fetchResultSet(); + ->from( 'abuse_filter' ); + if ( $pattern ) { + $queryBuilder->where( "af_pattern RLIKE $pattern" ); + } + if ( $consequence ) { + $queryBuilder->where( $dbr->expr( + 'af_actions', + IExpression::LIKE, + new LikeValue( $dbr->anyString(), $consequence, $dbr->anyString() ) + ) ); + } + $rows = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); foreach ( $rows as $row ) { $this->output( $row->dbname . "\t" . $row->af_id . "\n" ); diff --git a/tests/phpunit/integration/Maintenance/SearchFiltersTest.php b/tests/phpunit/integration/Maintenance/SearchFiltersTest.php index 6bfca9bb3..50fca932b 100644 --- a/tests/phpunit/integration/Maintenance/SearchFiltersTest.php +++ b/tests/phpunit/integration/Maintenance/SearchFiltersTest.php @@ -5,6 +5,7 @@ namespace MediaWiki\Extension\AbuseFilter\Tests\Integration; use Generator; use MediaWiki\Extension\AbuseFilter\Filter\Flags; use MediaWiki\Extension\AbuseFilter\Maintenance\SearchFilters; +use MediaWiki\MainConfigNames; use MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase; /** @@ -14,17 +15,6 @@ use MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase; * @covers \MediaWiki\Extension\AbuseFilter\Maintenance\SearchFilters */ class SearchFiltersTest extends MaintenanceBaseTestCase { - - protected function setUp(): void { - global $wgDBtype; - - parent::setUp(); - - if ( $wgDBtype !== 'mysql' ) { - $this->markTestSkipped( 'The script only works on MySQL' ); - } - } - /** * @inheritDoc */ @@ -46,15 +36,16 @@ class SearchFiltersTest extends MaintenanceBaseTestCase { 'af_hit_count' => 0, 'af_throttled' => 0, 'af_deleted' => 0, - 'af_actions' => '', 'af_global' => 0, 'af_group' => 'default' ]; $rows = [ - [ 'af_id' => 1, 'af_pattern' => '' ] + $defaultRow, - [ 'af_id' => 2, 'af_pattern' => 'rmspecials(page_title) === "foo"' ] + $defaultRow, - [ 'af_id' => 3, 'af_pattern' => 'user_editcount % 3 !== 1' ] + $defaultRow, - [ 'af_id' => 4, 'af_pattern' => 'rmspecials(added_lines_pst) !== ""' ] + $defaultRow + [ 'af_id' => 1, 'af_pattern' => '', 'af_actions' => '' ] + $defaultRow, + [ 'af_id' => 2, 'af_pattern' => 'rmspecials(page_title) === "foo"', 'af_actions' => 'warn' ] + $defaultRow, + [ 'af_id' => 3, 'af_pattern' => 'user_editcount % 3 !== 1', 'af_actions' => 'warn,block' ] + $defaultRow, + [ + 'af_id' => 4, 'af_pattern' => 'rmspecials(added_lines_pst) !== ""', 'af_actions' => 'block', + ] + $defaultRow ]; $this->getDb()->newInsertQueryBuilder() ->insertInto( 'abuse_filter' ) @@ -63,6 +54,29 @@ class SearchFiltersTest extends MaintenanceBaseTestCase { ->execute(); } + /** @dataProvider provideNonMySQLDatabaseTypes */ + public function testExecuteForNonMySQLDatabaseType( $dbType ) { + $this->expectCallToFatalError(); + $this->expectOutputString( "This maintenance script only works with MySQL databases\n" ); + $this->overrideConfigValue( MainConfigNames::DBtype, $dbType ); + $this->maintenance->execute(); + } + + public static function provideNonMySQLDatabaseTypes() { + return [ + 'PostgresSQL' => [ 'postgres' ], + 'SQLite' => [ 'sqlite' ], + ]; + } + + public function testExecuteWhenNeitherPatternOrConsequenceProvided() { + // It is safe to mock the DB type here, as the script should exit before any queries are made + $this->overrideConfigValue( MainConfigNames::DBtype, 'mysql' ); + $this->expectCallToFatalError(); + $this->expectOutputString( "One of --consequence or --pattern should be specified.\n" ); + $this->maintenance->execute(); + } + private function getExpectedOutput( array $ids, bool $withHeader = true ): string { global $wgDBname; $expected = $withHeader ? "wiki\tfilter\n" : ''; @@ -73,32 +87,41 @@ class SearchFiltersTest extends MaintenanceBaseTestCase { } public static function provideSearches(): Generator { - yield 'single filter' => [ 'page_title', [ 2 ] ]; - yield 'multiple filters' => [ 'rmspecials', [ 2, 4 ] ]; - yield 'regex' => [ '[a-z]\(', [ 2, 4 ] ]; + yield 'single filter for pattern search' => [ 'page_title', '', [ 2 ] ]; + yield 'multiple filters for pattern search' => [ 'rmspecials', '', [ 2, 4 ] ]; + yield 'single filter when consequence specified' => [ 'rmspecials', 'block', [ 4 ] ]; + yield 'regex for pattern' => [ '[a-z]\(', '', [ 2, 4 ] ]; } /** * @param string $pattern + * @param string $consequence * @param array $expectedIDs * @dataProvider provideSearches */ - public function testExecute_singleWiki( string $pattern, array $expectedIDs ) { + public function testExecute_singleWiki( string $pattern, string $consequence, array $expectedIDs ) { + if ( $this->getDb()->getType() !== 'mysql' ) { + $this->markTestSkipped( 'The script only works on MySQL' ); + } $this->setMwGlobals( [ 'wgConf' => (object)[ 'wikis' => [] ] ] ); - $this->maintenance->loadParamsAndArgs( null, [ 'pattern' => $pattern ] ); + $this->maintenance->loadParamsAndArgs( null, [ 'pattern' => $pattern, 'consequence' => $consequence ] ); $this->expectOutputString( $this->getExpectedOutput( $expectedIDs ) ); $this->maintenance->execute(); } /** * @param string $pattern + * @param string $consequence * @param array $expectedIDs * @dataProvider provideSearches */ - public function testExecute_multipleWikis( string $pattern, array $expectedIDs ) { + public function testExecute_multipleWikis( string $pattern, string $consequence, array $expectedIDs ) { + if ( $this->getDb()->getType() !== 'mysql' ) { + $this->markTestSkipped( 'The script only works on MySQL' ); + } global $wgDBname; $this->setMwGlobals( [ 'wgConf' => (object)[ 'wikis' => [ $wgDBname, $wgDBname ] ] ] ); - $this->maintenance->loadParamsAndArgs( null, [ 'pattern' => $pattern ] ); + $this->maintenance->loadParamsAndArgs( null, [ 'pattern' => $pattern, 'consequence' => $consequence ] ); $expectedText = $this->getExpectedOutput( $expectedIDs ) . $this->getExpectedOutput( $expectedIDs, false ); $this->expectOutputString( $expectedText ); $this->maintenance->execute();