From 0026a68a8ae10069e064c9c4c35be95e580fdf5a Mon Sep 17 00:00:00 2001 From: Daimona Eaytoy Date: Fri, 6 Jul 2018 12:08:55 +0200 Subject: [PATCH] Add PHPUnit tests for various generic functions Adding tests for generic functions in AbuseFilter class, ranging from simple utility function to variable computation. Bug: T42478 Change-Id: I903fb7ffbc436b27462e3e4611ab65ecb8a543ba --- tests/phpunit/AbuseFilterTest.php | 1401 +++++++++++++++++++++++++++++ 1 file changed, 1401 insertions(+) create mode 100644 tests/phpunit/AbuseFilterTest.php diff --git a/tests/phpunit/AbuseFilterTest.php b/tests/phpunit/AbuseFilterTest.php new file mode 100644 index 000000000..9018f0b89 --- /dev/null +++ b/tests/phpunit/AbuseFilterTest.php @@ -0,0 +1,1401 @@ +addToDatabase(); + $user->addGroup( 'basicFilteredUser' ); + self::$mUser = $user; + self::$mVariables = new AbuseFilterVariableHolder(); + // Make sure that the config we're using is the one we're expecting + $this->setMwGlobals( [ + 'wgUser' => $user, + 'wgRestrictionTypes' => [ + 'create', + 'edit', + 'move', + 'upload' + ], + 'wgAbuseFilterRestrictions' => [ + 'degroup' => true + ], + 'wgAbuseFilterIsCentral' => true, + 'wgAbuseFilterActions' => [ + 'throttle' => true, + 'warn' => true, + 'disallow' => true, + 'blockautopromote' => true, + 'block' => true, + 'rangeblock' => true, + 'degroup' => true, + 'tag' => true + ], + 'wgAbuseFilterValidGroups' => [ + 'default', + 'flow' + ], + 'wgEnableParserLimitReporting' => false + ] ); + $this->setGroupPermissions( [ + 'basicFilteredUser' => [ + 'abusefilter-view' => true + ], + 'intermediateFilteredUser' => [ + 'abusefilter-log' => true + ], + 'privilegedFilteredUser' => [ + 'abusefilter-private' => true, + 'abusefilter-revert' => true + ] + ] ); + } + + /** + * @see MediaWikiTestCase::tearDown + */ + protected function tearDown() { + $userGroups = self::$mUser->getGroups(); + // We want to start fresh + foreach ( $userGroups as $group ) { + self::$mUser->removeGroup( $group ); + } + parent::tearDown(); + } + + /** + * Given the name of a variable, naturally sets it to a determined amount + * + * @param string $var The variable name + * @return array the first position is the result (mixed), the second is a boolean + * indicating whether we've been able to compute the given variable + */ + private static function computeExpectedUserVariable( $var ) { + $success = true; + switch ( $var ) { + case 'user_editcount': + // Create a page and make the user edit it 7 times + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $page->doEditContent( + new WikitextContent( 'AbuseFilter test, page creation' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + for ( $i = 1; $i <= 7; $i++ ) { + $page->doEditContent( + new WikitextContent( "AbuseFilter test, page revision #$i" ), + 'Testing page for AbuseFilter', + EDIT_UPDATE, + false, + self::$mUser + ); + } + $result = 7; + break; + case 'user_name': + $result = self::$mUser->getName(); + break; + case 'user_emailconfirm': + $time = wfTimestampNow(); + self::$mUser->setEmailAuthenticationTimestamp( $time ); + $result = $time; + break; + case 'user_age': + $result = wfTimestampNow() - self::$mUser->getRegistration(); + break; + case 'user_groups': + self::$mUser->addGroup( 'intermediateFilteredUser' ); + $result = self::$mUser->getEffectiveGroups(); + break; + case 'user_rights': + self::$mUser->addGroup( 'privilegedFilteredUser' ); + $result = self::$mUser->getRights(); + break; + case 'user_blocked': + $block = new Block(); + $block->setTarget( self::$mUser ); + $block->setBlocker( 'UTSysop' ); + $block->mReason = 'Testing AbuseFilter variable user_blocked'; + $block->mExpiry = 'infinity'; + + $block->insert(); + $result = true; + break; + default: + $success = false; + $result = null; + } + return [ $result, $success ]; + } + + /** + * Check that the generated user-related variables are correct + * + * @param string $varName The name of the variable we're currently testing + * @covers AbuseFilter::generateUserVars + * @dataProvider provideUserVars + */ + public function testGenerateUserVars( $varName ) { + list( $computed, $successfully ) = self::computeExpectedUserVariable( $varName ); + if ( !$successfully ) { + $this->fail( "Given unknown user-related variable $varName." ); + } + + $variableHolder = AbuseFilter::generateUserVars( self::$mUser ); + $actual = $variableHolder->getVar( $varName )->toNative(); + $this->assertSame( + $computed, + $actual, + "AbuseFilter variable $varName is computed wrongly." + ); + } + + /** + * Data provider for testGenerateUserVars + * @return array + */ + public function provideUserVars() { + return [ + [ 'user_editcount' ], + [ 'user_name' ], + [ 'user_emailconfirm' ], + [ 'user_groups' ], + [ 'user_rights' ], + [ 'user_blocked' ] + ]; + } + + /** + * Check that user_age is correct. Needs a separate function to take into account the + * difference between this timestamp and the one in the actual code. + * + * @covers AbuseFilter::generateUserVars + */ + public function testUserAgeVar() { + $computed = self::computeExpectedUserVariable( 'user_age' )[0]; + + $variableHolder = AbuseFilter::generateUserVars( self::$mUser ); + $actual = $variableHolder->getVar( 'user_age' )->toNative(); + + $difference = abs( strtotime( $actual ) - strtotime( $computed ) ); + $this->assertLessThanOrEqual( + // 2 seconds should be a good confidence interval + 2, + $difference, + "AbuseFilter variable user_age is computed wrongly." + ); + } + + /** + * Given the name of a variable, naturally sets it to a determined amount + * + * @param string $suffix The suffix of the variable + * @param string|null $options Further options for the test + * @return array the first position is the result (mixed), the second is a boolean + * indicating whether we've been able to compute the given variable. If false, then + * the result may be null if the requested variable doesn't exist, or false if there + * has been some other problem. + */ + private static function computeExpectedTitleVariable( $suffix, $options ) { + self::$mTitle = Title::newFromText( 'AbuseFilter test' ); + $page = WikiPage::factory( self::$mTitle ); + if ( $options === 'restricted' ) { + $action = str_replace( '_restrictions_', '', $suffix ); + $namespace = 0; + if ( $action === 'upload' ) { + // Only files can have it + $namespace = 6; + } + self::$mTitle = Title::makeTitle( $namespace, 'AbuseFilter restrictions test' ); + $page = WikiPage::factory( self::$mTitle ); + if ( $action !== 'create' ) { + // To apply other restrictions, the title has to exist + $page->doEditContent( + new WikitextContent( 'AbuseFilter test for title variables' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + } + $_ = false; + $s = $page->doUpdateRestrictions( + [ $action => true ], + [ $action => 'infinity' ], + $_, + 'Testing restrictions for AbuseFilter', + self::$mUser + ); + } + $success = true; + switch ( $suffix ) { + case '_articleid': + $result = self::$mTitle->getArticleID(); + break; + case '_namespace': + $result = self::$mTitle->getNamespace(); + break; + case '_text': + $result = self::$mTitle->getText(); + break; + case '_prefixedtext': + $result = self::$mTitle->getPrefixedText(); + break; + case '_restrictions_create': + $restrictions = self::$mTitle->getRestrictions( 'create' ); + $restrictions = count( $restrictions ) ? $restrictions : []; + $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) ); + if ( $preliminarCheck ) { + $result = $restrictions; + } else { + $success = false; + $result = false; + } + break; + case '_restrictions_edit': + $restrictions = self::$mTitle->getRestrictions( 'edit' ); + $restrictions = count( $restrictions ) ? $restrictions : []; + $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) ); + if ( $preliminarCheck ) { + $result = $restrictions; + } else { + $success = false; + $result = false; + } + break; + case '_restrictions_move': + $restrictions = self::$mTitle->getRestrictions( 'move' ); + $restrictions = count( $restrictions ) ? $restrictions : []; + $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) ); + if ( $preliminarCheck ) { + $result = $restrictions; + } else { + $success = false; + $result = false; + } + break; + case '_restrictions_upload': + $restrictions = self::$mTitle->getRestrictions( 'upload' ); + $restrictions = count( $restrictions ) ? $restrictions : []; + $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) ); + if ( $preliminarCheck ) { + $result = $restrictions; + } else { + $success = false; + $result = false; + } + break; + case '_recent_contributors': + // Create the page and make a couple of edits from different users + $page->doEditContent( + new WikitextContent( 'AbuseFilter test for title variables' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + $mockContributors = [ 'Alice', 'Bob', 'Charlie' ]; + foreach ( $mockContributors as $user ) { + $page->doEditContent( + new WikitextContent( "AbuseFilter test, page revision by $user" ), + 'Testing page for AbuseFilter', + EDIT_UPDATE, + false, + User::newFromName( $user ) + ); + } + $contributors = array_reverse( $mockContributors ); + array_push( $contributors, self::$mUser->getName() ); + $result = $contributors; + break; + case '_first_contributor': + // Create the page and make a couple of edits from different users + $page->doEditContent( + new WikitextContent( 'AbuseFilter test for title variables' ), + 'Testing page for AbuseFilter', + EDIT_NEW, + false, + self::$mUser + ); + $mockContributors = [ 'Alice', 'Bob', 'Charlie' ]; + foreach ( $mockContributors as $user ) { + $page->doEditContent( + new WikitextContent( "AbuseFilter test, page revision by $user" ), + 'Testing page for AbuseFilter', + EDIT_UPDATE, + false, + User::newFromName( $user ) + ); + } + $result = self::$mUser->getName(); + break; + default: + $success = false; + $result = null; + } + return [ $result, $success ]; + } + + /** + * Check that the generated title-related variables are correct + * + * @param string $prefix The prefix of the variables we're currently testing + * @param string $suffix The suffix of the variables we're currently testing + * @param string|null $options Whether we want to execute the test with specific options + * Right now, this can only be 'restricted' for restrictions variables; in this case, + * the tested title will have the requested restriction. + * @covers AbuseFilter::generateTitleVars + * @dataProvider provideTitleVars + */ + public function testGenerateTitleVars( $prefix, $suffix, $options = null ) { + $varName = $prefix . $suffix; + list( $computed, $successfully ) = self::computeExpectedTitleVariable( $suffix, $options ); + if ( !$successfully ) { + if ( $computed === null ) { + $this->fail( "Given unknown title-related variable $varName." ); + } else { + $this->fail( "AbuseFilter variable $varName is computed wrongly." ); + } + } + + $variableHolder = AbuseFilter::generateTitleVars( self::$mTitle, $prefix ); + $actual = $variableHolder->getVar( $varName )->toNative(); + $this->assertSame( + $computed, + $actual, + "AbuseFilter variable $varName is computed wrongly." + ); + } + + /** + * Data provider for testGenerateUserVars + * @return array + */ + public function provideTitleVars() { + return [ + [ 'article', '_articleid' ], + [ 'article', '_namespace' ], + [ 'article', '_text' ], + [ 'article', '_prefixedtext' ], + [ 'article', '_restrictions_create' ], + [ 'article', '_restrictions_create', 'restricted' ], + [ 'article', '_restrictions_edit' ], + [ 'article', '_restrictions_edit', 'restricted' ], + [ 'article', '_restrictions_move' ], + [ 'article', '_restrictions_move', 'restricted' ], + [ 'article', '_restrictions_upload' ], + [ 'article', '_restrictions_upload', 'restricted' ], + [ 'article', '_first_contributor' ], + [ 'article', '_recent_contributors' ], + [ 'moved_from', '_articleid' ], + [ 'moved_from', '_namespace' ], + [ 'moved_from', '_text' ], + [ 'moved_from', '_prefixedtext' ], + [ 'moved_from', '_restrictions_create' ], + [ 'moved_from', '_restrictions_create', 'restricted' ], + [ 'moved_from', '_restrictions_edit' ], + [ 'moved_from', '_restrictions_edit', 'restricted' ], + [ 'moved_from', '_restrictions_move' ], + [ 'moved_from', '_restrictions_move', 'restricted' ], + [ 'moved_from', '_restrictions_upload' ], + [ 'moved_from', '_restrictions_upload', 'restricted' ], + [ 'moved_from', '_first_contributor' ], + [ 'moved_from', '_recent_contributors' ], + [ 'moved_to', '_articleid' ], + [ 'moved_to', '_namespace' ], + [ 'moved_to', '_text' ], + [ 'moved_to', '_prefixedtext' ], + [ 'moved_to', '_restrictions_create' ], + [ 'moved_to', '_restrictions_create', 'restricted' ], + [ 'moved_to', '_restrictions_edit' ], + [ 'moved_to', '_restrictions_edit', 'restricted' ], + [ 'moved_to', '_restrictions_move' ], + [ 'moved_to', '_restrictions_move', 'restricted' ], + [ 'moved_to', '_restrictions_upload' ], + [ 'moved_to', '_restrictions_upload', 'restricted' ], + [ 'moved_to', '_first_contributor' ], + [ 'moved_to', '_recent_contributors' ], + ]; + } + + /** + * Check that our tag validation is working properly. Note that we only need one test + * for each called function. Consistency within ChangeTags functions should be + * assured by tests in core. The test for canAddTagsAccompanyingChange and canCreateTag + * are missing because they won't actually fail, never. Resolving T173917 would + * greatly improve the situation and could help writing better tests. + * + * @param string $tag The tag to validate + * @param string|null $error The expected error message. Null if validations should pass + * @covers AbuseFilter::isAllowedTag + * @dataProvider provideTags + */ + public function testIsAllowedTag( $tag, $error ) { + $status = AbuseFilter::isAllowedTag( $tag ); + + if ( !$status->isGood() ) { + $actualError = $status->getErrors(); + $actualError = $actualError[0]['message']; + } else { + $actualError = null; + if ( $error !== null ) { + $this->fail( "Tag validation returned a valid status instead of the expected '$error' error." ); + } + } + + $this->assertSame( + $error, + $actualError, + "Expected message '$error', got '$actualError' while validating the tag '$tag'." + ); + } + + /** + * Data provider for testIsAllowedTag + * @return array + */ + public function provideTags() { + return [ + [ 'a|b', 'tags-create-invalid-chars' ], + [ 'mw-undo', 'abusefilter-edit-bad-tags' ], + [ 'abusefilter-condition-limit', 'abusefilter-tag-reserved' ], + [ 'my_tag', null ], + ]; + } + + /** + * Check that version comparing works well + * + * @param array $firstVersion [ stdClass, array ] + * @param array $secondVersion [ stdClass, array ] + * @param array $expected The differences + * @covers AbuseFilter::compareVersions + * @dataProvider provideVersions + */ + public function testCompareVersions( $firstVersion, $secondVersion, $expected ) { + $differences = AbuseFilter::compareVersions( $firstVersion, $secondVersion ); + + $this->assertSame( + $expected, + $differences, + 'AbuseFilter::compareVersions did not output the expected result.' + ); + } + + /** + * Data provider for testCompareVersions + * @return array + */ + public function provideVersions() { + return [ + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'OtherComments', + 'af_pattern' => '/*Other pattern*/', + 'af_comments' => 'Other comments', + 'af_deleted' => 1, + 'af_enabled' => 0, + 'af_hidden' => 1, + 'af_global' => 1, + 'af_group' => 'flow' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + 'af_public_comments', + 'af_pattern', + 'af_comments', + 'af_deleted', + 'af_enabled', + 'af_hidden', + 'af_global', + 'af_group', + ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'degroup' => [ + 'action' => 'degroup', + 'parameters' => [] + ] + ] + ], + [ 'actions' ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'OtherComments', + 'af_pattern' => '/*Other pattern*/', + 'af_comments' => 'Other comments', + 'af_deleted' => 1, + 'af_enabled' => 0, + 'af_hidden' => 1, + 'af_global' => 1, + 'af_group' => 'flow' + ], + [ + 'blockautopromote' => [ + 'action' => 'blockautopromote', + 'parameters' => [] + ] + ] + ], + [ + 'af_public_comments', + 'af_pattern', + 'af_comments', + 'af_deleted', + 'af_enabled', + 'af_hidden', + 'af_global', + 'af_group', + 'actions' + ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning' + ] + ] + ] + ], + [ 'actions' ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning' + ] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ], + [ 'actions' ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning' + ] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-my-best-warning' + ] + ], + 'degroup' => [ + 'action' => 'degroup', + 'parameters' => [] + ] + ] + ], + [ 'actions' ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning' + ] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Other Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 1, + 'af_global' => 0, + 'af_group' => 'flow' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-my-best-warning' + ] + ] + ] + ], + [ + 'af_pattern', + 'af_hidden', + 'af_group', + 'actions' + ] + ], + [ + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'default' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-beautiful-warning' + ] + ] + ] + ], + [ + (object)[ + 'af_public_comments' => 'Comments', + 'af_pattern' => '/*Pattern*/', + 'af_comments' => 'Comments', + 'af_deleted' => 0, + 'af_enabled' => 1, + 'af_hidden' => 0, + 'af_global' => 0, + 'af_group' => 'flow' + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-my-best-warning' + ] + ] + ] + ], + [ + 'af_group', + 'actions' + ] + ], + ]; + } + + /** + * Check that row translating from abuse_filter_history to abuse_filter is working fine + * + * @param stdClass $row The row to translate + * @param array $expected The expected result + * @covers AbuseFilter::translateFromHistory + * @dataProvider provideHistoryRows + */ + public function testTranslateFromHistory( $row, $expected ) { + $actual = AbuseFilter::translateFromHistory( $row ); + + $this->assertEquals( + $expected, + $actual, + 'AbuseFilter::translateFromHistory produced a wrong output.' + ); + } + + /** + * Data provider for testTranslateFromHistory + * @return array + */ + public function provideHistoryRows() { + return [ + [ + (object)[ + 'afh_filter' => 1, + 'afh_user' => 0, + 'afh_user_text' => 'FilteredUser', + 'afh_timestamp' => '20180706142932', + 'afh_pattern' => '/*Pattern*/', + 'afh_comments' => 'Comments', + 'afh_flags' => 'enabled,hidden', + 'afh_public_comments' => 'Description', + 'afh_actions' => serialize( [ + 'degroup' => [], + 'disallow' => [] + ] ), + 'afh_deleted' => 0, + 'afh_changed_fields' => 'actions', + 'afh_group' => 'default' + ], + [ + (object)[ + 'af_pattern' => '/*Pattern*/', + 'af_user' => 0, + 'af_user_text' => 'FilteredUser', + 'af_timestamp' => '20180706142932', + 'af_comments' => 'Comments', + 'af_public_comments' => 'Description', + 'af_deleted' => 0, + 'af_id' => 1, + 'af_group' => 'default', + 'af_hidden' => 1, + 'af_enabled' => 1 + ], + [ + 'degroup' => [ + 'action' => 'degroup', + 'parameters' => [] + ], + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ] + ], + [ + (object)[ + 'afh_filter' => 5, + 'afh_user' => 0, + 'afh_user_text' => 'FilteredUser', + 'afh_timestamp' => '20180706145516', + 'afh_pattern' => '1 === 1', + 'afh_comments' => '', + 'afh_flags' => '', + 'afh_public_comments' => 'Our best filter', + 'afh_actions' => serialize( [ + 'warn' => [ + 'abusefilter-warning', + '' + ], + 'disallow' => [], + ] ), + 'afh_deleted' => 0, + 'afh_changed_fields' => 'af_pattern,af_comments,af_enabled,actions', + 'afh_group' => 'flow' + ], + [ + (object)[ + 'af_pattern' => '1 === 1', + 'af_user' => 0, + 'af_user_text' => 'FilteredUser', + 'af_timestamp' => '20180706145516', + 'af_comments' => '', + 'af_public_comments' => 'Our best filter', + 'af_deleted' => 0, + 'af_id' => 5, + 'af_group' => 'flow', + 'af_hidden' => 0, + 'af_enabled' => 0 + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning', + '' + ] + ], + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ] + ] + ] + ], + [ + (object)[ + 'afh_filter' => 7, + 'afh_user' => 1, + 'afh_user_text' => 'AnotherUser', + 'afh_timestamp' => '20160511185604', + 'afh_pattern' => 'added_lines irlike "lol" & summary == "ggwp"', + 'afh_comments' => 'Show vandals no mercy, for you shall receive none.', + 'afh_flags' => 'enabled,hidden', + 'afh_public_comments' => 'Whatever', + 'afh_actions' => serialize( [ + 'warn' => [ + 'abusefilter-warning', + '' + ], + 'disallow' => [], + 'block' => [ + 'blocktalk', + '8 hours', + 'infinity' + ] + ] ), + 'afh_deleted' => 0, + 'afh_changed_fields' => 'af_pattern,af_comments,af_enabled,af_public_comments,actions', + 'afh_group' => 'default' + ], + [ + (object)[ + 'af_pattern' => 'added_lines irlike "lol" & summary == "ggwp"', + 'af_user' => 1, + 'af_user_text' => 'AnotherUser', + 'af_timestamp' => '20160511185604', + 'af_comments' => 'Show vandals no mercy, for you shall receive none.', + 'af_public_comments' => 'Whatever', + 'af_deleted' => 0, + 'af_id' => 7, + 'af_group' => 'default', + 'af_hidden' => 1, + 'af_enabled' => 1 + ], + [ + 'warn' => [ + 'action' => 'warn', + 'parameters' => [ + 'abusefilter-warning', + '' + ] + ], + 'disallow' => [ + 'action' => 'disallow', + 'parameters' => [] + ], + 'block' => [ + 'action' => 'block', + 'parameters' => [ + 'blocktalk', + '8 hours', + 'infinity' + ] + ] + ] + ] + ], + [ + (object)[ + 'afh_filter' => 131, + 'afh_user' => 15, + 'afh_user_text' => 'YetAnotherUser', + 'afh_timestamp' => '20180511185604', + 'afh_pattern' => 'user_name == "Thatguy"', + 'afh_comments' => '', + 'afh_flags' => 'hidden,deleted', + 'afh_public_comments' => 'No comment.', + 'afh_actions' => serialize( [ + 'throttle' => [ + '131', + '3,60', + 'user' + ], + 'tag' => [ + 'mytag', + 'yourtag' + ] + ] ), + 'afh_deleted' => 1, + 'afh_changed_fields' => 'af_pattern', + 'afh_group' => 'default' + ], + [ + (object)[ + 'af_pattern' => 'user_name == "Thatguy"', + 'af_user' => 15, + 'af_user_text' => 'YetAnotherUser', + 'af_timestamp' => '20180511185604', + 'af_comments' => '', + 'af_public_comments' => 'No comment.', + 'af_deleted' => 1, + 'af_id' => 131, + 'af_group' => 'default', + 'af_hidden' => 1, + 'af_enabled' => 0 + ], + [ + 'throttle' => [ + 'action' => 'throttle', + 'parameters' => [ + '131', + '3,60', + 'user' + ] + ], + 'tag' => [ + 'action' => 'tag', + 'parameters' => [ + 'mytag', + 'yourtag' + ] + ] + ] + ] + ] + ]; + } + + /** + * Given the name of a variable, naturally sets it to a determined amount + * + * @param string $old The old wikitext of the page + * @param string $new The new wikitext of the page + * @return array + */ + private static function computeExpectedEditVariable( $old, $new ) { + global $wgParser; + $popts = ParserOptions::newFromUser( self::$mUser ); + // Order matters here. Some variables rely on other ones. + $variables = [ + 'new_html', + 'new_pst', + 'new_text', + 'edit_diff', + 'edit_diff_pst', + 'new_size', + 'old_size', + 'edit_delta', + 'added_lines', + 'removed_lines', + 'added_lines_pst', + 'all_links', + 'old_links', + 'added_links', + 'removed_links' + ]; + + // Set required variables + self::$mVariables->setVar( 'old_wikitext', $old ); + self::$mVariables->setVar( 'new_wikitext', $new ); + self::$mVariables->setVar( 'summary', 'Testing page for AbuseFilter' ); + + $computedVariables = []; + foreach ( $variables as $var ) { + $success = true; + // Reset text variables since some operations are changing them. + $oldText = $old; + $newText = $new; + switch ( $var ) { + case 'edit_diff_pst': + $newText = self::$mVariables->getVar( 'new_pst' )->toString(); + // Intentional fall-through + case 'edit_diff': + $diffs = new Diff( explode( "\n", $oldText ), explode( "\n", $newText ) ); + $format = new UnifiedDiffFormatter(); + $result = $format->format( $diffs ); + break; + case 'new_size': + $result = strlen( $newText ); + break; + case 'old_size': + $result = strlen( $oldText ); + break; + case 'edit_delta': + $result = strlen( $newText ) - strlen( $oldText ); + break; + case 'added_lines_pst': + case 'added_lines': + case 'removed_lines': + $diffVariable = $var === 'added_lines_pst' ? 'edit_diff_pst' : 'edit_diff'; + $diff = self::$mVariables->getVar( $diffVariable )->toString(); + $line_prefix = $var === 'removed_lines' ? '-' : '+'; + $diff_lines = explode( "\n", $diff ); + $interest_lines = []; + foreach ( $diff_lines as $line ) { + if ( substr( $line, 0, 1 ) === $line_prefix ) { + $interest_lines[] = substr( $line, strlen( $line_prefix ) ); + } + } + $result = $interest_lines; + break; + case 'new_text': + $newHtml = self::$mVariables->getVar( 'new_html' )->toString(); + $result = StringUtils::delimiterReplace( '<', '>', '', $newHtml ); + break; + case 'new_pst': + case 'new_html': + $article = self::$mPage; + $content = ContentHandler::makeContent( $newText, $article->getTitle() ); + $editInfo = $article->prepareContentForEdit( $content ); + + if ( $var === 'new_pst' ) { + $result = $editInfo->pstContent->serialize( $editInfo->format ); + } else { + $result = $editInfo->output->getText(); + } + break; + case 'all_links': + $article = self::$mPage; + $content = ContentHandler::makeContent( $newText, $article->getTitle() ); + $editInfo = $article->prepareContentForEdit( $content ); + $result = array_keys( $editInfo->output->getExternalLinks() ); + break; + case 'old_links': + $article = self::$mPage; + $popts->setTidy( true ); + $edit = $wgParser->parse( $oldText, $article->getTitle(), $popts ); + $result = array_keys( $edit->getExternalLinks() ); + break; + case 'added_links': + case 'removed_links': + $oldLinks = self::$mVariables->getVar( 'old_links' )->toString(); + $newLinks = self::$mVariables->getVar( 'all_links' )->toString(); + $oldLinks = explode( "\n", $oldLinks ); + $newLinks = explode( "\n", $newLinks ); + + if ( $var === 'added_links' ) { + $result = array_diff( $newLinks, $oldLinks ); + } else { + $result = array_diff( $oldLinks, $newLinks ); + } + break; + default: + $success = false; + $result = null; + } + $computedVariables[$var] = [ $result, $success ]; + self::$mVariables->setVar( $var, $result ); + } + return $computedVariables; + } + + /** + * Check that the generated variables for edits are correct + * + * @param string $oldText The old wikitext of the page + * @param string $newText The new wikitext of the page + * @covers AbuseFilter::getEditVars + * @dataProvider provideEditVars + */ + public function testGetEditVars( $oldText, $newText ) { + global $wgLang; + self::$mTitle = Title::makeTitle( 0, 'AbuseFilter test' ); + self::$mPage = WikiPage::factory( self::$mTitle ); + + self::$mPage->doEditContent( + new WikitextContent( $oldText ), + 'Creating the test page', + EDIT_NEW, + false, + self::$mUser + ); + self::$mPage->doEditContent( + new WikitextContent( $newText ), + 'Testing page for AbuseFilter', + EDIT_UPDATE, + false, + self::$mUser + ); + + $computeResult = self::computeExpectedEditVariable( $oldText, $newText ); + + $computedVariables = []; + foreach ( $computeResult as $varName => $computed ) { + if ( !$computed[1] ) { + $this->fail( "Given unknown edit variable $varName." ); + } + $computedVariables[$varName] = $computed[0]; + } + + self::$mVariables->addHolders( AbuseFilter::getEditVars( self::$mTitle, self::$mPage ) ); + + $actualVariables = []; + foreach ( self::$mVariables->mVars as $varName => $_ ) { + $actualVariables[$varName] = self::$mVariables->getVar( $varName )->toNative(); + } + + $differences = []; + foreach ( $computedVariables as $var => $computed ) { + if ( !isset( $actualVariables[$var] ) ) { + $this->fail( "AbuseFilter::getEditVars didn't set the $var variable." ); + } elseif ( $computed !== $actualVariables[$var] ) { + $differences[] = $var; + } + } + + $this->assertCount( + 0, + $differences, + 'The following AbuseFilter variables are computed wrongly: ' . $wgLang->commaList( $differences ) + ); + } + + /** + * Data provider for testGetEditVars + * @return array + */ + public function provideEditVars() { + return [ + [ + '[https://www.mediawiki.it/wiki/Extension:AbuseFilter AbuseFilter] test page', + 'Adding something to compute edit variables. Here are some diacritics to make sure ' . + "the test behaves well with unicode: Là giù cascherò io altresì.\n名探偵コナン.\n" . + "[[Help:Pre Save Transform|]] should make the difference as well.\n" . + 'Instead, [https://www.mediawiki.it this] is an external link.' + ], + [ + 'Adding something to compute edit variables. Here are some diacritics to make sure ' . + "the test behaves well with unicode: Là giù cascherò io altresì.\n名探偵コナン.\n" . + "[[Help:Pre Save Transform|]] should make the difference as well.\n" . + 'Instead, [https://www.mediawiki.it this] is an external link.', + '[https://www.mediawiki.it/wiki/Extension:AbuseFilter AbuseFilter] test page' + ], + [ + "A '''foo''' is not a ''bar''.", + "Actually, according to [http://en.wikipedia.org ''Wikipedia''], a '''''foo''''' " . + 'is more or less the same as a bar, except that a foo is ' . + 'usually provided together with a [[cellar door|]] to make it workYes, really.' + ], + [ + 'This edit will be pretty smll', + 'This edit will be pretty small' + ] + ]; + } +}