Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4057840
SpecialAbuseFilterTest.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
37 KB
Referenced Files
None
Subscribers
None
SpecialAbuseFilterTest.php
View Options
<?php
namespace
MediaWiki\Extension\AbuseFilter\Tests\Integration\Special
;
use
MediaWiki\Context\RequestContext
;
use
MediaWiki\Deferred\DeferredUpdates
;
use
MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager
;
use
MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionStatus
;
use
MediaWiki\Extension\AbuseFilter\AbuseFilterServices
;
use
MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException
;
use
MediaWiki\Extension\AbuseFilter\Filter\Flags
;
use
MediaWiki\Extension\AbuseFilter\Filter\MutableFilter
;
use
MediaWiki\Extension\AbuseFilter\FilterLookup
;
use
MediaWiki\Extension\AbuseFilter\ProtectedVarsAccessLogger
;
use
MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseFilter
;
use
MediaWiki\Extension\AbuseFilter\SpecsFormatter
;
use
MediaWiki\Extension\AbuseFilter\Tests\Integration\FilterFromSpecsTestTrait
;
use
MediaWiki\Extension\AbuseFilter\Variables\VariableHolder
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewDiff
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewEdit
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewExamine
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewHistory
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewImport
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewList
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewRevert
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTestBatch
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTools
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Logging\LogEntryBase
;
use
MediaWiki\Logging\ManualLogEntry
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Permissions\Authority
;
use
MediaWiki\Permissions\UltimateAuthority
;
use
MediaWiki\Request\FauxRequest
;
use
MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\UserIdentity
;
use
SpecialPageTestBase
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
use
Wikimedia\Parsoid\Utils\DOMUtils
;
/**
* @covers \MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseFilter
* @covers \MediaWiki\Extension\AbuseFilter\Special\AbuseFilterSpecialPage
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterView
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewDiff
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewEdit
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewExamine
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewHistory
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewImport
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewList
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewRevert
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTestBatch
* @covers \MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTools
* @covers \MediaWiki\Extension\AbuseFilter\Pager\AbuseFilterHistoryPager
* @covers \MediaWiki\Extension\AbuseFilter\Pager\AbuseFilterPager
* @group Database
*/
class
SpecialAbuseFilterTest
extends
SpecialPageTestBase
{
use
MockAuthorityTrait
;
use
FilterFromSpecsTestTrait
;
private
Authority
$authorityCannotUseProtectedVar
;
private
Authority
$authorityCanUseProtectedVar
;
private
static
int
$recentChangeId
;
private
static
string
$userWhoHitFilter
;
protected
function
setUp
():
void
{
parent
::
setUp
();
// Clear the protected access hooks, as in CI other extensions (such as CheckUser) may attempt to
// define additional restrictions or alter logging that cause the tests to fail.
$this
->
clearHooks
(
[
'AbuseFilterCanViewProtectedVariables'
,
'AbuseFilterLogProtectedVariableValueAccess'
,
]
);
// Create an authority who can see private filters but not protected variables
$this
->
authorityCannotUseProtectedVar
=
$this
->
mockUserAuthorityWithPermissions
(
$this
->
getMutableTestUser
()->
getUserIdentity
(),
[
'abusefilter-log-private'
,
'abusefilter-view-private'
,
'abusefilter-modify'
,
'abusefilter-log-detail'
,
]
);
// Create an authority who can see private and protected variables
$this
->
authorityCanUseProtectedVar
=
$this
->
mockUserAuthorityWithPermissions
(
$this
->
getMutableTestUser
()->
getUserIdentity
(),
[
'abusefilter-access-protected-vars'
,
'abusefilter-log-private'
,
'abusefilter-view-private'
,
'abusefilter-modify'
,
'abusefilter-log-detail'
,
]
);
}
protected
function
tearDown
():
void
{
parent
::
tearDown
();
// Clear protected variable access logs between tests to avoid failures, as the
// test class does not truncate the 'logging' table between tests.
$this
->
dropProtectedVarAccessLogs
();
}
/**
* @inheritDoc
*/
public
function
addDBDataOnce
()
{
$filterStore
=
AbuseFilterServices
::
getFilterStore
();
$performer
=
$this
->
getTestSysop
()->
getUserIdentity
();
$userWhoHitFilter
=
$this
->
getTestUser
()->
getUser
();
$authority
=
new
UltimateAuthority
(
$performer
);
// Create a test filter where first revision is public, and the second two are protected.
// The public revision exists to test handling in AbuseFilterViewHistory.
$firstFilterRevision
=
$this
->
getFilterFromSpecs
(
[
'id'
=>
'1'
,
'rules'
=>
'user_name = "1.2.3.5"'
,
'name'
=>
'Filter to be converted'
,
'privacy'
=>
Flags
::
FILTER_PUBLIC
,
'userIdentity'
=>
$performer
,
'timestamp'
=>
$this
->
getDb
()->
timestamp
(
'20190825000000'
),
]
);
$this
->
assertStatusGood
(
$filterStore
->
saveFilter
(
$authority
,
null
,
$firstFilterRevision
,
MutableFilter
::
newDefault
()
)
);
$secondFilterRevision
=
$this
->
getFilterFromSpecs
(
[
'id'
=>
'1'
,
'rules'
=>
'user_unnamed_ip = "1.2.3.5"'
,
'name'
=>
'Filter with protected variables'
,
'privacy'
=>
Flags
::
FILTER_USES_PROTECTED_VARS
,
'userIdentity'
=>
$performer
,
'timestamp'
=>
$this
->
getDb
()->
timestamp
(
'20190826000000'
),
]
);
$this
->
assertStatusGood
(
$filterStore
->
saveFilter
(
$authority
,
1
,
$secondFilterRevision
,
$firstFilterRevision
)
);
$this
->
assertStatusGood
(
$filterStore
->
saveFilter
(
$authority
,
1
,
$this
->
getFilterFromSpecs
(
[
'id'
=>
'1'
,
'rules'
=>
'user_unnamed_ip = "1.2.3.4"'
,
'name'
=>
'Filter with protected variables'
,
'privacy'
=>
Flags
::
FILTER_USES_PROTECTED_VARS
,
'userIdentity'
=>
$performer
,
'timestamp'
=>
$this
->
getDb
()->
timestamp
(
'20190827000000'
),
'hitCount'
=>
1
,
'actions'
=>
[
'tags'
=>
[
'test'
]
]
]
),
$secondFilterRevision
)
);
// Create a second filter which is not public
$this
->
assertStatusGood
(
$filterStore
->
saveFilter
(
$authority
,
null
,
$this
->
getFilterFromSpecs
(
[
'id'
=>
'2'
,
'rules'
=>
'user_name = "1.2.3.4"'
,
'name'
=>
'Filter without protected variables'
,
'privacy'
=>
Flags
::
FILTER_PUBLIC
,
'userIdentity'
=>
$performer
,
'timestamp'
=>
'20000101000000'
,
]
),
MutableFilter
::
newDefault
()
)
);
// Add a log on the protected filter which has a hit count of 1
RequestContext
::
getMain
()->
getRequest
()->
setIP
(
'1.2.3.4'
);
$abuseFilterLoggerFactory
=
AbuseFilterServices
::
getAbuseLoggerFactory
();
$abuseFilterLoggerFactory
->
newLogger
(
$this
->
getExistingTestPage
()->
getTitle
(),
$userWhoHitFilter
,
VariableHolder
::
newFromArray
(
[
'action'
=>
'edit'
,
'user_unnamed_ip'
=>
'1.2.3.4'
,
'user_name'
=>
$userWhoHitFilter
->
getName
(),
]
)
)->
addLogEntries
(
[
1
=>
[
'warn'
]
]
);
// Verify that the expected number of DB rows were created
$this
->
newSelectQueryBuilder
()
->
select
(
'COUNT(*)'
)
->
table
(
'abuse_filter'
)
->
caller
(
__METHOD__
)
->
assertFieldValue
(
2
);
$this
->
newSelectQueryBuilder
()
->
select
(
'COUNT(*)'
)
->
table
(
'abuse_filter_history'
)
->
caller
(
__METHOD__
)
->
assertFieldValue
(
4
);
$this
->
newSelectQueryBuilder
()
->
select
(
'COUNT(*)'
)
->
table
(
'abuse_filter_log'
)
->
caller
(
__METHOD__
)
->
assertFieldValue
(
1
);
// Create a testing recentchanges table row by creating a logging table row that is sent to recentchanges.
$logEntry
=
new
ManualLogEntry
(
'move'
,
'move'
);
$logEntry
->
setPerformer
(
$userWhoHitFilter
);
$logEntry
->
setTarget
(
$this
->
getExistingTestPage
()->
getTitle
()
);
$logEntry
->
setComment
(
'A very good reason'
);
$logEntry
->
setParameters
(
[
'4::target'
=>
wfRandomString
(),
'5::noredir'
=>
'0'
]
);
$logId
=
$logEntry
->
insert
();
$logEntry
->
publish
(
$logId
);
// Check that the recentchanges row for the log entry exists and get the ID for it.
$recentChangeId
=
$this
->
newSelectQueryBuilder
()
->
select
(
'rc_id'
)
->
from
(
'recentchanges'
)
->
where
(
[
'rc_logid'
=>
$logId
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
$this
->
assertNotFalse
(
$recentChangeId
);
self
::
$recentChangeId
=
$recentChangeId
;
self
::
$userWhoHitFilter
=
$userWhoHitFilter
->
getName
();
}
/**
* @dataProvider provideInstantiateView
*/
public
function
testInstantiateView
(
string
$viewClass
,
array
$params
=
[]
)
{
$sp
=
$this
->
newSpecialPage
();
$view
=
$sp
->
instantiateView
(
$viewClass
,
$params
);
$this
->
assertInstanceOf
(
$viewClass
,
$view
);
}
public
static
function
provideInstantiateView
():
array
{
return
[
[
AbuseFilterViewDiff
::
class
],
[
AbuseFilterViewEdit
::
class
,
[
'filter'
=>
1
]
],
[
AbuseFilterViewExamine
::
class
],
[
AbuseFilterViewHistory
::
class
],
[
AbuseFilterViewImport
::
class
],
[
AbuseFilterViewList
::
class
],
[
AbuseFilterViewRevert
::
class
],
[
AbuseFilterViewTestBatch
::
class
],
[
AbuseFilterViewTools
::
class
],
];
}
/**
* @inheritDoc
*/
protected
function
newSpecialPage
():
SpecialAbuseFilter
{
$services
=
MediaWikiServices
::
getInstance
();
$sp
=
new
SpecialAbuseFilter
(
$services
->
getService
(
AbuseFilterPermissionManager
::
SERVICE_NAME
),
$services
->
getObjectFactory
()
);
$sp
->
setLinkRenderer
(
$services
->
getLinkRendererFactory
()->
create
()
);
return
$sp
;
}
public
function
testViewEditTokenMismatch
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'new'
,
new
FauxRequest
(
[
'wpFilterDescription'
=>
'Test filter'
,
'wpFilterRules'
=>
'user_name = "1.2.3.4"'
,
'wpFilterNotes'
=>
''
,
],
// This was posted
true
,
),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'abusefilter-edit-token-not-match'
,
$html
,
'The token mismatch warning message was not present.'
);
}
public
function
testViewEditUnrecoverableError
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'new'
,
new
FauxRequest
(
[
'wpFilterDescription'
=>
''
,
'wpFilterRules'
=>
'user_name = "1.2.3.4"'
,
'wpFilterNotes'
=>
''
,
],
// This was posted
true
,
)
);
$this
->
assertStringContainsString
(
'abusefilter-edit-notallowed'
,
$html
,
'The permission error message was not present.'
);
}
public
function
testViewEditForInvalidImport
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'new'
,
new
FauxRequest
(
[
'wpImportText'
=>
'abc'
],
true
),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'(abusefilter-import-invalid-data'
,
$html
,
'An unknown filter ID should cause an error message.'
);
$this
->
assertStringContainsString
(
'(abusefilter-return'
,
$html
,
'Button to return the filter management was missing.'
);
}
/** @dataProvider provideViewEditForBadFilter */
public
function
testViewEditForBadFilter
(
$subPage
)
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
$subPage
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'(abusefilter-edit-badfilter'
,
$html
,
'An unknown filter ID should cause an error message.'
);
$this
->
assertStringContainsString
(
'(abusefilter-return'
,
$html
,
'Button to return the filter management was missing.'
);
}
public
static
function
provideViewEditForBadFilter
()
{
return
[
'Unknown filter ID'
=>
[
'12345'
],
'Unknown history ID for existing filter'
=>
[
'history/1/item/123456'
],
];
}
public
function
testViewEditProtectedVarsCheckboxPresentForProtectedFilter
()
{
// Xml::buildForm uses the global wfMessage which means we need to set
// the language for the user globally too.
$this
->
setUserLang
(
'qqx'
);
[
$html
,
]
=
$this
->
executeSpecialPage
(
'1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCanUseProtectedVar
);
$this
->
assertStringNotContainsString
(
'abusefilter-edit-protected-help-message'
,
$html
,
'The enabled checkbox to protect the filter was not present.'
);
$this
->
assertStringContainsString
(
'abusefilter-edit-protected-variable-already-protected'
,
$html
,
'The disabled checkbox explaining that the filter is protected was not present.'
);
// Also check that the filter hit count is present and as expected for the protected filter.
$this
->
assertStringContainsString
(
'(abusefilter-edit-hitcount'
,
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-hitcount: 1'
,
$html
);
}
public
function
testViewEditForProtectedFilterWhenUserLacksAuthority
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'(abusefilter-edit-denied-protected-vars'
,
$html
,
'The protected filter permission error was not present.'
);
}
public
function
testViewEditProtectedVarsCheckboxAbsentForUnprotectedFilter
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'2'
,
new
FauxRequest
(),
null
,
$this
->
authorityCanUseProtectedVar
);
$this
->
assertStringNotContainsString
(
'abusefilter-edit-protected'
,
$html
,
'Elements related to protected filters were present.'
);
}
public
function
testViewEditProtectedVarsSave
()
{
$authority
=
$this
->
authorityCanUseProtectedVar
;
$user
=
$this
->
getServiceContainer
()->
getUserFactory
()->
newFromUserIdentity
(
$authority
->
getUser
()
);
// Set the abuse filter editor to the context user, so that the edit token matches
RequestContext
::
getMain
()->
getRequest
()->
getSession
()->
setUser
(
$user
);
[
$html
,
]
=
$this
->
executeSpecialPage
(
'new'
,
new
FauxRequest
(
[
'wpFilterDescription'
=>
'Uses protected variable'
,
'wpFilterRules'
=>
'user_unnamed_ip = "4.2.3.4"'
,
'wpFilterNotes'
=>
''
,
'wpEditToken'
=>
$user
->
getEditToken
(
[
'abusefilter'
,
'new'
]
),
],
// This was posted
true
,
RequestContext
::
getMain
()->
getRequest
()->
getSession
()
),
null
,
$authority
);
$this
->
assertStringContainsString
(
'abusefilter-edit-protected-variable-not-protected'
,
$html
,
'The error message about protecting the filter was not present.'
);
$this
->
assertStringContainsString
(
'abusefilter-edit-protected-help-message'
,
$html
,
'The enabled checkbox to protect the filter was not present.'
);
}
public
function
testViewEditProtectedVarsSaveSuccess
()
{
$authority
=
$this
->
authorityCanUseProtectedVar
;
$user
=
$this
->
getServiceContainer
()->
getUserFactory
()->
newFromUserIdentity
(
$authority
->
getUser
()
);
// Set the abuse filter editor to the context user, so that the edit token matches
RequestContext
::
getMain
()->
getRequest
()->
getSession
()->
setUser
(
$user
);
[
$html
,
$response
]
=
$this
->
executeSpecialPage
(
'new'
,
new
FauxRequest
(
[
'wpFilterDescription'
=>
'Uses protected variable'
,
'wpFilterRules'
=>
'user_unnamed_ip = "4.2.3.4"'
,
'wpFilterProtected'
=>
'1'
,
'wpFilterNotes'
=>
''
,
'wpEditToken'
=>
$user
->
getEditToken
(
[
'abusefilter'
,
'new'
]
),
],
// This was posted
true
,
RequestContext
::
getMain
()->
getRequest
()->
getSession
()
),
null
,
$authority
);
// On saving successfully, the page redirects
$this
->
assertSame
(
''
,
$html
);
$this
->
assertStringContainsString
(
'result=success'
,
$response
->
getHeader
(
'location'
)
);
}
public
function
testViewTestBatchProtectedVarsFilterVisibility
()
{
// Assert that the user who cannot see protected variables cannot load the filter
[
$html
,
]
=
$this
->
executeSpecialPage
(
'test/1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringNotContainsString
(
'1.2.3.4'
,
$html
);
// Assert that the user who can see protected variables can load the filter
[
$html
,
]
=
$this
->
executeSpecialPage
(
'test/1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCanUseProtectedVar
);
$this
->
assertStringContainsString
(
'1.2.3.4'
,
$html
);
}
public
function
testViewTestBatchWhenSubmittedForProtectedFilter
()
{
$this
->
addCustomProtectedVariableToGenericVars
();
// Assert that the user who can see protected variables can submit the form for a protected filter
// and that this submission causes protected variable access logs to be created
[
$html
,
]
=
$this
->
executeSpecialPage
(
'test'
,
new
FauxRequest
(
[
'wpFilterRules'
=>
"custom_variable = 'custom_variable_value'"
,
'wpTestAction'
=>
0
,
'wpTestUser'
=>
''
,
'wpTestPeriodStart'
=>
''
,
'wpTestPeriodEnd'
=>
''
,
'wpTestPage'
=>
''
,
'wpShowNegative'
=>
1
,
],
true
),
null
,
$this
->
authorityCanUseProtectedVar
);
$this
->
assertStringContainsString
(
'custom_variable_value'
,
$html
);
// Verify that a protected variable access log was created as protected variable values were viewed.
$this
->
verifyProtectedVariableAccessLogExists
(
$this
->
authorityCanUseProtectedVar
->
getUser
(),
self
::
$userWhoHitFilter
,
[
'custom_variable'
]
);
}
private
function
verifyProtectedVariableAccessLogExists
(
UserIdentity
$performer
,
string
$target
,
array
$variablesViewed
):
void
{
$result
=
$this
->
newSelectQueryBuilder
()
->
select
(
'log_params'
)
->
from
(
'logging'
)
->
join
(
'actor'
,
null
,
'actor_id=log_actor'
)
->
where
(
[
'log_action'
=>
'view-protected-var-value'
,
'log_type'
=>
ProtectedVarsAccessLogger
::
LOG_TYPE
,
'actor_name'
=>
$performer
->
getName
(),
'log_title'
=>
Title
::
newFromText
(
$target
)->
getDBkey
(),
'log_namespace'
=>
NS_USER
,
]
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
$this
->
assertSame
(
1
,
$result
->
numRows
()
);
$result
->
rewind
();
$this
->
assertArrayEquals
(
[
'variables'
=>
$variablesViewed
],
LogEntryBase
::
extractParams
(
$result
->
fetchRow
()[
'log_params'
]
),
false
,
true
);
}
/**
* Common test code used by tests which load the list of AbuseFilters,
* used to verify that the headings on the table of AbuseFilters are
* as expected.
*
* @param string $html The HTML of the special page
* @param Authority $authority The Authority who viewed the special page
* @param bool $searchModeEnabled Whether the special page request included searching
* for filters with a specific substring in their pattern.
*/
private
function
verifyViewListHeadingsPresent
(
string
$html
,
Authority
$authority
,
bool
$searchModeEnabled
=
false
)
{
$tableHtml
=
$this
->
assertAndGetByElementClass
(
$html
,
'mw-datatable'
);
$expectedTableHeadings
=
[
'abusefilter-list-id'
,
'abusefilter-list-public'
,
'abusefilter-list-consequences'
,
'abusefilter-list-status'
,
'abusefilter-list-lastmodified'
,
'abusefilter-list-visibility'
,
];
$expectedTableHeadingsToBeMissing
=
[];
if
(
$authority
->
isAllowed
(
'abusefilter-log-detail'
)
)
{
$expectedTableHeadings
[]
=
'abusefilter-list-hitcount'
;
}
else
{
$expectedTableHeadingsToBeMissing
[]
=
'abusefilter-list-hitcount'
;
}
$canViewPrivateFilters
=
$this
->
getServiceContainer
()->
get
(
AbuseFilterPermissionManager
::
SERVICE_NAME
)
->
canViewPrivateFilters
(
$authority
);
if
(
$canViewPrivateFilters
&&
$searchModeEnabled
)
{
$expectedTableHeadings
[]
=
'abusefilter-list-pattern'
;
}
else
{
$expectedTableHeadingsToBeMissing
[]
=
'abusefilter-list-pattern'
;
}
foreach
(
$expectedTableHeadings
as
$heading
)
{
$this
->
assertStringContainsString
(
$heading
,
$tableHtml
);
}
foreach
(
$expectedTableHeadingsToBeMissing
as
$heading
)
{
$this
->
assertStringNotContainsString
(
$heading
,
$tableHtml
);
}
}
/**
* Calls DOMCompat::querySelectorAll, expects that it returns one valid Element object and then returns
* the HTML inside that Element.
*
* @param string $html The HTML to search through
* @param string $class The CSS class to search for, excluding the "." character
* @return string The HTML inside the given class
*/
private
function
assertAndGetByElementClass
(
string
$html
,
string
$class
):
string
{
$specialPageDocument
=
DOMUtils
::
parseHTML
(
$html
);
$element
=
DOMCompat
::
querySelectorAll
(
$specialPageDocument
,
'.'
.
$class
);
$this
->
assertCount
(
1
,
$element
,
"Could not find only one element with CSS class $class in $html"
);
return
DOMCompat
::
getInnerHTML
(
$element
[
0
]
);
}
public
function
testViewListWhenLimitIsOne
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
''
,
new
FauxRequest
(
[
'limit'
=>
1
]
),
null
,
$this
->
authorityCanUseProtectedVar
);
// Verify the structure of one row in the table, ensuring the correct flags are set.
$this
->
verifyViewListHeadingsPresent
(
$html
,
$this
->
authorityCanUseProtectedVar
);
$this
->
assertStringContainsString
(
'AbuseFilter/1'
,
$this
->
assertAndGetByElementClass
(
$html
,
'TablePager_col_af_id'
),
'Missing the URL to the filter'
);
$this
->
assertStringContainsString
(
'Filter with protected variables'
,
$this
->
assertAndGetByElementClass
(
$html
,
'TablePager_col_af_public_comments'
)
);
$cellClassesToExpectedText
=
[
'TablePager_col_af_actions'
=>
'(abusefilter-action-tags)'
,
'TablePager_col_af_enabled'
=>
'(abusefilter-enabled)'
,
'TablePager_col_af_hidden'
=>
'(abusefilter-protected)'
,
];
foreach
(
$cellClassesToExpectedText
as
$class
=>
$expectedText
)
{
$this
->
assertSame
(
$expectedText
,
$this
->
assertAndGetByElementClass
(
$html
,
$class
)
);
}
$this
->
assertStringContainsString
(
'abusefilter-hitcount: 1'
,
$this
->
assertAndGetByElementClass
(
$html
,
'TablePager_col_af_hit_count'
)
);
$timestampCellHtml
=
$this
->
assertAndGetByElementClass
(
$html
,
'TablePager_col_af_timestamp'
);
$this
->
assertStringContainsString
(
'abusefilter-edit-lastmod-text'
,
$timestampCellHtml
);
$this
->
assertStringContainsString
(
'UTSysop'
,
$timestampCellHtml
,
'Missing last editor of filter'
);
}
public
function
testViewListProtectedVarsFilterVisibility
()
{
// Ensure that even if the user cannot view the details of a protected filter
// they can still see the filter in the filter list
[
$html
,
]
=
$this
->
executeSpecialPage
(
''
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'abusefilter-protected'
,
$html
);
$this
->
verifyViewListHeadingsPresent
(
$html
,
$this
->
authorityCannotUseProtectedVar
);
}
public
function
testViewListWithSearchQueryProtectedVarsFilterVisibility
()
{
// Stub out a page with query results for a filter that uses protected variables
// &sort=af_id&limit=50&asc=&desc=1&deletedfilters=hide&querypattern=user_unnamed_ip&searchoption=LIKE
$requestWithProtectedVar
=
new
FauxRequest
(
[
'sort'
=>
'af_id'
,
'limit'
=>
50
,
'asc'
=>
''
,
'desc'
=>
1
,
'deletedfilters'
=>
'hide'
,
'querypattern'
=>
'user_unnamed_ip = "1'
,
'searchoption'
=>
'LIKE'
,
'rulescope'
=>
'all'
,
'furtheroptions'
=>
[]
]
);
// Assert that the user who cannot see protected variables sees no filters when searching
[
$html
,
]
=
$this
->
executeSpecialPage
(
''
,
$requestWithProtectedVar
,
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'table_pager_empty'
,
$html
);
$this
->
verifyViewListHeadingsPresent
(
$html
,
$this
->
authorityCannotUseProtectedVar
,
true
);
// Assert that the user who can see protected variables sees the filter from the db
[
$html
,
]
=
$this
->
executeSpecialPage
(
''
,
$requestWithProtectedVar
,
null
,
$this
->
authorityCanUseProtectedVar
);
$this
->
assertStringContainsString
(
'Filter with protected variables'
,
$html
);
$this
->
verifyViewListHeadingsPresent
(
$html
,
$this
->
authorityCanUseProtectedVar
,
true
);
// Check that the search found one result and that the pattern is bolded to show the text match
$patternCellHtml
=
$this
->
assertAndGetByElementClass
(
$html
,
'TablePager_col_af_pattern'
);
$this
->
assertSame
(
'<b>user_unnamed_ip = "1</b>.2.3.4"'
,
$patternCellHtml
);
}
public
function
testViewHistoryForProtectedFilterWhenUserLacksAuthority
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'history/1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'(abusefilter-history-error-protected)'
,
$html
,
'The protected filter permission error was not present.'
);
$this
->
assertStringNotContainsString
(
'abusefilter-history-select-user'
,
$html
,
'The filter history should not be shown if the user cannot see the filter.'
);
}
/**
* Common test code used by tests which load the history of AbuseFilter filters,
* used to verify that the headings on the table on the page is as expected
*
* @param string $html The HTML of the special page
*/
private
function
verifyHistoryHeadingsPresent
(
string
$html
)
{
$tableHtml
=
$this
->
assertAndGetByElementClass
(
$html
,
'mw-datatable'
);
$expectedTableHeadings
=
[
'abusefilter-history-timestamp'
,
'abusefilter-history-user'
,
'abusefilter-history-public'
,
'abusefilter-history-flags'
,
'abusefilter-history-actions'
,
'abusefilter-history-diff'
,
'abusefilter-history-timestamp'
,
];
foreach
(
$expectedTableHeadings
as
$heading
)
{
$this
->
assertStringContainsString
(
$heading
,
$tableHtml
);
}
}
/**
* Common test code used by tests which load the history of AbuseFilter filters,
* used to verify that the search form shown on the page has the expected fields.
*
* @param string $html The HTML of the special page
*/
private
function
verifyHistorySearchFormFields
(
string
$html
)
{
$this
->
assertStringContainsString
(
'(abusefilter-history-select-user'
,
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-history-select-filter'
,
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-history-select-submit'
,
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-history-select-legend'
,
$html
);
}
public
function
testViewHistoryWhenFilteringForSpecificFilter
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'history/1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCanUseProtectedVar
);
// Verify the structure of the form fields and items near the form.
$this
->
verifyHistorySearchFormFields
(
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-history-backedit)'
,
$html
);
// Verify the structure of the table
$this
->
verifyHistoryHeadingsPresent
(
$html
);
// Get the HTML for the most recent edit to the filter we are filtering for
$specialPageDocument
=
DOMUtils
::
parseHTML
(
$html
);
$element
=
DOMCompat
::
querySelectorAll
(
$specialPageDocument
,
'.mw-abusefilter-history-id-3'
);
$this
->
assertNotCount
(
0
,
$element
,
"Could not find most recent edit in $html"
);
$rowHtml
=
Html
::
rawElement
(
'table'
,
[],
DOMCompat
::
getInnerHTML
(
$element
[
0
]
)
);
// Verify the structure of the row we have found
$this
->
assertStringContainsString
(
'UTSysop'
,
$this
->
assertAndGetByElementClass
(
$rowHtml
,
'TablePager_col_afh_user_text'
),
"Missing editor of the version of the filter in $rowHtml"
);
$this
->
assertStringContainsString
(
'Filter with protected variables'
,
$this
->
assertAndGetByElementClass
(
$rowHtml
,
'TablePager_col_afh_public_comments'
),
"Missing name of filter in $rowHtml"
);
$this
->
assertSame
(
$this
->
getServiceContainer
()->
get
(
SpecsFormatter
::
SERVICE_NAME
)->
formatFlags
(
'protected,enabled'
,
$this
->
getServiceContainer
()->
getLanguageFactory
()->
getLanguage
(
'qqx'
)
),
$this
->
assertAndGetByElementClass
(
$rowHtml
,
'TablePager_col_afh_flags'
),
"Unexpected flags on the version of the filter in $rowHtml"
);
$this
->
assertStringContainsString
(
'abusefilter-action-tags'
,
$this
->
assertAndGetByElementClass
(
$rowHtml
,
'TablePager_col_afh_actions'
),
"Unexpected actions on the version of the filter in $rowHtml"
);
$this
->
assertStringContainsString
(
'abusefilter-history-diff'
,
$this
->
assertAndGetByElementClass
(
$rowHtml
,
'TablePager_col_afh_id'
),
"Missing diff for the specific version of the filter in $rowHtml"
);
}
public
function
testViewHistoryHidesProtectedFiltersWhenUserLacksPermissions
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'history'
,
new
FauxRequest
(
[]
),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
verifyHistorySearchFormFields
(
$html
);
$this
->
verifyHistoryHeadingsPresent
(
$html
);
// Verify that the only filter versions shown is the one without protected variables, including
// versions of the filter which is now protected.
$this
->
assertStringNotContainsString
(
'Filter with protected variables'
,
$html
);
$this
->
assertStringNotContainsString
(
'Filter to be converted'
,
$html
);
$this
->
assertStringContainsString
(
'Filter without protected variables'
,
$html
);
}
/** @dataProvider provideViewDiffWhenDiffInvalid */
public
function
testViewDiffWhenDiffInvalid
(
$subPage
)
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
$subPage
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'(abusefilter-diff-invalid)'
,
$html
);
}
public
static
function
provideViewDiffWhenDiffInvalid
()
{
return
[
'Filter ID is not numeric'
=>
[
'history/abc/diff/prev/1'
],
'Version IDs do not exist'
=>
[
'history/1/diff/prev/123456'
],
];
}
/** @dataProvider provideViewDiffForProtectedFilterWhenUserLacksAuthority */
public
function
testViewDiffForProtectedFilterWhenUserLacksAuthority
(
$subPage
)
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
$subPage
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
assertStringContainsString
(
'(abusefilter-history-error-protected)'
,
$html
,
'The protected filter permission error was not present.'
);
}
public
static
function
provideViewDiffForProtectedFilterWhenUserLacksAuthority
()
{
return
[
'Diff between version which was not protected and a version which is protected'
=>
[
'history/1/diff/next/1'
],
'Diff between two protected versions of the filter'
=>
[
'history/1/diff/3/prev'
],
];
}
private
function
verifyHasExamineIntroMessage
(
string
$html
)
{
$this
->
assertStringContainsString
(
'(abusefilter-examine-intro'
,
$html
,
'Missing examine explainer message'
);
}
public
function
testViewExamineForLogEntryWithMissingId
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'examine/log/1234'
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
verifyHasExamineIntroMessage
(
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-examine-notfound)'
,
$html
,
'Missing error message for unknown AbuseLog ID.'
);
}
public
function
testViewExamineForLogEntryWhereUserCannotSeeTheFilter
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'examine/log/1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
verifyHasExamineIntroMessage
(
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-log-cannot-see-details)'
,
$html
,
'Missing protected filter access error.'
);
}
public
function
testViewExamineForLogEntryWhereUserCannotSeeSpecificProtectedVariable
()
{
// Mock that all users lack access to user_unnamed_ip only, so we can test denying access based on the
// protected variables that are present in the log.
$this
->
setTemporaryHook
(
'AbuseFilterCanViewProtectedVariables'
,
static
function
(
Authority
$performer
,
array
$variables
,
AbuseFilterPermissionStatus
$returnStatus
)
{
if
(
in_array
(
'user_unnamed_ip'
,
$variables
)
)
{
$returnStatus
->
fatal
(
'test'
);
}
}
);
[
$html
,
]
=
$this
->
executeSpecialPage
(
'examine/log/1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCanUseProtectedVar
);
$this
->
verifyHasExamineIntroMessage
(
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-examine-protected-vars-permission)'
,
$html
,
'Missing protected filter access error.'
);
}
public
function
testViewExamineForLogEntryWhenFilterIsGlobalAndGlobalFiltersHaveBeenDisabled
()
{
// Mock FilterLookup::getFilter to throw a CentralDBNotAvailableException exception
$mockFilterLookup
=
$this
->
createMock
(
FilterLookup
::
class
);
$mockFilterLookup
->
method
(
'getFilter'
)
->
willThrowException
(
new
CentralDBNotAvailableException
()
);
$this
->
setService
(
'AbuseFilterFilterLookup'
,
$mockFilterLookup
);
[
$html
,
]
=
$this
->
executeSpecialPage
(
'examine/log/1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
// Verify that even though the Filter details could not be fetched, the filter is still considered
// protected (to assume the most strict restrictions).
$this
->
verifyHasExamineIntroMessage
(
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-log-cannot-see-details)'
,
$html
,
'Missing protected filter access error.'
);
}
public
function
testViewExamineForLogEntryWhenUserCanSeeLog
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'examine/log/1'
,
new
FauxRequest
(),
null
,
$this
->
authorityCanUseProtectedVar
);
DeferredUpdates
::
doUpdates
();
$this
->
verifyHasExamineIntroMessage
(
$html
);
// Check that the test tools elements are loaded
$this
->
assertStringContainsString
(
'(abusefilter-examine-test'
,
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-examine-test-button'
,
$html
);
// Verify that the examiner for the log entry is displayed by checking that the user_unnamed_ip
// variable value is present.
$this
->
assertStringContainsString
(
'(abusefilter-examine-vars'
,
$html
);
$abuseLogDetailsTableHtml
=
$this
->
assertAndGetByElementClass
(
$html
,
'mw-abuselog-details'
);
$this
->
assertStringContainsString
(
'1.2.3.4'
,
$abuseLogDetailsTableHtml
);
// Verify that a protected variable access log was created as protected variable values were viewed.
$this
->
verifyProtectedVariableAccessLogExists
(
$this
->
authorityCanUseProtectedVar
->
getUser
(),
self
::
$userWhoHitFilter
,
[
'user_unnamed_ip'
]
);
}
public
function
testViewExamineForRecentChangeWithMissingId
()
{
[
$html
,
]
=
$this
->
executeSpecialPage
(
'examine/1234'
,
new
FauxRequest
(),
null
,
$this
->
authorityCannotUseProtectedVar
);
$this
->
verifyHasExamineIntroMessage
(
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-examine-notfound)'
,
$html
,
'Missing error message for unknown AbuseLog ID.'
);
}
private
function
addCustomProtectedVariableToGenericVars
()
{
$this
->
setTemporaryHook
(
'AbuseFilterCustomProtectedVariables'
,
static
function
(
&
$variables
)
{
$variables
[]
=
'custom_variable'
;
}
);
$this
->
setTemporaryHook
(
'AbuseFilter-builder'
,
static
function
(
array
&
$realValues
)
{
$realValues
[
'vars'
][
'custom_variable'
]
=
'custom-variable-test'
;
}
);
$this
->
setTemporaryHook
(
'AbuseFilter-generateGenericVars'
,
static
function
(
VariableHolder
$vars
)
{
$vars
->
setVar
(
'custom_variable'
,
'custom_variable_value'
);
}
);
$this
->
resetServices
();
}
public
function
testViewExamineForRecentChangeWhereUserCannotSeeSpecificProtectedVariableDueToPermission
()
{
// Mock that all users lack access to the 'custom_variable' variable due to it being a protected variable.
$this
->
addCustomProtectedVariableToGenericVars
();
$this
->
setTemporaryHook
(
'AbuseFilterCanViewProtectedVariables'
,
static
function
(
Authority
$performer
,
array
$variables
,
AbuseFilterPermissionStatus
$returnStatus
)
{
if
(
in_array
(
'custom_variable'
,
$variables
)
)
{
$returnStatus
->
setPermission
(
'test-permission'
);
}
}
);
[
$html
,
]
=
$this
->
executeSpecialPage
(
'examine/'
.
self
::
$recentChangeId
,
null
,
null
,
$this
->
authorityCanUseProtectedVar
);
$this
->
verifyHasExamineIntroMessage
(
$html
);
$this
->
assertStringNotContainsString
(
'mw-abuselog-details-custom_variable'
,
$html
,
'The "custom_variable" variable was not unset, but it should '
.
'have been because the user cannot see it.'
);
}
public
function
testViewExamineForRecentChangeWhenUserCanSeeRecentChange
()
{
$this
->
addCustomProtectedVariableToGenericVars
();
[
$html
,
]
=
$this
->
executeSpecialPage
(
'examine/'
.
self
::
$recentChangeId
,
null
,
null
,
$this
->
authorityCanUseProtectedVar
);
DeferredUpdates
::
doUpdates
();
$this
->
verifyHasExamineIntroMessage
(
$html
);
// Check that the test tools elements are loaded
$this
->
assertStringContainsString
(
'(abusefilter-examine-test'
,
$html
);
$this
->
assertStringContainsString
(
'(abusefilter-examine-test-button'
,
$html
);
// Verify that the custom_variable variable is shown with it's value.
$customVariableTableRow
=
$this
->
assertAndGetByElementClass
(
$html
,
'mw-abuselog-details-custom_variable'
);
$this
->
assertStringContainsString
(
'custom_variable_value'
,
$customVariableTableRow
);
// Verify that a lazily loaded non-protected variable is shown (regression testing for T403645)
$userTypeVariableTableRow
=
$this
->
assertAndGetByElementClass
(
$html
,
'mw-abuselog-details-user_type'
);
$this
->
assertStringContainsString
(
'user_type'
,
$userTypeVariableTableRow
);
// Verify that a protected variable access log was created as protected variable values were viewed.
$this
->
verifyProtectedVariableAccessLogExists
(
$this
->
authorityCanUseProtectedVar
->
getUser
(),
static
::
$userWhoHitFilter
,
[
'custom_variable'
]
);
}
/**
* Drops the 'view-protected-var-value' logs from the 'logging' table.
*
* This is needed because in {@link self::addDBDataOnce} we added rows to the 'logging' table and so the table is
* not reset between tests.
*
* @return void
*/
private
function
dropProtectedVarAccessLogs
():
void
{
$this
->
getDb
()->
newDeleteQueryBuilder
()
->
deleteFrom
(
'logging'
)
->
where
(
[
'log_action'
=>
'view-protected-var-value'
,
'log_type'
=>
ProtectedVarsAccessLogger
::
LOG_TYPE
,
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 16 2026, 04:21 (12 w, 1 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
17/b8/68db32a1baa20eb2878bccc546ba
Default Alt Text
SpecialAbuseFilterTest.php (37 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment