Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F6037050
AbuseFilterPager.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
AbuseFilterPager.php
View Options
<?php
namespace
MediaWiki\Extension\AbuseFilter\Pager
;
use
LogicException
;
use
MediaWiki\Cache\LinkBatchFactory
;
use
MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager
;
use
MediaWiki\Extension\AbuseFilter\FilterLookup
;
use
MediaWiki\Extension\AbuseFilter\FilterUtils
;
use
MediaWiki\Extension\AbuseFilter\SpecsFormatter
;
use
MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewList
;
use
MediaWiki\Linker\Linker
;
use
MediaWiki\Linker\LinkRenderer
;
use
MediaWiki\Pager\TablePager
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\User\UserIdentityValue
;
use
stdClass
;
use
UnexpectedValueException
;
use
Wikimedia\Rdbms\FakeResultWrapper
;
use
Wikimedia\Rdbms\IResultWrapper
;
/**
* Class to build paginated filter list
*/
class
AbuseFilterPager
extends
TablePager
{
/**
* The unique sort fields for the sort options for unique paginate
*/
private
const
INDEX_FIELDS
=
[
'af_id'
=>
[
'af_id'
],
'af_enabled'
=>
[
'af_enabled'
,
'af_deleted'
,
'af_id'
],
'af_timestamp'
=>
[
'af_timestamp'
,
'af_id'
],
'af_hidden'
=>
[
'af_hidden'
,
'af_id'
],
'af_group'
=>
[
'af_group'
,
'af_id'
],
'af_hit_count'
=>
[
'af_hit_count'
,
'af_id'
],
'af_public_comments'
=>
[
'af_public_comments'
,
'af_id'
],
];
private
?
LinkBatchFactory
$linkBatchFactory
;
private
AbuseFilterPermissionManager
$afPermManager
;
private
FilterLookup
$filterLookup
;
protected
SpecsFormatter
$specsFormatter
;
/**
* @var AbuseFilterViewList The associated page
*/
private
$mPage
;
/**
* @var array Query WHERE conditions
*/
private
$conds
;
/**
* @var string|null The pattern being searched
*/
private
$searchPattern
;
/**
* @var string|null The pattern search mode (LIKE, RLIKE or IRLIKE)
*/
private
$searchMode
;
/**
* @param AbuseFilterViewList $page
* @param LinkRenderer $linkRenderer
* @param ?LinkBatchFactory $linkBatchFactory
* @param AbuseFilterPermissionManager $afPermManager
* @param SpecsFormatter $specsFormatter
* @param FilterLookup $filterLookup
* @param array $conds
* @param ?string $searchPattern Null if no pattern was specified
* @param ?string $searchMode
*/
public
function
__construct
(
AbuseFilterViewList
$page
,
LinkRenderer
$linkRenderer
,
?
LinkBatchFactory
$linkBatchFactory
,
AbuseFilterPermissionManager
$afPermManager
,
SpecsFormatter
$specsFormatter
,
FilterLookup
$filterLookup
,
array
$conds
,
?
string
$searchPattern
,
?
string
$searchMode
)
{
// needed by parent's constructor call
$this
->
afPermManager
=
$afPermManager
;
$this
->
specsFormatter
=
$specsFormatter
;
parent
::
__construct
(
$page
->
getContext
(),
$linkRenderer
);
$this
->
mPage
=
$page
;
$this
->
linkBatchFactory
=
$linkBatchFactory
;
$this
->
filterLookup
=
$filterLookup
;
$this
->
conds
=
$conds
;
$this
->
searchPattern
=
$searchPattern
;
$this
->
searchMode
=
$searchMode
;
}
/**
* @return array
*/
public
function
getQueryInfo
()
{
return
$this
->
filterLookup
->
getAbuseFilterQueryBuilder
(
$this
->
getDatabase
()
)
->
andWhere
(
$this
->
conds
)
->
getQueryInfo
();
}
/**
* @param IResultWrapper $result
*/
protected
function
preprocessResults
(
$result
)
{
// LinkBatchFactory only provided and needed for local wiki results
if
(
$this
->
linkBatchFactory
===
null
||
$this
->
getNumRows
()
===
0
)
{
return
;
}
$lb
=
$this
->
linkBatchFactory
->
newLinkBatch
();
$lb
->
setCaller
(
__METHOD__
);
foreach
(
$result
as
$row
)
{
$lb
->
addUser
(
new
UserIdentityValue
(
$row
->
af_user
??
0
,
$row
->
af_user_text
)
);
}
$lb
->
execute
();
$result
->
seek
(
0
);
}
/**
* @inheritDoc
* This is the same as the parent implementation if no search pattern was specified.
* Otherwise, it does a query with no limit and then slices the results à la ContribsPager.
*/
public
function
reallyDoQuery
(
$offset
,
$limit
,
$order
)
{
if
(
$this
->
searchMode
===
null
)
{
return
parent
::
reallyDoQuery
(
$offset
,
$limit
,
$order
);
}
[
$tables
,
$fields
,
$conds
,
$fname
,
$options
,
$join_conds
]
=
$this
->
buildQueryInfo
(
$offset
,
$limit
,
$order
);
unset
(
$options
[
'LIMIT'
]
);
$res
=
$this
->
mDb
->
newSelectQueryBuilder
()
->
tables
(
$tables
)
->
fields
(
$fields
)
->
conds
(
$conds
)
->
caller
(
$fname
)
->
options
(
$options
)
->
joinConds
(
$join_conds
)
->
fetchResultSet
();
$filtered
=
[];
foreach
(
$res
as
$row
)
{
// FilterLookup::filterFromRow $actions is either an array or callable. We want to provide
// an empty array, as we don't need to use actions for this code. Phan detects an empty callable,
// which is a bug.
// @phan-suppress-next-line PhanTypeInvalidCallableArraySize
$filter
=
$this
->
filterLookup
->
filterFromRow
(
$row
,
[]
);
// Exclude filters from the search result which include variables the user cannot see to avoid
// exposing the pattern when the user cannot see the filter.
if
(
$filter
->
isProtected
()
&&
!
$this
->
afPermManager
->
canViewProtectedVariablesInFilter
(
$this
->
getAuthority
(),
$filter
)->
isGood
()
)
{
continue
;
}
if
(
$this
->
matchesPattern
(
$filter
->
getRules
()
)
)
{
$filtered
[
$filter
->
getID
()]
=
$row
;
}
}
// sort results and enforce limit like ContribsPager
if
(
$order
===
self
::
QUERY_ASCENDING
)
{
ksort
(
$filtered
);
}
else
{
krsort
(
$filtered
);
}
$filtered
=
array_slice
(
$filtered
,
0
,
$limit
);
// Phan false positive: FakeResultWrapper requires sequential indexes starting at 0
// @phan-suppress-next-line PhanRedundantArrayValuesCall
$filtered
=
array_values
(
$filtered
);
return
new
FakeResultWrapper
(
$filtered
);
}
/**
* Check whether $subject matches the given $pattern.
*
* @param string $subject
* @return bool
* @throws LogicException
*/
private
function
matchesPattern
(
$subject
)
{
$pattern
=
$this
->
searchPattern
;
switch
(
$this
->
searchMode
)
{
case
'RLIKE'
:
return
(
bool
)
preg_match
(
"/$pattern/u"
,
$subject
);
case
'IRLIKE'
:
return
(
bool
)
preg_match
(
"/$pattern/ui"
,
$subject
);
case
'LIKE'
:
return
mb_stripos
(
$subject
,
$pattern
)
!==
false
;
default
:
throw
new
LogicException
(
"Unknown search type {$this->searchMode}"
);
}
}
/**
* Note: this method is called by parent::__construct
* @return array
* @see MediaWiki\Pager\Pager::getFieldNames()
*/
public
function
getFieldNames
()
{
$headers
=
[
'af_id'
=>
'abusefilter-list-id'
,
'af_public_comments'
=>
'abusefilter-list-public'
,
'af_actions'
=>
'abusefilter-list-consequences'
,
'af_enabled'
=>
'abusefilter-list-status'
,
'af_timestamp'
=>
'abusefilter-list-lastmodified'
,
'af_hidden'
=>
'abusefilter-list-visibility'
,
];
$performer
=
$this
->
getAuthority
();
if
(
$this
->
afPermManager
->
canSeeLogDetails
(
$performer
)
)
{
$headers
[
'af_hit_count'
]
=
'abusefilter-list-hitcount'
;
}
if
(
$this
->
afPermManager
->
canViewPrivateFilters
(
$performer
)
&&
$this
->
searchMode
!==
null
)
{
// This is also excluded in the default view
$headers
[
'af_pattern'
]
=
'abusefilter-list-pattern'
;
}
if
(
count
(
$this
->
getConfig
()->
get
(
'AbuseFilterValidGroups'
)
)
>
1
)
{
$headers
[
'af_group'
]
=
'abusefilter-list-group'
;
}
foreach
(
$headers
as
&
$msg
)
{
$msg
=
$this
->
msg
(
$msg
)->
text
();
}
return
$headers
;
}
/**
* @param string $name
* @param string|null $value
* @return string
*/
public
function
formatValue
(
$name
,
$value
)
{
$lang
=
$this
->
getLanguage
();
$user
=
$this
->
getUser
();
$linkRenderer
=
$this
->
getLinkRenderer
();
$row
=
$this
->
mCurrentRow
;
switch
(
$name
)
{
case
'af_id'
:
return
$linkRenderer
->
makeLink
(
SpecialPage
::
getTitleFor
(
'AbuseFilter'
,
$value
),
$lang
->
formatNum
(
intval
(
$value
)
)
);
case
'af_pattern'
:
return
$this
->
getHighlightedPattern
(
$row
);
case
'af_public_comments'
:
return
$linkRenderer
->
makeLink
(
SpecialPage
::
getTitleFor
(
'AbuseFilter'
,
$row
->
af_id
),
$value
);
case
'af_actions'
:
$actions
=
explode
(
','
,
$value
);
$displayActions
=
[];
foreach
(
$actions
as
$action
)
{
$displayActions
[]
=
$this
->
specsFormatter
->
getActionDisplay
(
$action
);
}
return
$lang
->
commaList
(
$displayActions
);
case
'af_enabled'
:
$statuses
=
[];
if
(
$row
->
af_deleted
)
{
$statuses
[]
=
$this
->
msg
(
'abusefilter-deleted'
)->
parse
();
}
elseif
(
$row
->
af_enabled
)
{
$statuses
[]
=
$this
->
msg
(
'abusefilter-enabled'
)->
parse
();
if
(
$row
->
af_throttled
)
{
$statuses
[]
=
$this
->
msg
(
'abusefilter-throttled'
)->
parse
();
}
}
else
{
$statuses
[]
=
$this
->
msg
(
'abusefilter-disabled'
)->
parse
();
}
if
(
$row
->
af_global
&&
$this
->
getConfig
()->
get
(
'AbuseFilterIsCentral'
)
)
{
$statuses
[]
=
$this
->
msg
(
'abusefilter-status-global'
)->
parse
();
}
return
$lang
->
commaList
(
$statuses
);
case
'af_hidden'
:
$flagMsgs
=
[];
if
(
FilterUtils
::
isHidden
(
(
int
)
$value
)
)
{
$flagMsgs
[]
=
$this
->
msg
(
'abusefilter-hidden'
)->
parse
();
}
if
(
FilterUtils
::
isProtected
(
(
int
)
$value
)
)
{
$flagMsgs
[]
=
$this
->
msg
(
'abusefilter-protected'
)->
parse
();
}
if
(
!
$flagMsgs
)
{
return
$this
->
msg
(
'abusefilter-unhidden'
)->
parse
();
}
return
$lang
->
commaList
(
$flagMsgs
);
case
'af_hit_count'
:
// FilterLookup::filterFromRow $actions is either an array or callable. We want to provide
// an empty array, as we don't need to use actions for this code.
// @phan-suppress-next-line PhanTypeInvalidCallableArraySize
$filter
=
$this
->
filterLookup
->
filterFromRow
(
$row
,
[]
);
if
(
$this
->
afPermManager
->
canSeeLogDetailsForFilter
(
$this
->
getAuthority
(),
$filter
)
)
{
$count_display
=
$this
->
msg
(
'abusefilter-hitcount'
)
->
numParams
(
$value
)->
text
();
$link
=
$linkRenderer
->
makeKnownLink
(
SpecialPage
::
getTitleFor
(
'AbuseLog'
),
$count_display
,
[],
[
'wpSearchFilter'
=>
$row
->
af_id
]
);
}
else
{
$link
=
""
;
}
return
$link
;
case
'af_timestamp'
:
$userLink
=
Linker
::
userLink
(
$row
->
af_user
??
0
,
$row
->
af_user_text
)
.
Linker
::
userToolLinks
(
$row
->
af_user
??
0
,
$row
->
af_user_text
);
return
$this
->
msg
(
'abusefilter-edit-lastmod-text'
)
->
rawParams
(
$this
->
mPage
->
getLinkToLatestDiff
(
$row
->
af_id
,
$lang
->
userTimeAndDate
(
$value
,
$user
)
),
$userLink
,
$this
->
mPage
->
getLinkToLatestDiff
(
$row
->
af_id
,
$lang
->
userDate
(
$value
,
$user
)
),
$this
->
mPage
->
getLinkToLatestDiff
(
$row
->
af_id
,
$lang
->
userTime
(
$value
,
$user
)
)
)->
params
(
wfEscapeWikiText
(
$row
->
af_user_text
)
)->
parse
();
case
'af_group'
:
return
$this
->
specsFormatter
->
nameGroup
(
$value
);
default
:
throw
new
UnexpectedValueException
(
"Unknown row type $name!"
);
}
}
/**
* Get the filter pattern with <b> elements surrounding the searched pattern
*
* @param stdClass $row
* @return string
*/
private
function
getHighlightedPattern
(
stdClass
$row
)
{
if
(
$this
->
searchMode
===
null
)
{
throw
new
LogicException
(
'Cannot search without a mode.'
);
}
$maxLen
=
50
;
if
(
$this
->
searchMode
===
'LIKE'
)
{
$position
=
mb_stripos
(
$row
->
af_pattern
,
$this
->
searchPattern
);
$length
=
mb_strlen
(
$this
->
searchPattern
);
}
else
{
$regex
=
'/'
.
$this
->
searchPattern
.
'/u'
;
if
(
$this
->
searchMode
===
'IRLIKE'
)
{
$regex
.=
'i'
;
}
$matches
=
[];
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$check
=
@
preg_match
(
$regex
,
$row
->
af_pattern
,
$matches
);
// This may happen in case of catastrophic backtracking, or regexps matching
// the empty string.
if
(
$check
===
false
||
strlen
(
$matches
[
0
]
)
===
0
)
{
return
htmlspecialchars
(
mb_substr
(
$row
->
af_pattern
,
0
,
50
)
);
}
$length
=
mb_strlen
(
$matches
[
0
]
);
$position
=
mb_strpos
(
$row
->
af_pattern
,
$matches
[
0
]
);
}
$remaining
=
$maxLen
-
$length
;
if
(
$remaining
<=
0
)
{
$pattern
=
'<b>'
.
htmlspecialchars
(
mb_substr
(
$row
->
af_pattern
,
$position
,
$maxLen
)
)
.
'</b>'
;
}
else
{
// Center the snippet on the matched string
$minoffset
=
max
(
$position
-
round
(
$remaining
/
2
),
0
);
$pattern
=
mb_substr
(
$row
->
af_pattern
,
$minoffset
,
$maxLen
);
$pattern
=
htmlspecialchars
(
mb_substr
(
$pattern
,
0
,
$position
-
$minoffset
)
)
.
'<b>'
.
htmlspecialchars
(
mb_substr
(
$pattern
,
$position
-
$minoffset
,
$length
)
)
.
'</b>'
.
htmlspecialchars
(
mb_substr
(
$pattern
,
$position
-
$minoffset
+
$length
,
$remaining
-
(
$position
-
$minoffset
+
$length
)
)
);
}
return
$pattern
;
}
/**
* @codeCoverageIgnore Merely declarative
* @inheritDoc
*/
public
function
getDefaultSort
()
{
return
'af_id'
;
}
/**
* @codeCoverageIgnore Merely declarative
* @inheritDoc
*/
public
function
getTableClass
()
{
return
parent
::
getTableClass
()
.
' mw-abusefilter-list-scrollable'
;
}
/**
* @param stdClass $row
* @return string
* @see TablePager::getRowClass()
*/
public
function
getRowClass
(
$row
)
{
if
(
$row
->
af_enabled
)
{
return
$row
->
af_throttled
?
'mw-abusefilter-list-throttled'
:
'mw-abusefilter-list-enabled'
;
}
elseif
(
$row
->
af_deleted
)
{
return
'mw-abusefilter-list-deleted'
;
}
else
{
return
'mw-abusefilter-list-disabled'
;
}
}
/**
* @inheritDoc
*/
public
function
getIndexField
()
{
return
[
self
::
INDEX_FIELDS
[
$this
->
mSort
]
];
}
/**
* @param string $field
*
* @return bool
*/
public
function
isFieldSortable
(
$field
)
{
if
(
(
$field
===
'af_hit_count'
||
$field
===
'af_public_comments'
)
&&
!
$this
->
afPermManager
->
canSeeLogDetails
(
$this
->
getAuthority
()
)
)
{
return
false
;
}
return
isset
(
self
::
INDEX_FIELDS
[
$field
]
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Oct 4 2026, 19:37 (5 w, 6 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
5e/7b/a4daa9560217b7e560da61f3fa14
Default Alt Text
AbuseFilterPager.php (13 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment