Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F2752828
AbuseFilterConsequencesTest.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
47 KB
Referenced Files
None
Subscribers
None
AbuseFilterConsequencesTest.php
View Options
<?php
use
MediaWiki\Block\BlockUser
;
use
MediaWiki\Content\Content
;
use
MediaWiki\Content\ContentHandler
;
use
MediaWiki\Content\JsonContent
;
use
MediaWiki\Content\WikitextContent
;
use
MediaWiki\Context\RequestContext
;
use
MediaWiki\Extension\AbuseFilter\AbuseFilterServices
;
use
MediaWiki\Extension\AbuseFilter\Filter\Flags
;
use
MediaWiki\Extension\AbuseFilter\FilterRunnerFactory
;
use
MediaWiki\Extension\AbuseFilter\Hooks\Handlers\FilteredActionsHandler
;
use
MediaWiki\Extension\AbuseFilter\Parser\AFPData
;
use
MediaWiki\Json\FormatJson
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Status\Status
;
use
MediaWiki\Storage\PageEditStash
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\User
;
use
MediaWiki\WikiMap\WikiMap
;
use
Psr\Log\LoggerInterface
;
use
Wikimedia\Rdbms\IDBAccessObject
;
use
Wikimedia\Rdbms\SelectQueryBuilder
;
use
Wikimedia\Timestamp\ConvertibleTimestamp
;
/**
* Complete tests where filters are saved, actions are executed and the right
* consequences are expected to be taken
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*
* @license GPL-2.0-or-later
*/
/**
* @group Test
* @group AbuseFilter
* @group AbuseFilterConsequences
* @group Database
* @group Large
*
* @covers \MediaWiki\Extension\AbuseFilter\FilterRunner
* @covers \MediaWiki\Extension\AbuseFilter\Hooks\Handlers\FilteredActionsHandler
* @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\RunVariableGenerator
* @covers \MediaWiki\Extension\AbuseFilter\AbuseFilterPreAuthenticationProvider
* @covers \MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagger
* @covers \MediaWiki\Extension\AbuseFilter\BlockAutopromoteStore
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Block
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\BlockAutopromote
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\BlockingConsequence
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Consequence
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Degroup
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Disallow
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\RangeBlock
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Tag
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Throttle
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Warn
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesLookup
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutor
*/
class
AbuseFilterConsequencesTest
extends
MediaWikiIntegrationTestCase
{
use
AbuseFilterCreateAccountTestTrait
;
use
AbuseFilterUploadTestTrait
;
/**
* @var User The user performing actions
*/
private
$user
;
// phpcs:disable Generic.Files.LineLength
/** @var array Filters that may be created, their key is the ID. */
private
const
FILTERS
=
[
1
=>
[
'af_pattern'
=>
'added_lines irlike "foo"'
,
'af_public_comments'
=>
'Mock filter for edit'
,
'actions'
=>
[
'warn'
=>
[
'abusefilter-my-warning'
],
'tag'
=>
[
'filtertag'
]
]
],
2
=>
[
'af_pattern'
=>
'action = "move" & moved_to_title contains "test" & moved_to_title === moved_to_text'
,
'af_public_comments'
=>
'Mock filter for move'
,
'af_hidden'
=>
Flags
::
FILTER_HIDDEN
,
'actions'
=>
[
'disallow'
=>
[],
'block'
=>
[
'blocktalk'
,
'8 hours'
,
'infinity'
]
]
],
3
=>
[
'af_pattern'
=>
'action = "delete" & "test" in lcase(page_prefixedtitle) & page_prefixedtitle === article_prefixedtext'
,
'af_public_comments'
=>
'Mock filter for delete'
,
'af_global'
=>
1
,
'actions'
=>
[
'degroup'
=>
[]
]
],
5
=>
[
// XXX Need to hardcode UTSysop here because this is a constant
'af_pattern'
=>
'user_name == "UTSysop"'
,
'af_public_comments'
=>
'Mock filter'
,
'actions'
=>
[
'tag'
=>
[
'firstTag'
,
'secondTag'
]
]
],
6
=>
[
'af_pattern'
=>
'edit_delta === 7'
,
'af_public_comments'
=>
'Mock filter with edit_delta'
,
'af_hidden'
=>
Flags
::
FILTER_HIDDEN
,
'af_global'
=>
1
,
'actions'
=>
[
'disallow'
=>
[
'abusefilter-disallowed-really'
]
]
],
7
=>
[
'af_pattern'
=>
'timestamp === string(timestamp)'
,
'af_public_comments'
=>
'Mock filter with timestamp'
,
'actions'
=>
[
'degroup'
=>
[]
]
],
8
=>
[
'af_pattern'
=>
'added_lines_pst irlike "
\\
[
\\
[Link
\\
|Link
\\
]
\\
]"'
,
'af_public_comments'
=>
'Mock filter with pst'
,
'actions'
=>
[
'disallow'
=>
[],
'block'
=>
[
'NoTalkBlockSet'
,
'4 hours'
,
'4 hours'
]
]
],
9
=>
[
'af_pattern'
=>
'new_size > old_size'
,
'af_public_comments'
=>
'Mock filter with size'
,
'af_hidden'
=>
Flags
::
FILTER_HIDDEN
,
'actions'
=>
[
'disallow'
=>
[],
'block'
=>
[
'blocktalk'
,
'3 hours'
,
'3 hours'
]
]
],
10
=>
[
'af_pattern'
=>
'1 == 1'
,
'af_public_comments'
=>
'Mock throttled filter'
,
'af_hidden'
=>
Flags
::
FILTER_HIDDEN
,
'af_throttled'
=>
1
,
'actions'
=>
[
'tag'
=>
[
'testTag'
],
'block'
=>
[
'blocktalk'
,
'infinity'
,
'infinity'
]
]
],
11
=>
[
'af_pattern'
=>
'1 == 1'
,
'af_public_comments'
=>
'Catch-all filter which throttles'
,
'actions'
=>
[
'throttle'
=>
[
11
,
'1,3600'
,
'site'
],
'disallow'
=>
[]
]
],
12
=>
[
'af_pattern'
=>
'page_title == user_name & user_name === page_title'
,
'af_public_comments'
=>
'Mock filter for userpage'
,
'actions'
=>
[
'disallow'
=>
[],
'block'
=>
[
'blocktalk'
,
'8 hours'
,
'1 day'
],
'degroup'
=>
[]
]
],
13
=>
[
'af_pattern'
=>
'2 == 2'
,
'af_public_comments'
=>
'Another throttled mock filter'
,
'af_throttled'
=>
1
,
'actions'
=>
[
'block'
=>
[
'blocktalk'
,
'8 hours'
,
'1 day'
],
'degroup'
=>
[]
]
],
14
=>
[
'af_pattern'
=>
'5/int(article_text) == 3'
,
'af_public_comments'
=>
'Filter with a possible division by zero'
,
'actions'
=>
[
'disallow'
=>
[]
]
],
15
=>
[
'af_pattern'
=>
'action contains "createaccount"'
,
'af_public_comments'
=>
'Catch-all for account creations'
,
'af_hidden'
=>
Flags
::
FILTER_HIDDEN
,
'actions'
=>
[
'disallow'
=>
[]
]
],
18
=>
[
'af_pattern'
=>
'1 == 1'
,
'af_public_comments'
=>
'Global filter'
,
'af_global'
=>
1
,
'actions'
=>
[
'warn'
=>
[
'abusefilter-warning'
],
'disallow'
=>
[]
]
],
19
=>
[
// XXX Need to hardcode UTSysop here because this is a constant
'af_pattern'
=>
'user_name === "UTSysop"'
,
'af_public_comments'
=>
'Another global filter'
,
'af_global'
=>
1
,
'actions'
=>
[
'tag'
=>
[
'globalTag'
]
]
],
20
=>
[
'af_pattern'
=>
'page_title === "Cellar door"'
,
'af_public_comments'
=>
'Yet another global filter'
,
'af_global'
=>
1
,
'actions'
=>
[
'disallow'
=>
[],
]
],
21
=>
[
'af_pattern'
=>
'1==1'
,
'af_public_comments'
=>
'Dangerous filter'
,
'actions'
=>
[
'blockautopromote'
=>
[]
]
],
22
=>
[
'af_pattern'
=>
'action contains "upload" & "Block me" in added_lines & file_size > 0 & '
.
'file_mime contains "/" & file_width + file_height > 0 & summary !== ""'
,
'af_public_comments'
=>
'Filter for uploads'
,
'actions'
=>
[
'warn'
=>
[
'abusefilter-random-upload'
],
'blockautopromote'
=>
[]
]
],
23
=>
[
'af_pattern'
=>
'1 === 1'
,
'af_public_comments'
=>
'Catch-all for warning + disallow'
,
'actions'
=>
[
'warn'
=>
[
'abusefilter-my-warning'
],
'disallow'
=>
[
'abusefilter-my-disallow'
]
]
],
24
=>
[
// We used this for testRevIdSet()
'af_pattern'
=>
'1 == 1'
,
'af_public_comments'
=>
'Always matches, no actions'
,
'actions'
=>
[]
],
];
// phpcs:enable Generic.Files.LineLength
/**
* @inheritDoc
*/
protected
function
setUp
():
void
{
parent
::
setUp
();
// Ensure that our user is a sysop
$this
->
user
=
$this
->
getTestSysop
()->
getUser
();
// Pin time to avoid time shifts on relative block duration
ConvertibleTimestamp
::
setFakeTime
(
time
()
);
// Make sure that the config we're using is the one we're expecting
$this
->
overrideConfigValues
(
[
// Exclude noisy creation log
MainConfigNames
::
PageCreationLog
=>
false
,
'AbuseFilterActions'
=>
[
'throttle'
=>
true
,
'warn'
=>
true
,
'disallow'
=>
true
,
'blockautopromote'
=>
true
,
'block'
=>
true
,
'rangeblock'
=>
true
,
'degroup'
=>
true
,
'tag'
=>
true
],
]
);
}
/**
* @inheritDoc
*/
protected
function
tearDown
():
void
{
$this
->
clearUploads
();
parent
::
tearDown
();
}
/**
* Creates new filters with the given ids, referred to self::FILTERS
*
* @param int[] $ids IDs of the filters to create
*/
private
function
createFilters
(
$ids
)
{
global
$wgAbuseFilterActions
;
$dbw
=
$this
->
getDb
();
$defaultRowSection
=
[
'af_actor'
=>
1
,
'af_timestamp'
=>
$dbw
->
timestamp
(),
'af_group'
=>
'default'
,
'af_comments'
=>
''
,
'af_hit_count'
=>
0
,
'af_enabled'
=>
1
,
'af_hidden'
=>
Flags
::
FILTER_PUBLIC
,
'af_throttled'
=>
0
,
'af_deleted'
=>
0
,
'af_global'
=>
0
];
$filterRows
=
[];
$actionsRows
=
[];
foreach
(
$ids
as
$id
)
{
$filter
=
self
::
FILTERS
[
$id
]
+
$defaultRowSection
;
$actions
=
$filter
[
'actions'
];
unset
(
$filter
[
'actions'
]
);
$filter
[
'af_actions'
]
=
implode
(
','
,
array_keys
(
$actions
)
);
$filter
[
'af_id'
]
=
$id
;
ksort
(
$filter
);
$filterRows
[]
=
$filter
;
foreach
(
array_filter
(
$wgAbuseFilterActions
)
as
$action
=>
$_
)
{
if
(
isset
(
$actions
[
$action
]
)
)
{
$parameters
=
$actions
[
$action
];
$thisRow
=
[
'afa_filter'
=>
$id
,
'afa_consequence'
=>
$action
,
'afa_parameters'
=>
implode
(
"
\n
"
,
$parameters
)
];
$actionsRows
[]
=
$thisRow
;
}
}
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'abuse_filter'
)
->
rows
(
$filterRows
)
->
caller
(
__METHOD__
)
->
execute
();
if
(
$actionsRows
)
{
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'abuse_filter_action'
)
->
rows
(
$actionsRows
)
->
caller
(
__METHOD__
)
->
execute
();
}
}
/**
* @param Title $title Title of the page to edit
* @param Content $content The new content of the page
* @param string $summary The summary of the edit
* @return string The status of the operation, as returned by the API.
*/
private
function
stashEdit
(
Title
$title
,
Content
$content
,
string
$summary
)
{
$services
=
$this
->
getServiceContainer
();
$pageUpdater
=
$services
->
getWikiPageFactory
()->
newFromTitle
(
$title
)->
newPageUpdater
(
$this
->
user
);
return
$services
->
getPageEditStash
()->
parseAndCache
(
$pageUpdater
,
$content
,
$this
->
user
,
$summary
);
}
/**
* @param Title $title Title of the page to edit
* @param Content|string $oldText Old content of the page
* @param Content|string $newText The new content of the page
* @param string $summary The summary of the edit
* @param bool|null $fromStash Whether to stash the edit. Null means no stashing, false means
* stash the edit but don't reuse it for saving, true means stash and reuse.
* @return Status
*/
private
function
doEdit
(
Title
$title
,
$oldText
,
$newText
,
$summary
,
$fromStash
=
null
)
{
$services
=
$this
->
getServiceContainer
();
$page
=
$services
->
getWikiPageFactory
()->
newFromTitle
(
$title
);
if
(
!
$page
->
exists
()
)
{
$status
=
$this
->
editPage
(
$page
,
$oldText
,
__METHOD__
.
' page creation'
);
if
(
!
$status
->
isGood
()
)
{
throw
new
RuntimeException
(
"Could not create test page. $status"
);
}
$page
->
clear
();
$title
->
resetArticleID
(
-
1
);
}
if
(
$fromStash
!==
null
)
{
// If we want to save from stash, submit the same text
$stashText
=
$newText
;
if
(
$fromStash
===
false
)
{
// Otherwise, stash some random text which won't match the actual edit
$stashText
=
md5
(
uniqid
(
rand
(),
true
)
);
}
$stashContent
=
$stashText
instanceof
Content
?
$stashText
:
new
WikitextContent
(
$stashText
);
$stashResult
=
$this
->
stashEdit
(
$title
,
$stashContent
,
$summary
);
if
(
$stashResult
!==
PageEditStash
::
ERROR_NONE
)
{
throw
new
RuntimeException
(
"The edit cannot be stashed, got the following error: $stashResult"
);
}
}
$content
=
$newText
instanceof
Content
?
$newText
:
ContentHandler
::
makeContent
(
$newText
,
$title
);
$status
=
Status
::
newGood
();
$context
=
RequestContext
::
getMain
();
$context
->
setUser
(
$this
->
user
);
$context
->
setWikiPage
(
$page
);
$hooksHandler
=
new
FilteredActionsHandler
(
$services
->
getStatsdDataFactory
(),
AbuseFilterServices
::
getFilterRunnerFactory
(),
AbuseFilterServices
::
getVariableGeneratorFactory
(),
AbuseFilterServices
::
getEditRevUpdater
(),
AbuseFilterServices
::
getBlockedDomainFilter
(),
$services
->
getPermissionManager
()
);
$hooksHandler
->
onEditFilterMergedContent
(
$context
,
$content
,
$status
,
$summary
,
$this
->
user
,
false
);
if
(
$status
->
isGood
()
)
{
// Edit the page in case the test will expect for it to exist
$this
->
editPage
(
$page
,
$newText
,
$summary
,
NS_MAIN
,
$this
->
user
);
$page
->
clear
();
$title
->
resetArticleID
(
-
1
);
}
return
$status
;
}
/**
* Executes an action to filter
*
* @param array $params Parameters of the action
* @return Status
*/
private
function
doAction
(
$params
)
{
$target
=
Title
::
newFromTextThrow
(
$params
[
'target'
]
);
switch
(
$params
[
'action'
]
)
{
case
'edit'
:
$status
=
$this
->
doEdit
(
$target
,
$params
[
'oldText'
],
$params
[
'newText'
],
$params
[
'summary'
]
);
break
;
case
'stashedit'
:
$stashStatus
=
$params
[
'stashType'
]
===
'hit'
;
$status
=
$this
->
doEdit
(
$target
,
$params
[
'oldText'
],
$params
[
'newText'
],
$params
[
'summary'
],
$stashStatus
);
break
;
case
'move'
:
// Ensure that the page exists
$this
->
getExistingTestPage
(
$target
);
$newTitle
=
isset
(
$params
[
'newTitle'
]
)
?
Title
::
newFromTextThrow
(
$params
[
'newTitle'
]
)
:
$this
->
getNonExistingTestPage
()->
getTitle
();
$status
=
$this
->
getServiceContainer
()
->
getMovePageFactory
()
->
newMovePage
(
$target
,
$newTitle
)
->
move
(
$this
->
user
,
'AbuseFilter move test'
,
false
);
break
;
case
'delete'
:
$page
=
$this
->
getExistingTestPage
(
$target
);
$status
=
$page
->
doDeleteArticleReal
(
'Testing deletion in AbuseFilter'
,
$this
->
user
);
break
;
case
'createaccount'
:
$status
=
$this
->
createAccount
(
$params
[
'username'
]
);
break
;
case
'upload'
:
[
$status
,
$this
->
clearPath
]
=
$this
->
doUpload
(
$this
->
user
,
$params
[
'target'
],
$params
[
'newText'
]
??
'AbuseFilter test upload'
,
$params
[
'summary'
]
??
'Test'
);
break
;
default
:
throw
new
UnexpectedValueException
(
'Unrecognized action '
.
$params
[
'action'
]
);
}
// Clear cache since we'll need to retrieve some fresh data about the user
// like blocks and groups later when checking expected values
$this
->
user
->
clearInstanceCache
();
return
$status
;
}
/**
* @param array[] $actionsParams Arrays of parameters for every action
* @return Status[]
*/
private
function
doActions
(
$actionsParams
)
{
$ret
=
[];
foreach
(
$actionsParams
as
$params
)
{
$ret
[]
=
$this
->
doAction
(
$params
);
}
return
$ret
;
}
/**
* Helper function to retrieve change tags applied to an edit or log entry
*
* @param array $actionParams As given by the data provider
* @return string[] The applied tags
*/
private
function
getActionTags
(
$actionParams
)
{
$dbw
=
$this
->
getDb
();
$title
=
Title
::
newFromTextThrow
(
$actionParams
[
'target'
]
);
$store
=
$this
->
getServiceContainer
()->
getChangeTagsStore
();
if
(
$actionParams
[
'action'
]
===
'edit'
||
$actionParams
[
'action'
]
===
'stashedit'
)
{
return
$store
->
getTags
(
$dbw
,
null
,
$title
->
getLatestRevID
()
);
}
$logType
=
$actionParams
[
'action'
]
===
'createaccount'
?
'newusers'
:
$actionParams
[
'action'
];
$logAction
=
$logType
===
'newusers'
?
'create2'
:
$logType
;
$id
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'log_id'
)
->
from
(
'logging'
)
->
where
(
[
'log_namespace'
=>
$title
->
getNamespace
(),
'log_title'
=>
$title
->
getDBkey
(),
'log_type'
=>
$logType
,
'log_action'
=>
$logAction
]
)
->
orderBy
(
'log_id'
,
SelectQueryBuilder
::
SORT_DESC
)
->
caller
(
__METHOD__
)
->
fetchField
();
if
(
!
$id
)
{
$this
->
fail
(
'Could not find the action in the logging table.'
);
}
return
$store
->
getTags
(
$dbw
,
null
,
null
,
(
int
)
$id
);
}
/**
* Checks that consequences are effectively taken and builds an array of expected and actual
* consequences which can be compared.
*
* @param Status $result As returned by self::doAction
* @param array $actionParams As it's given by data providers
* @param array $expectedConsequences
* @return array [ expected consequences, actual consequences ]
*/
private
function
checkConsequences
(
$result
,
$actionParams
,
$expectedConsequences
)
{
$expectedErrors
=
[];
$testErrorMessage
=
false
;
foreach
(
$expectedConsequences
as
$consequence
=>
$ids
)
{
foreach
(
$ids
as
$id
)
{
$params
=
self
::
FILTERS
[
$id
][
'actions'
][
$consequence
];
switch
(
$consequence
)
{
case
'warn'
:
// Aborts the hook with the warning message as error.
$expectedErrors
[]
=
$params
[
0
]
??
'abusefilter-warning'
;
break
;
case
'disallow'
:
// Aborts the hook with the disallow message error.
$expectedErrors
[]
=
$params
[
0
]
??
'abusefilter-disallowed'
;
break
;
case
'block'
:
// Aborts the hook with 'abusefilter-blocked-display' error. Should block
// the user with expected duration and options.
$userBlock
=
$this
->
user
->
getBlock
(
false
);
if
(
!
$userBlock
)
{
$testErrorMessage
=
"User isn't blocked."
;
break
;
}
$shouldPreventTalkEdit
=
$params
[
0
]
===
'blocktalk'
;
$edittalkCheck
=
$userBlock
->
appliesToUsertalk
(
$this
->
user
->
getTalkPage
()
)
===
$shouldPreventTalkEdit
;
if
(
!
$edittalkCheck
)
{
$testErrorMessage
=
'The expected block option "edittalk" options does not '
.
'match the actual one.'
;
break
;
}
$expectedExpiry
=
BlockUser
::
parseExpiryInput
(
$params
[
2
]
);
$actualExpiry
=
$userBlock
->
getExpiry
();
if
(
!
wfIsInfinity
(
$actualExpiry
)
)
{
$actualExpiry
=
wfTimestamp
(
TS_MW
,
$actualExpiry
);
}
if
(
$expectedExpiry
===
false
||
$expectedExpiry
!==
$actualExpiry
)
{
$testErrorMessage
=
"The expected block expiry ($expectedExpiry) does not "
.
"match the actual one ($actualExpiry)."
;
break
;
}
$expectedErrors
[]
=
'abusefilter-blocked-display'
;
break
;
case
'degroup'
:
// Aborts the hook with 'abusefilter-degrouped' error and degroups the user.
$expectedErrors
[]
=
'abusefilter-degrouped'
;
$ugm
=
MediaWikiServices
::
getInstance
()->
getUserGroupManager
();
$groupCheck
=
!
in_array
(
'sysop'
,
$ugm
->
getUserEffectiveGroups
(
$this
->
user
)
);
if
(
!
$groupCheck
)
{
$testErrorMessage
=
'The user was not degrouped.'
;
}
break
;
case
'tag'
:
// Only adds tags, to be retrieved in change_tag table.
$appliedTags
=
$this
->
getActionTags
(
$actionParams
);
$tagCheck
=
count
(
array_diff
(
$params
,
$appliedTags
)
)
===
0
;
if
(
!
$tagCheck
)
{
$expectedTags
=
implode
(
', '
,
$params
);
$actualTags
=
implode
(
', '
,
$appliedTags
);
$testErrorMessage
=
"Expected the action to have the following tags: $expectedTags. "
.
"Got the following instead: $actualTags."
;
}
break
;
case
'throttle'
:
throw
new
UnexpectedValueException
(
'Use self::testThrottleConsequence to test throttling'
);
case
'blockautopromote'
:
// Aborts the hook with 'abusefilter-autopromote-blocked' error and prevent promotion.
$expectedErrors
[]
=
'abusefilter-autopromote-blocked'
;
$value
=
AbuseFilterServices
::
getBlockAutopromoteStore
()
->
getAutoPromoteBlockStatus
(
$this
->
user
);
if
(
!
$value
)
{
$testErrorMessage
=
"The key for blocking autopromotion wasn't set."
;
}
break
;
default
:
throw
new
UnexpectedValueException
(
"Consequence not recognized: $consequence."
);
}
if
(
$testErrorMessage
)
{
$this
->
fail
(
$testErrorMessage
);
}
}
}
$actual
=
[];
foreach
(
$result
->
getMessages
()
as
$msg
)
{
if
(
strpos
(
$msg
->
getKey
(),
'abusefilter'
)
!==
false
)
{
$actual
[]
=
$msg
->
getKey
();
}
}
sort
(
$expectedErrors
);
sort
(
$actual
);
return
[
$expectedErrors
,
$actual
];
}
/**
* @param array $actionParams
* @param array $expectedConsequences
*/
private
function
assertAbuseLog
(
$actionParams
,
$expectedConsequences
):
void
{
$consequencesByFilter
=
[];
foreach
(
$expectedConsequences
as
$consequence
=>
$ids
)
{
foreach
(
$ids
as
$id
)
{
$consequencesByFilter
[
$id
][]
=
$consequence
;
}
}
foreach
(
$consequencesByFilter
as
$filter
=>
$consequences
)
{
$action
=
$actionParams
[
'action'
];
if
(
$action
===
'stashedit'
)
{
$action
=
'edit'
;
}
$title
=
Title
::
newFromTextThrow
(
$actionParams
[
'target'
]
);
$row
=
$this
->
getDb
()->
newSelectQueryBuilder
()
->
select
(
'*'
)
->
from
(
'abuse_filter_log'
)
->
where
(
[
'afl_filter_id'
=>
$filter
,
'afl_global'
=>
0
,
]
)
->
orderBy
(
'afl_id'
,
SelectQueryBuilder
::
SORT_DESC
)
->
caller
(
__METHOD__
)
->
fetchRow
();
$this
->
assertNotFalse
(
$row
);
$dumpStr
=
FormatJson
::
encode
(
$row
);
$this
->
assertSame
(
$action
,
$row
->
afl_action
,
$dumpStr
);
$this
->
assertNull
(
$row
->
afl_wiki
,
$dumpStr
);
if
(
$action
===
'createaccount'
)
{
$this
->
assertSame
(
$actionParams
[
'username'
],
$row
->
afl_user_text
,
$dumpStr
);
$this
->
assertSame
(
-
1
,
(
int
)
$row
->
afl_namespace
,
$dumpStr
);
}
else
{
$this
->
assertSame
(
$this
->
user
->
getName
(),
$row
->
afl_user_text
,
$dumpStr
);
$this
->
assertSame
(
$title
->
getNamespace
(),
(
int
)
$row
->
afl_namespace
,
$dumpStr
);
$this
->
assertSame
(
$title
->
getDBkey
(),
$row
->
afl_title
,
$dumpStr
);
}
if
(
in_array
(
'disallow'
,
$consequences
)
&&
array_intersect
(
$consequences
,
[
'block'
,
'blockautopromote'
,
'degroup'
]
)
)
{
$consequences
=
array_diff
(
$consequences
,
[
'disallow'
]
);
}
$this
->
assertArrayEquals
(
$consequences
,
explode
(
','
,
$row
->
afl_actions
),
false
,
false
,
$dumpStr
);
}
}
/**
* Creates new filters, execute an action and check the consequences
*
* @param int[] $createIds IDs of the filters to create
* @param array $actionParams Details of the action we need to execute to trigger filters
* @param array $expectedConsequences The consequences we're expecting
* @dataProvider provideFilters
* @covers \MediaWiki\Extension\AbuseFilter\AbuseLogger
*/
public
function
testFilterConsequences
(
$createIds
,
$actionParams
,
$expectedConsequences
)
{
$this
->
createFilters
(
$createIds
);
$result
=
$this
->
doAction
(
$actionParams
);
[
$expected
,
$actual
]
=
$this
->
checkConsequences
(
$result
,
$actionParams
,
$expectedConsequences
);
$this
->
assertEquals
(
$expected
,
$actual
,
'The error messages obtained by performing the action do not match.'
);
$this
->
assertAbuseLog
(
$actionParams
,
$expectedConsequences
);
}
/**
* Data provider for testFilterConsequences. For every test case, we pass
* - an array with the IDs of the filters to be created (listed in self::FILTERS),
* - an array with details of the action to execute in order to trigger the filters,
* - an array of expected consequences of the form
* [ 'consequence name' => [ IDs of the filter to take its parameters from ] ]
* Such IDs may be more than one if we have a warning that is shown twice.
*
* @return array
*/
public
static
function
provideFilters
()
{
// XXX Need to hardcode the username of $this->user here.
$username
=
'UTSysop'
;
return
[
'Basic test for "edit" action'
=>
[
[
1
,
2
],
[
'action'
=>
'edit'
,
'target'
=>
'Test page'
,
'oldText'
=>
'Some old text for the test.'
,
'newText'
=>
'I like foo'
,
'summary'
=>
'Test AbuseFilter for edit action.'
],
[
'warn'
=>
[
1
]
]
],
'Basic test for "move" action'
=>
[
[
2
],
[
'action'
=>
'move'
,
'target'
=>
'Test page'
,
'newTitle'
=>
'Another test page'
],
[
'block'
=>
[
2
]
]
],
'Basic test for "delete" action'
=>
[
[
2
,
3
],
[
'action'
=>
'delete'
,
'target'
=>
'Test page'
],
[
'degroup'
=>
[
3
]
]
],
'Basic test for "createaccount", no consequences.'
=>
[
[
1
,
2
,
3
],
[
'action'
=>
'createaccount'
,
'target'
=>
'User:AnotherUser'
,
'username'
=>
'AnotherUser'
],
[]
],
'Basic test for "createaccount", disallowed.'
=>
[
[
15
],
[
'action'
=>
'createaccount'
,
'target'
=>
'User:AnotherUser'
,
'username'
=>
'AnotherUser'
],
[
'disallow'
=>
[
15
]
]
],
'Check that all tags are applied'
=>
[
[
5
],
[
'action'
=>
'edit'
,
'target'
=>
"User:$username"
,
'oldText'
=>
'Hey.'
,
'newText'
=>
'I am a very nice user, really!'
,
'summary'
=>
''
],
[
'tag'
=>
[
5
]
]
],
'Check that degroup and block are executed together'
=>
[
[
2
,
3
,
7
,
8
],
[
'action'
=>
'edit'
,
'target'
=>
'Link'
,
'oldText'
=>
'What is a link?'
,
'newText'
=>
'A link is something like this: [[Link|]].'
,
'summary'
=>
'Explaining'
],
[
'degroup'
=>
[
7
],
'block'
=>
[
8
]
]
],
'Check that the block duration is the longer one'
=>
[
[
8
,
9
],
[
'action'
=>
'edit'
,
'target'
=>
'Whatever'
,
'oldText'
=>
'Whatever is whatever'
,
'newText'
=>
'Whatever is whatever, whatever it is. BTW, here is a [[Link|]]'
,
'summary'
=>
'Whatever'
],
[
'disallow'
=>
[
8
],
'block'
=>
[
8
]
]
],
'Check that throttled filters only execute "safe" actions'
=>
[
[
10
],
[
'action'
=>
'edit'
,
'target'
=>
'Buffalo'
,
'oldText'
=>
'Buffalo'
,
'newText'
=>
'Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.'
,
'summary'
=>
'Buffalo!'
],
[
'tag'
=>
[
10
]
]
],
'Check that degroup and block are both executed and degroup warning is shown twice'
=>
[
[
1
,
3
,
7
,
12
],
[
'action'
=>
'edit'
,
'target'
=>
"User:$username"
,
'oldText'
=>
''
,
'newText'
=>
'A couple of lines about me...'
,
'summary'
=>
'My user page'
],
[
'block'
=>
[
12
],
'degroup'
=>
[
7
,
12
]
]
],
'Check that every throttled filter only executes "safe" actions'
=>
[
[
10
,
13
],
[
'action'
=>
'edit'
,
'target'
=>
'Tyger! Tyger! Burning bright'
,
'oldText'
=>
'In the forests of the night'
,
'newText'
=>
'What immortal hand or eye'
,
'summary'
=>
'Could frame thy fearful symmetry?'
],
[
'tag'
=>
[
10
]
]
],
'Check that runtime exceptions (division by zero) are correctly handled'
=>
[
[
14
],
[
'action'
=>
'edit'
,
'target'
=>
'0'
,
'oldText'
=>
'Old text'
,
'newText'
=>
'New text'
,
'summary'
=>
'Some summary'
],
[]
],
'Test for blockautopromote action.'
=>
[
[
21
],
[
'action'
=>
'edit'
,
'target'
=>
'Rainbow'
,
'oldText'
=>
''
,
'newText'
=>
'...'
,
'summary'
=>
''
],
[
'blockautopromote'
=>
[
21
]
],
],
'No consequences'
=>
[
[
8
],
[
'action'
=>
'edit'
,
'target'
=>
'Anything'
,
'oldText'
=>
'Bar'
,
'newText'
=>
'Foo'
,
'summary'
=>
''
],
[]
],
'Test upload action'
=>
[
[
5
],
[
'action'
=>
'upload'
,
'target'
=>
'File:MyFile.svg'
,
],
[
'tag'
=>
[
5
]
]
],
'Test upload action 2'
=>
[
[
22
],
[
'action'
=>
'upload'
,
'target'
=>
'File:MyFile.svg'
,
'newText'
=>
'Block me please!'
,
'summary'
=>
'Asking to be blocked'
],
[
'warn'
=>
[
22
]
]
],
];
}
/**
* Check an array of results from self::doAction to ensure that all but the last actions have been
* executed (i.e. no errors).
* @param Status[] $results As returned by self::doActions
* @return Status The Status of the last action, to be later checked with self::checkConsequences
*/
private
function
checkThrottleConsequence
(
$results
)
{
$finalRes
=
array_pop
(
$results
);
foreach
(
$results
as
$result
)
{
if
(
!
$result
->
isGood
()
)
{
$this
->
fail
(
'Only the last actions should have triggered a filter; the other ones '
.
'should have been allowed.'
);
}
}
return
$finalRes
;
}
/**
* Like self::testFilterConsequences but for throttle, which deserves a special treatment
*
* @param int[] $createIds IDs of the filters to create
* @param array[] $actionsParams Details of the action we need to execute to trigger filters
* @param array $consequences The consequences we're expecting
* @dataProvider provideThrottleFilters
* @covers \MediaWiki\Extension\AbuseFilter\AbuseLogger
*/
public
function
testThrottle
(
$createIds
,
$actionsParams
,
$consequences
)
{
$this
->
createFilters
(
$createIds
);
$results
=
$this
->
doActions
(
$actionsParams
);
$res
=
$this
->
checkThrottleConsequence
(
$results
);
$lastParams
=
array_pop
(
$actionsParams
);
[
$expected
,
$actual
]
=
$this
->
checkConsequences
(
$res
,
$lastParams
,
$consequences
);
$this
->
assertEquals
(
$expected
,
$actual
,
'The error messages obtained by performing the action do not match.'
);
$this
->
assertAbuseLog
(
$lastParams
,
$consequences
);
}
/**
* Data provider for testThrottle. For every test case, we pass
* - an array with the IDs of the filters to be created (listed in self::FILTERS),
* - an array of array, where every sub-array holds the details of the action to execute in
* order to trigger the filters, each one like in self::provideFilters
* - an array of expected consequences for the last action (i.e. after throttling) of the form
* [ 'consequence name' => [ IDs of the filter to take its parameters from ] ]
* Such IDs may be more than one if we have a warning that is shown twice.
*
*
* @return array
*/
public
static
function
provideThrottleFilters
()
{
return
[
'Basic test for throttling edits'
=>
[
[
11
],
[
[
'action'
=>
'edit'
,
'target'
=>
'Throttle'
,
'oldText'
=>
'What is throttle?'
,
'newText'
=>
'Throttle is something that should happen...'
,
'summary'
=>
'Throttle'
],
[
'action'
=>
'edit'
,
'target'
=>
'Throttle'
,
'oldText'
=>
'Throttle is something that should happen...'
,
'newText'
=>
'... Right now!'
,
'summary'
=>
'Throttle'
]
],
[
'disallow'
=>
[
11
]
]
],
'Basic test for throttling "move"'
=>
[
[
11
],
[
[
'action'
=>
'move'
,
'target'
=>
'Throttle test'
,
'newTitle'
=>
'Another throttle test'
],
[
'action'
=>
'move'
,
'target'
=>
'Another throttle test'
,
'newTitle'
=>
'Yet another throttle test'
],
],
[
'disallow'
=>
[
11
]
]
],
'Basic test for throttling "delete"'
=>
[
[
11
],
[
[
'action'
=>
'delete'
,
'target'
=>
'Test page'
],
[
'action'
=>
'delete'
,
'target'
=>
'Test page'
]
],
[
'disallow'
=>
[
11
]
]
],
'Basic test for throttling "createaccount"'
=>
[
[
11
],
[
[
'action'
=>
'createaccount'
,
'target'
=>
'User:AnotherUser'
,
'username'
=>
'AnotherUser'
],
[
'action'
=>
'createaccount'
,
'target'
=>
'User:YetAnotherUser'
,
'username'
=>
'YetAnotherUser'
]
],
[
'disallow'
=>
[
11
]
]
]
];
}
/**
* Like self::testFilterConsequences but for warn, which deserves a special treatment.
* Data provider passes parameters for a single action, which we repeat twice
*
* @param int[] $createIds IDs of the filters to create
* @param array[] $actionParams Details of the action we need to execute to trigger filters
* @param int[] $warnIDs IDs of the filters which will warn us
* @param array $consequences The consequences we're expecting
* @dataProvider provideWarnFilters
* @covers \MediaWiki\Extension\AbuseFilter\AbuseLogger
*/
public
function
testWarn
(
$createIds
,
$actionParams
,
$warnIDs
,
$consequences
)
{
$this
->
createFilters
(
$createIds
);
$params
=
[
$actionParams
,
$actionParams
];
[
$warnedStatus
,
$finalStatus
]
=
$this
->
doActions
(
$params
);
[
$expectedWarn
,
$actualWarn
]
=
$this
->
checkConsequences
(
$warnedStatus
,
$actionParams
,
[
'warn'
=>
$warnIDs
]
);
$this
->
assertSame
(
$expectedWarn
,
$actualWarn
,
'The error messages for the first action do not match.'
);
[
$expectedFinal
,
$actualFinal
]
=
$this
->
checkConsequences
(
$finalStatus
,
$actionParams
,
$consequences
);
$this
->
assertSame
(
$expectedFinal
,
$actualFinal
,
'The error messages for the second action do not match.'
);
$this
->
assertAbuseLog
(
$actionParams
,
$consequences
);
}
/**
* Data provider for testWarn. For every test case, we pass
* - an array with the IDs of the filters to be created (listed in self::FILTERS),
* - an array with action parameters, like in self::provideFilters. This will be executed twice.
* - an array of IDs of the filter which should give a warning
* - an array of expected consequences for the last action (i.e. after throttling) of the form
* [ 'consequence name' => [ IDs of the filter to take its parameters from ] ]
* Such IDs may be more than one if we have a warning that is shown twice.
*
* @return array
*/
public
static
function
provideWarnFilters
()
{
return
[
'Basic test for warning and then tag'
=>
[
[
1
],
[
'action'
=>
'edit'
,
'target'
=>
'Foo'
,
'oldText'
=>
'Neutral text'
,
'newText'
=>
'First foo version'
,
'summary'
=>
''
],
[
1
],
[
'tag'
=>
[
1
]
],
],
'Basic test for warning on "move"'
=>
[
[
23
],
[
'action'
=>
'move'
,
'target'
=>
'Test warn'
,
'newTitle'
=>
'Another warn test'
],
[
23
],
[
'disallow'
=>
[
23
]
]
],
'Basic test for warning on "delete"'
=>
[
[
23
],
[
'action'
=>
'delete'
,
'target'
=>
'Warned'
],
[
23
],
[
'disallow'
=>
[
23
]
]
],
'Basic test for warning on "createaccount"'
=>
[
[
23
],
[
'action'
=>
'createaccount'
,
'target'
=>
'User:AnotherWarnedUser'
,
'username'
=>
'AnotherWarnedUser'
],
[
23
],
[
'disallow'
=>
[
23
]
]
]
];
}
/**
* @param int[] $createIds IDs of the filters to create
* @param array $actionParams Details of the action we need to execute to trigger filters
* @param string[] $usedVars The variables effectively computed by filters in $createIds.
* We'll search these in the stored dump.
* @covers \MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore
* @dataProvider provideFiltersAndVariables
*/
public
function
testVarDump
(
$createIds
,
$actionParams
,
$usedVars
)
{
$this
->
createFilters
(
$createIds
);
// We don't care about consequences here
$this
->
doAction
(
$actionParams
);
// We just take a dump from a single filters, as they're all identical for the same action
$row
=
$this
->
getDb
()->
newSelectQueryBuilder
()
->
select
(
'afl_var_dump, afl_ip'
)
->
from
(
'abuse_filter_log'
)
->
orderBy
(
'afl_timestamp'
,
SelectQueryBuilder
::
SORT_DESC
)
->
caller
(
__METHOD__
)
->
fetchRow
();
$vars
=
AbuseFilterServices
::
getVariablesBlobStore
()->
loadVarDump
(
$row
)->
getVars
();
$interestingVars
=
array_intersect_key
(
$vars
,
array_fill_keys
(
$usedVars
,
true
)
);
sort
(
$usedVars
);
ksort
(
$interestingVars
);
$this
->
assertEquals
(
$usedVars
,
array_keys
(
$interestingVars
),
"The saved variables aren't the expected ones."
);
$this
->
assertContainsOnlyInstancesOf
(
AFPData
::
class
,
$interestingVars
,
'Some variables have not been computed.'
);
}
/**
* Data provider for testVarDump
*
* @return array
*/
public
static
function
provideFiltersAndVariables
()
{
return
[
[
[
1
,
2
],
[
'action'
=>
'edit'
,
'target'
=>
'Test page'
,
'oldText'
=>
'Some old text for the test.'
,
'newText'
=>
'I like foo'
,
'summary'
=>
'Test AbuseFilter for edit action.'
],
[
'added_lines'
,
'action'
]
],
[
[
1
,
2
],
[
'action'
=>
'stashedit'
,
'target'
=>
'Test page'
,
'oldText'
=>
'Some old text for the test.'
,
'newText'
=>
'I like foo'
,
'summary'
=>
'Test AbuseFilter for edit action.'
,
'stashType'
=>
'hit'
],
[
'added_lines'
,
'action'
]
],
[
[
1
,
2
],
[
'action'
=>
'stashedit'
,
'target'
=>
'Test page'
,
'oldText'
=>
'Some old text for the test.'
,
'newText'
=>
'I like foo'
,
'summary'
=>
'Test AbuseFilter for edit action.'
,
'stashType'
=>
'miss'
],
[
'added_lines'
,
'action'
]
],
[
[
2
],
[
'action'
=>
'move'
,
'target'
=>
'Test page'
,
'newTitle'
=>
'Another test page'
],
[
'action'
,
'moved_to_title'
]
],
[
[
2
,
3
],
[
'action'
=>
'delete'
,
'target'
=>
'Test page'
],
[
'action'
,
'page_prefixedtitle'
]
],
[
[
15
],
[
'action'
=>
'createaccount'
,
'target'
=>
'User:AnotherUser'
,
'username'
=>
'AnotherUser'
],
[
'action'
]
],
];
}
/**
* Same as testFilterConsequences but only for stashed edits
*
* @param string $type Either "hit" or "miss". The former saves the edit from stash, the second
* stashes the edit but doesn't reuse it.
* @param int[] $createIds IDs of the filters to create
* @param array $actionParams Details of the action we need to execute to trigger filters
* @param array $consequences The consequences we're expecting
* @dataProvider provideStashedEdits
* @covers \MediaWiki\Extension\AbuseFilter\AbuseLogger
*/
public
function
testStashedEdit
(
$type
,
$createIds
,
$actionParams
,
$consequences
)
{
if
(
$type
!==
'hit'
&&
$type
!==
'miss'
)
{
throw
new
InvalidArgumentException
(
'$type must be either "hit" or "miss"'
);
}
// Add some info in actionParams identical for all tests
$actionParams
[
'action'
]
=
'stashedit'
;
$actionParams
[
'stashType'
]
=
$type
;
$loggerMock
=
$this
->
createMock
(
LoggerInterface
::
class
);
$loggerCalls
=
[];
$loggerMock
->
method
(
'debug'
)
->
willReturnCallback
(
static
function
(
$msg
,
$args
)
use
(
&
$loggerCalls
)
{
if
(
isset
(
$args
[
'logtype'
]
)
)
{
$loggerCalls
[]
=
$args
[
'logtype'
];
}
}
);
$this
->
setLogger
(
'StashEdit'
,
$loggerMock
);
$this
->
createFilters
(
$createIds
);
$result
=
$this
->
doAction
(
$actionParams
);
// Check that we stored the edit and then hit/missed the cache
if
(
!
in_array
(
'store'
,
$loggerCalls
,
true
)
)
{
$this
->
fail
(
'Did not store the edit in cache as expected for a stashed edit.'
);
}
elseif
(
!
in_array
(
$type
,
$loggerCalls
,
true
)
)
{
$this
->
fail
(
"Did not $type the cache as expected for a stashed edit."
);
}
[
$expected
,
$actual
]
=
$this
->
checkConsequences
(
$result
,
$actionParams
,
$consequences
);
$this
->
assertEquals
(
$expected
,
$actual
,
'The error messages obtained by performing the action do not match.'
);
$this
->
assertAbuseLog
(
$actionParams
,
$consequences
);
}
public
static
function
provideStashedEdits
()
{
// XXX Need to hardcode the username of $this->user here.
$username
=
'UTSysop'
;
$sets
=
[
[
[
1
,
2
],
[
'target'
=>
'Test page'
,
'oldText'
=>
'Some old text for the test.'
,
'newText'
=>
'I like foo'
,
'summary'
=>
'Test AbuseFilter for edit action.'
],
[
'warn'
=>
[
1
]
]
],
[
[
5
],
[
'target'
=>
"User:$username"
,
'oldText'
=>
'Hey.'
,
'newText'
=>
'I am a very nice user, really!'
,
'summary'
=>
''
],
[
'tag'
=>
[
5
]
]
],
[
[
6
],
[
'target'
=>
'Help:Help'
,
'oldText'
=>
'Some help.'
,
'newText'
=>
'Some help for you'
,
'summary'
=>
'Help! I need somebody'
],
[
'disallow'
=>
[
6
]
]
],
[
[
2
,
3
,
7
,
8
],
[
'target'
=>
'Link'
,
'oldText'
=>
'What is a link?'
,
'newText'
=>
'A link is something like this: [[Link|]].'
,
'summary'
=>
'Explaining'
],
[
'degroup'
=>
[
7
],
'block'
=>
[
8
]
]
],
[
[
8
,
9
],
[
'target'
=>
'Whatever'
,
'oldText'
=>
'Whatever is whatever'
,
'newText'
=>
'Whatever is whatever, whatever it is. BTW, here is a [[Link|]]'
,
'summary'
=>
'Whatever'
],
[
'disallow'
=>
[
8
],
'block'
=>
[
8
]
]
],
[
[
10
],
[
'target'
=>
'Buffalo'
,
'oldText'
=>
'Buffalo'
,
'newText'
=>
'Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.'
,
'summary'
=>
'Buffalo!'
],
[
'tag'
=>
[
10
]
]
],
[
[
1
,
3
,
7
,
12
],
[
'target'
=>
"User:$username"
,
'oldText'
=>
''
,
'newText'
=>
'A couple of lines about me...'
,
'summary'
=>
'My user page'
],
[
'block'
=>
[
12
],
'degroup'
=>
[
7
,
12
]
]
],
[
[
10
,
13
],
[
'target'
=>
'Tyger! Tyger! Burning bright'
,
'oldText'
=>
'In the forests of the night'
,
'newText'
=>
'What immortal hand or eye'
,
'summary'
=>
'Could frame thy fearful symmetry?'
],
[
'tag'
=>
[
10
]
]
],
[
[
14
],
[
'target'
=>
'0'
,
'oldText'
=>
'Old text'
,
'newText'
=>
'New text'
,
'summary'
=>
'Some summary'
],
[]
],
];
foreach
(
$sets
as
$set
)
{
// Test both successfully saving a stashed edit and stashing the edit but re-executing filters
yield
[
'miss'
,
...
$set
];
yield
[
'hit'
,
...
$set
];
}
}
/**
* Tests for global filters, defined on a central wiki and executed on another (e.g. a filter
* defined on meta but triggered on another wiki using meta's global filters).
* For simplicity, this test creates filters in the local database but makes the extension believe that it's
* actually external.
*
* @param int[] $createIds IDs of the filters to create
* @param array $actionParams Details of the action we need to execute to trigger filters
* @param array $consequences The consequences we're expecting
* @dataProvider provideGlobalFilters
* @covers \MediaWiki\Extension\AbuseFilter\AbuseLogger
*/
public
function
testGlobalFilters
(
$createIds
,
$actionParams
,
$consequences
)
{
$this
->
overrideConfigValues
(
[
'AbuseFilterCentralDB'
=>
WikiMap
::
getCurrentWikiId
(),
'AbuseFilterIsCentral'
=>
false
,
]
);
$this
->
createFilters
(
$createIds
);
AbuseFilterServices
::
getFilterLookup
(
$this
->
getServiceContainer
()
)->
hideLocalFiltersForTesting
();
$result
=
$this
->
doAction
(
$actionParams
);
[
$expected
,
$actual
]
=
$this
->
checkConsequences
(
$result
,
$actionParams
,
$consequences
);
// First check that the filters work as expected
$this
->
assertEquals
(
$expected
,
$actual
,
'The error messages obtained by performing the action do not match.'
);
// Check that the hits were logged on the "external" DB
$dbr
=
$this
->
getDb
();
$loggedFilters
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'afl_filter_id'
)
->
from
(
'abuse_filter_log'
)
->
where
(
$dbr
->
expr
(
'afl_wiki'
,
'!='
,
null
)
)
->
caller
(
__METHOD__
)
->
fetchFieldValues
();
// Use assertEquals because selectFieldValues returns an array of strings
$this
->
assertEquals
(
$createIds
,
$loggedFilters
,
'Some filter hits were not logged in the external DB.'
);
}
/**
* Data provider for testGlobalFilters
*
* @return array
*/
public
static
function
provideGlobalFilters
()
{
return
[
[
[
18
],
[
'action'
=>
'edit'
,
'target'
=>
'Global'
,
'oldText'
=>
'Old text'
,
'newText'
=>
'New text'
,
'summary'
=>
''
],
[
'warn'
=>
[
18
]
]
],
[
[
19
],
[
'action'
=>
'edit'
,
'target'
=>
'A global page'
,
'oldText'
=>
'Foo'
,
'newText'
=>
'Bar'
,
'summary'
=>
'Baz'
],
[
'tag'
=>
[
19
]
]
],
[
[
18
],
[
'action'
=>
'move'
,
'target'
=>
'Cellar door'
,
'newTitle'
=>
'Attic door'
],
[
'warn'
=>
[
18
]
]
],
[
[
19
,
20
],
[
'action'
=>
'delete'
,
'target'
=>
'Cellar door'
,
],
[
'disallow'
=>
[
20
]
]
],
[
[
19
],
[
'action'
=>
'stashedit'
,
'target'
=>
'Cellar door'
,
'oldText'
=>
''
,
'newText'
=>
'Too many doors'
,
'summary'
=>
''
,
'stashType'
=>
'hit'
],
[
'tag'
=>
[
19
]
]
],
[
[
18
],
[
'action'
=>
'createaccount'
,
'target'
=>
'User:AbuseFilterGlobalUser'
,
'username'
=>
'AbuseFilterGlobalUser'
],
[
'warn'
=>
[
18
]
]
]
];
}
/**
* Make sure that after an edit is saved, if a filter was hit the afl_rev_id is updated
* to reflect the new edit
*
* Regression tests for T286140
*/
public
function
testRevIdSet
()
{
// Filter 24 has no actions and always matches
$this
->
createFilters
(
[
24
]
);
$targetTitle
=
Title
::
makeTitle
(
NS_MAIN
,
'TestRevIdSet'
);
$startingRevId
=
$targetTitle
->
getLatestRevID
(
IDBAccessObject
::
READ_LATEST
);
$this
->
doEdit
(
$targetTitle
,
'Old text'
,
'New text'
,
'Summary'
);
$latestRevId
=
$targetTitle
->
getLatestRevID
(
IDBAccessObject
::
READ_LATEST
);
$this
->
assertNotSame
(
$startingRevId
,
$latestRevId
,
'Edit should have been properly saved'
);
// Check the database for the filter hit
// We don't have an easy way to retrieve the afl_id for this relevant hit,
// so instead find the latest row for this filter
$filterHit
=
$this
->
getDb
()->
newSelectQueryBuilder
()
->
select
(
'*'
)
->
from
(
'abuse_filter_log'
)
->
where
(
[
'afl_filter_id'
=>
24
]
)
->
orderBy
(
'afl_id'
,
SelectQueryBuilder
::
SORT_DESC
)
->
caller
(
__METHOD__
)
->
fetchRow
();
// Helpful for debugging
$filterHitStr
=
FormatJson
::
encode
(
$filterHit
);
$this
->
assertSame
(
$latestRevId
,
(
int
)(
$filterHit
->
afl_rev_id
),
"AbuseLog entry updated with the revision id (full filter hit: $filterHitStr)"
);
}
/**
* Test a null edit.
* @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\RunVariableGenerator
*/
public
function
testNullEdit
()
{
$this
->
setService
(
FilterRunnerFactory
::
SERVICE_NAME
,
$this
->
createNoOpMock
(
FilterRunnerFactory
::
class
)
);
// Filter 24 has no actions and always matches
$this
->
createFilters
(
[
24
]
);
$targetTitle
=
Title
::
makeTitle
(
NS_MAIN
,
'TestNullEdit'
);
$text
=
'Some text'
;
$status
=
$this
->
doEdit
(
$targetTitle
,
$text
,
$text
,
'Summary'
);
$this
->
assertStatusGood
(
$status
);
}
/**
* Test a content model change.
* @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\RunVariableGenerator
*/
public
function
testContentModelChange
()
{
// Filter 23 always matches and disables
$this
->
createFilters
(
[
23
]
);
$targetTitle
=
Title
::
makeTitle
(
NS_MAIN
,
'TestContentModelChange'
);
$text
=
FormatJson
::
encode
(
[
'key'
=>
'value'
]
);
$oldContent
=
new
JsonContent
(
$text
);
$newContent
=
new
WikitextContent
(
$text
);
$status
=
$this
->
doEdit
(
$targetTitle
,
$oldContent
,
$newContent
,
'Summary'
);
$this
->
assertStatusNotOK
(
$status
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Jul 3, 20:25 (1 d, 20 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
54/33/2f3095f4cf0d0849347fdffb9ebc
Default Alt Text
AbuseFilterConsequencesTest.php (47 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment