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' ] ]; } }