maintenance/SearchFilters: Allow searching by privacy level

Why:

* Filters should only be protected if they contain protected
  variables, or have done in the past.
* Before T377765, it was possible to protect any filter, and at
  least one filter was mistakenly protected.
* To check whether any other filters have been mistakenly
  protected, it is helpful to run a query on all databases for
  protected filters.

What:

* Add an option to maintenance/SearchFilters to allow searching
  by privacy level.

Bug: T380290
Change-Id: I40837de7c63fb8001734df80524a0bf79ff50135
This commit is contained in:
Thalia 2024-11-25 13:12:20 +00:00
parent 0ea7944ea3
commit 47fb507e28
2 changed files with 86 additions and 18 deletions

View file

@ -19,10 +19,17 @@ class SearchFilters extends Maintenance {
public function __construct() {
parent::__construct();
$this->addDescription(
'Find all filters matching a regular expression pattern and/or that have given consequence'
'Find all filters matching a regular expression pattern and/or that have a given ' .
'consequence and/or privacy level'
);
$this->addOption( 'pattern', 'Regular expression pattern', false, true );
$this->addOption( 'consequence', 'The consequence that the filter should have', false, true );
$this->addOption(
'privacy',
'The privacy level that the filter should include (a constant from Flags)',
false,
true
);
$this->requireExtension( 'Abuse Filter' );
}
@ -37,8 +44,12 @@ class SearchFilters extends Maintenance {
$this->fatalError( 'This maintenance script only works with MySQL databases' );
}
if ( !$this->getOption( 'pattern' ) && !$this->getOption( 'consequence' ) ) {
$this->fatalError( 'One of --consequence or --pattern should be specified.' );
if (
!$this->getOption( 'pattern' ) &&
!$this->getOption( 'consequence' ) &&
$this->getOption( 'privacy' ) === null
) {
$this->fatalError( 'One of --consequence, --pattern or --privacy should be specified.' );
}
$this->output( "wiki\tfilter\n" );
@ -59,6 +70,7 @@ class SearchFilters extends Maintenance {
$dbr = $this->getDB( DB_REPLICA, [], $dbname );
$pattern = $dbr->addQuotes( $this->getOption( 'pattern' ) );
$consequence = $this->getOption( 'consequence' );
$privacy = $this->getOption( 'privacy' );
if ( $dbr->tableExists( 'abuse_filter', __METHOD__ ) ) {
$queryBuilder = $dbr->newSelectQueryBuilder()
@ -74,6 +86,21 @@ class SearchFilters extends Maintenance {
new LikeValue( $dbr->anyString(), $consequence, $dbr->anyString() )
) );
}
if ( $privacy !== '' ) {
if ( $privacy === '0' ) {
$queryBuilder->where( $dbr->expr(
'af_hidden',
'=',
0
) );
} else {
$privacy = (int)$privacy;
$queryBuilder->where( $dbr->bitAnd(
'af_hidden',
$privacy
) . " = $privacy" );
}
}
$rows = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
foreach ( $rows as $row ) {

View file

@ -32,7 +32,6 @@ class SearchFiltersTest extends MaintenanceBaseTestCase {
'af_enabled' => 1,
'af_comments' => '',
'af_public_comments' => 'Test filter',
'af_hidden' => Flags::FILTER_PUBLIC,
'af_hit_count' => 0,
'af_throttled' => 0,
'af_deleted' => 0,
@ -40,11 +39,29 @@ class SearchFiltersTest extends MaintenanceBaseTestCase {
'af_group' => 'default'
];
$rows = [
[ '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',
'af_id' => 1,
'af_pattern' => '',
'af_actions' => '',
'af_hidden' => Flags::FILTER_PUBLIC,
] + $defaultRow,
[
'af_id' => 2,
'af_pattern' => 'rmspecials(page_title) === "foo"',
'af_actions' => 'warn',
'af_hidden' => Flags::FILTER_PUBLIC,
] + $defaultRow,
[
'af_id' => 3,
'af_pattern' => 'user_editcount % 3 !== 1',
'af_actions' => 'warn,block',
'af_hidden' => Flags::FILTER_USES_PROTECTED_VARS,
] + $defaultRow,
[
'af_id' => 4,
'af_pattern' => 'rmspecials(added_lines_pst) !== ""',
'af_actions' => 'block',
'af_hidden' => Flags::FILTER_HIDDEN | Flags::FILTER_USES_PROTECTED_VARS,
] + $defaultRow
];
$this->getDb()->newInsertQueryBuilder()
@ -69,11 +86,11 @@ class SearchFiltersTest extends MaintenanceBaseTestCase {
];
}
public function testExecuteWhenNeitherPatternOrConsequenceProvided() {
public function testExecuteWhenNoArgumentsProvided() {
// 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->expectOutputString( "One of --consequence, --pattern or --privacy should be specified.\n" );
$this->maintenance->execute();
}
@ -87,24 +104,38 @@ class SearchFiltersTest extends MaintenanceBaseTestCase {
}
public static function provideSearches(): Generator {
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 ] ];
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 ] ];
yield 'single filter for privacy level search' => [ '', '', '1', [ 4 ] ];
yield 'multiple filters for privacy level search' => [ '', '', '2', [ 3, 4 ] ];
yield 'search for multiple privacy levels' => [ '', '', '3', [ 4 ] ];
yield 'search for public filters (handle zero)' => [ '', '', '0', [ 1, 2 ] ];
}
/**
* @param string $pattern
* @param string $consequence
* @param string $privacy
* @param array $expectedIDs
* @dataProvider provideSearches
*/
public function testExecute_singleWiki( string $pattern, string $consequence, array $expectedIDs ) {
public function testExecute_singleWiki(
string $pattern,
string $consequence,
string $privacy,
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, 'consequence' => $consequence ] );
$this->maintenance->loadParamsAndArgs( null, [
'pattern' => $pattern,
'consequence' => $consequence,
'privacy' => $privacy,
] );
$this->expectOutputString( $this->getExpectedOutput( $expectedIDs ) );
$this->maintenance->execute();
}
@ -112,16 +143,26 @@ class SearchFiltersTest extends MaintenanceBaseTestCase {
/**
* @param string $pattern
* @param string $consequence
* @param string $privacy
* @param array $expectedIDs
* @dataProvider provideSearches
*/
public function testExecute_multipleWikis( string $pattern, string $consequence, array $expectedIDs ) {
public function testExecute_multipleWikis(
string $pattern,
string $consequence,
string $privacy,
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, 'consequence' => $consequence ] );
$this->maintenance->loadParamsAndArgs( null, [
'pattern' => $pattern,
'consequence' => $consequence,
'privacy' => $privacy,
] );
$expectedText = $this->getExpectedOutput( $expectedIDs ) . $this->getExpectedOutput( $expectedIDs, false );
$this->expectOutputString( $expectedText );
$this->maintenance->execute();