diff --git a/maintenance/RemoveProtectedFlagFromFilter.php b/maintenance/RemoveProtectedFlagFromFilter.php new file mode 100644 index 000000000..3ad0a5a3f --- /dev/null +++ b/maintenance/RemoveProtectedFlagFromFilter.php @@ -0,0 +1,75 @@ +addDescription( + 'Remove the "protected" flag from a filter, while keeping other privacy flags' + ); + $this->addArg( 'filter', 'ID of the protected filter to update' ); + $this->requireExtension( 'Abuse Filter' ); + } + + /** + * @inheritDoc + */ + public function execute() { + $filter = $this->getArg( 0 ); + + $privacyLevel = $this->getReplicaDB()->newSelectQueryBuilder() + ->select( 'af_hidden' ) + ->from( 'abuse_filter' ) + ->where( [ + 'af_id' => $filter + ] ) + ->caller( __METHOD__ ) + ->fetchField(); + + if ( $privacyLevel === false ) { + $this->fatalError( "Filter $filter not found.\n" ); + } + + if ( ( $privacyLevel & Flags::FILTER_USES_PROTECTED_VARS ) === 0 ) { + $this->output( "Filter $filter is not protected. Nothing to update.\n" ); + return false; + } + + // The new privacy level is the old level with the bit representing "protected" unset. + $newPrivacyLevel = (string)( $privacyLevel & ( ~Flags::FILTER_USES_PROTECTED_VARS ) ); + + $this->getPrimaryDB()->newUpdateQueryBuilder() + ->update( 'abuse_filter' ) + ->set( [ 'af_hidden' => $newPrivacyLevel ] ) + ->where( [ 'af_id' => $filter ] ) + ->caller( __METHOD__ ) + ->execute(); + + $this->output( "Successfully removed \"protected\" flag from filter $filter.\n" ); + return true; + } +} + +$maintClass = RemoveProtectedFlagFromFilter::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/tests/phpunit/integration/Maintenance/RemoveProtectedFlagFromFilterTest.php b/tests/phpunit/integration/Maintenance/RemoveProtectedFlagFromFilterTest.php new file mode 100644 index 000000000..5463c555b --- /dev/null +++ b/tests/phpunit/integration/Maintenance/RemoveProtectedFlagFromFilterTest.php @@ -0,0 +1,113 @@ + 1, + 'af_timestamp' => $this->getDb()->timestamp( '20000101000000' ), + 'af_enabled' => 1, + 'af_comments' => '', + 'af_public_comments' => 'Test filter', + 'af_hit_count' => 0, + 'af_throttled' => 0, + 'af_deleted' => 0, + 'af_global' => 0, + 'af_group' => 'default', + 'af_pattern' => '', + 'af_actions' => '', + ]; + $rows = [ + [ + 'af_id' => 1, + 'af_hidden' => Flags::FILTER_PUBLIC + ] + $defaultRow, + [ + 'af_id' => 2, + 'af_hidden' => Flags::FILTER_HIDDEN + ] + $defaultRow, + [ + 'af_id' => 3, + 'af_hidden' => Flags::FILTER_USES_PROTECTED_VARS + ] + $defaultRow, + [ + 'af_id' => 4, + 'af_hidden' => Flags::FILTER_USES_PROTECTED_VARS | Flags::FILTER_HIDDEN + ] + $defaultRow, + ]; + $this->getDb()->newInsertQueryBuilder() + ->insertInto( 'abuse_filter' ) + ->rows( $rows ) + ->caller( __METHOD__ ) + ->execute(); + } + + public function testExecuteNonexistentFilter() { + $filter = 100; + $this->expectCallToFatalError(); + $this->maintenance->loadParamsAndArgs( null, null, [ $filter ] ); + $this->maintenance->execute(); + } + + /** + * @dataProvider provideUnprotectedFilter + */ + public function testExecuteUnprotectedFilter( $filter ) { + $this->expectOutputString( "Filter $filter is not protected. Nothing to update.\n" ); + $this->maintenance->loadParamsAndArgs( null, null, [ $filter ] ); + $this->assertFalse( $this->maintenance->execute() ); + } + + public function provideUnprotectedFilter() { + return [ + 'Fail on public filter' => [ + 'filterId' => 1, + ], + 'Fail on unprotected, private filter' => [ + 'filterId' => 2, + ], + ]; + } + + /** + * @dataProvider provideProtectedFilter + */ + public function testExecuteProtectedFilter( $filter ) { + $this->maintenance->loadParamsAndArgs( null, null, [ $filter ] ); + $this->assertTrue( $this->maintenance->execute() ); + } + + public function provideProtectedFilter() { + return [ + 'Remove protected flag from protected filter' => [ + 'filterId' => 3, + 'expectedNewPrivacyLevel' => Flags::FILTER_PUBLIC, + ], + 'Remove protected flag from private, protected filter' => [ + 'filterId' => 4, + 'expectedNewPrivacyLevel' => Flags::FILTER_HIDDEN, + ], + ]; + } +}