Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5978370
AbstractCheckUserPager.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
26 KB
Referenced Files
None
Subscribers
None
AbstractCheckUserPager.php
View Options
<?php
namespace
MediaWiki\CheckUser\CheckUser\Pagers
;
use
HtmlArmor
;
use
LogicException
;
use
MediaWiki\Block\DatabaseBlock
;
use
MediaWiki\CheckUser\CheckUser\CheckUserPagerNavigationBuilder
;
use
MediaWiki\CheckUser\CheckUser\Widgets\HTMLFieldsetCheckUser
;
use
MediaWiki\CheckUser\CheckUserQueryInterface
;
use
MediaWiki\CheckUser\HookHandler\Preferences
;
use
MediaWiki\CheckUser\Services\CheckUserLogService
;
use
MediaWiki\CheckUser\Services\CheckUserLookupUtils
;
use
MediaWiki\CheckUser\Services\TokenQueryManager
;
use
MediaWiki\Context\IContextSource
;
use
MediaWiki\Context\RequestContext
;
use
MediaWiki\Extension\GlobalBlocking\GlobalBlockingServices
;
use
MediaWiki\Extension\TorBlock\TorExitNodes
;
use
MediaWiki\Html\FormOptions
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Html\TemplateParser
;
use
MediaWiki\Linker\LinkRenderer
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Navigation\PagerNavigationBuilder
;
use
MediaWiki\Pager\RangeChronologicalPager
;
use
MediaWiki\Registration\ExtensionRegistry
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\SpecialPage\SpecialPageFactory
;
use
MediaWiki\Title\Title
;
use
MediaWiki\Title\TitleValue
;
use
MediaWiki\User\CentralId\CentralIdLookup
;
use
MediaWiki\User\Options\UserOptionsLookup
;
use
MediaWiki\User\UserFactory
;
use
MediaWiki\User\UserGroupManager
;
use
MediaWiki\User\UserGroupMembership
;
use
MediaWiki\User\UserIdentity
;
use
MediaWiki\User\UserIdentityLookup
;
use
MediaWiki\Utils\MWTimestamp
;
use
stdClass
;
use
Wikimedia\Rdbms\FakeResultWrapper
;
use
Wikimedia\Rdbms\IConnectionProvider
;
use
Wikimedia\Rdbms\IResultWrapper
;
use
Wikimedia\Rdbms\SelectQueryBuilder
;
abstract
class
AbstractCheckUserPager
extends
RangeChronologicalPager
implements
CheckUserQueryInterface
{
/**
* Form fields that when paging should be set and managed
* by the token. Used so the client cannot generate results
* that do not match the original request which generated
* the associated CheckUserLog entry.
*/
public
const
TOKEN_MANAGED_FIELDS
=
[
'reason'
,
'checktype'
,
'period'
,
'dir'
,
'limit'
,
'offset'
,
];
/**
* Null if $target is a user.
* Boolean is $target is a IP / range.
* - False if XFF is not appended
* - True if XFF is appended
*
* @var null|bool
*/
protected
?
bool
$xfor
=
null
;
/** @var string The type of CheckUserLog entry this check should generate. */
private
string
$logType
;
/** @var FormOptions The submitted form data in a helper class */
protected
FormOptions
$opts
;
protected
UserIdentity
$target
;
/** @var bool Should Special:CheckUser display Client Hints data. */
protected
bool
$displayClientHints
;
/**
* @var string one of the SpecialCheckUser::SUBTYPE_... constants used by this abstract pager
* to know what the current checktype is.
*/
protected
string
$checkType
;
protected
UserGroupManager
$userGroupManager
;
protected
CentralIdLookup
$centralIdLookup
;
private
TokenQueryManager
$tokenQueryManager
;
private
SpecialPageFactory
$specialPageFactory
;
private
UserIdentityLookup
$userIdentityLookup
;
private
CheckUserLogService
$checkUserLogService
;
protected
TemplateParser
$templateParser
;
protected
UserFactory
$userFactory
;
protected
CheckUserLookupUtils
$checkUserLookupUtils
;
private
UserOptionsLookup
$userOptionsLookup
;
/**
* @param FormOptions $opts
* @param UserIdentity $target
* @param string $logType
* @param TokenQueryManager $tokenQueryManager
* @param UserGroupManager $userGroupManager
* @param CentralIdLookup $centralIdLookup
* @param IConnectionProvider $dbProvider
* @param SpecialPageFactory $specialPageFactory
* @param UserIdentityLookup $userIdentityLookup
* @param CheckUserLogService $checkUserLogService
* @param UserFactory $userFactory
* @param CheckUserLookupUtils $checkUserLookupUtils
* @param UserOptionsLookup $userOptionsLookup
* @param IContextSource|null $context
* @param LinkRenderer|null $linkRenderer
* @param ?int $limit
*/
public
function
__construct
(
FormOptions
$opts
,
UserIdentity
$target
,
string
$logType
,
TokenQueryManager
$tokenQueryManager
,
UserGroupManager
$userGroupManager
,
CentralIdLookup
$centralIdLookup
,
IConnectionProvider
$dbProvider
,
SpecialPageFactory
$specialPageFactory
,
UserIdentityLookup
$userIdentityLookup
,
CheckUserLogService
$checkUserLogService
,
UserFactory
$userFactory
,
CheckUserLookupUtils
$checkUserLookupUtils
,
UserOptionsLookup
$userOptionsLookup
,
?
IContextSource
$context
=
null
,
?
LinkRenderer
$linkRenderer
=
null
,
?
int
$limit
=
null
)
{
$this
->
opts
=
$opts
;
$this
->
target
=
$target
;
$this
->
logType
=
$logType
;
$this
->
mDb
=
$dbProvider
->
getReplicaDatabase
();
parent
::
__construct
(
$context
,
$linkRenderer
);
$maximumRowCount
=
$this
->
getConfig
()->
get
(
'CheckUserMaximumRowCount'
);
$this
->
mDefaultLimit
=
$limit
??
$maximumRowCount
;
if
(
$this
->
opts
->
getValue
(
'limit'
)
)
{
$this
->
mLimit
=
min
(
$this
->
opts
->
getValue
(
'limit'
),
$this
->
getConfig
()->
get
(
'CheckUserMaximumRowCount'
)
);
}
else
{
$this
->
mLimit
=
$maximumRowCount
;
}
$this
->
mLimitsShown
=
[
$maximumRowCount
/
25
,
$maximumRowCount
/
10
,
$maximumRowCount
/
5
,
$maximumRowCount
/
2
,
$maximumRowCount
,
];
$this
->
mLimitsShown
=
array_map
(
'ceil'
,
$this
->
mLimitsShown
);
$this
->
mLimitsShown
=
array_unique
(
$this
->
mLimitsShown
);
$this
->
displayClientHints
=
$this
->
getConfig
()->
get
(
'CheckUserDisplayClientHints'
);
$this
->
userGroupManager
=
$userGroupManager
;
$this
->
centralIdLookup
=
$centralIdLookup
;
$this
->
tokenQueryManager
=
$tokenQueryManager
;
$this
->
specialPageFactory
=
$specialPageFactory
;
$this
->
userIdentityLookup
=
$userIdentityLookup
;
$this
->
checkUserLogService
=
$checkUserLogService
;
$this
->
userFactory
=
$userFactory
;
$this
->
checkUserLookupUtils
=
$checkUserLookupUtils
;
$this
->
userOptionsLookup
=
$userOptionsLookup
;
$this
->
templateParser
=
new
TemplateParser
(
__DIR__
.
'/../../../templates'
);
// Get any set token data. Used for paging without adding extra logs
$tokenData
=
$this
->
tokenQueryManager
->
getDataFromRequest
(
$this
->
getRequest
()
);
if
(
!
$tokenData
)
{
// Log if the token data is not set. A token will only be generated by
// the server for CheckUser for paging links after running a check.
// It will also only be valid if not tampered with as it's encrypted.
// Paging through the entries won't need an extra log entry.
$this
->
checkUserLogService
->
addLogEntry
(
$this
->
getUser
(),
$this
->
logType
,
$target
->
getId
()
?
'user'
:
'ip'
,
$target
->
getName
(),
$this
->
opts
->
getValue
(
'reason'
),
$target
->
getId
()
);
}
$this
->
setPeriodCondition
();
}
/**
* Generate the cutoff timestamp condition for the query
*/
protected
function
setPeriodCondition
():
void
{
$period
=
$this
->
opts
->
getValue
(
'period'
);
if
(
$period
)
{
$cutoffTime
=
MWTimestamp
::
getInstance
();
$cutoffTime
->
timestamp
->
setTime
(
0
,
0
)->
modify
(
"-$period day"
);
// Call to RangeChronologicalPager::getDateRangeCond sets
// $this->startOffset to the $cutoffTime. The call does
// not set $this->endOffset as the empty string is provided.
$this
->
getDateRangeCond
(
$cutoffTime
->
getTimestamp
(),
''
);
}
}
/**
* Get formatted timestamp(s) to show the time of first and last change.
* If both timestamps are the same, it will be shown only once.
*
* @param string $first Timestamp of the first change
* @param string $last Timestamp of the last change
* @return string
*/
protected
function
getTimeRangeString
(
string
$first
,
string
$last
):
string
{
if
(
$first
===
$last
)
{
return
$this
->
getFormattedTimestamp
(
$first
);
}
else
{
return
$this
->
msg
(
'checkuser-time-range'
)
->
dateTimeParams
(
$first
,
$last
)
->
escaped
();
}
}
/**
* Get a link to block information about the passed block for displaying to the user.
*
* @param DatabaseBlock $block
* @return string
*/
protected
function
getBlockFlag
(
DatabaseBlock
$block
):
string
{
if
(
$block
->
getType
()
===
DatabaseBlock
::
TYPE_AUTO
)
{
$ret
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
SpecialPage
::
getTitleFor
(
'BlockList'
),
$this
->
msg
(
'checkuser-blocked'
)->
text
(),
[],
[
'wpTarget'
=>
"#{$block->getId()}"
]
);
}
else
{
$userPage
=
Title
::
makeTitle
(
NS_USER
,
$block
->
getTargetName
()
);
$ret
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
SpecialPage
::
getTitleFor
(
'Log'
),
$this
->
msg
(
'checkuser-blocked'
)->
text
(),
[],
[
'type'
=>
'block'
,
'page'
=>
$userPage
->
getPrefixedText
()
]
);
// Add the blocked range if the block is on a range
if
(
$block
->
getType
()
===
DatabaseBlock
::
TYPE_RANGE
)
{
$ret
.=
' - '
.
htmlspecialchars
(
$block
->
getTargetName
()
);
}
}
return
Html
::
rawElement
(
'strong'
,
[
'class'
=>
'mw-changeslist-links'
],
$ret
);
}
/**
* Get an HTML link (<a> element) to Special:CheckUser
*
* @param string $text content to use within <a> tag
* @param array $params query parameters to use in the URL
* @return string
*/
protected
function
getSelfLink
(
string
$text
,
array
$params
):
string
{
$title
=
$this
->
getTitleValue
();
return
$this
->
getLinkRenderer
()->
makeKnownLink
(
$title
,
new
HtmlArmor
(
'<bdi>'
.
htmlspecialchars
(
$text
)
.
'</bdi>'
),
[],
$params
);
}
/**
* @param string $page the string title get the TitleValue for.
* @return TitleValue the associated TitleValue object
*/
protected
function
getTitleValue
(
string
$page
=
'CheckUser'
):
TitleValue
{
return
new
TitleValue
(
NS_SPECIAL
,
$this
->
specialPageFactory
->
getLocalNameFor
(
$page
)
);
}
/**
* @param string $page the string title get the Title for.
* @return Title the associated Title object
*/
protected
function
getPageTitle
(
string
$page
=
'CheckUser'
):
Title
{
return
Title
::
newFromLinkTarget
(
$this
->
getTitleValue
(
$page
)
);
}
/**
* Get a formatted timestamp string in the current language
* for displaying to the user.
*
* @param string $timestamp
* @return string
*/
protected
function
getFormattedTimestamp
(
string
$timestamp
):
string
{
return
$this
->
getLanguage
()->
userTimeAndDate
(
wfTimestamp
(
TS_MW
,
$timestamp
),
$this
->
getUser
()
);
}
/**
* Generates the "no matches for X" message.
* Unless the target was an xff also try
* to display the time of the last edit.
*
* @inheritDoc
*/
protected
function
getEmptyBody
():
string
{
if
(
!
$this
->
xfor
)
{
// Only attempt to find the last edit or logged action timestamp if the
// target is a user or an IP. If the target is a XFF then skip this.
$user
=
$this
->
userIdentityLookup
->
getUserIdentityByName
(
$this
->
target
->
getName
()
);
$lastEdit
=
max
(
$this
->
mDb
->
newSelectQueryBuilder
()
->
select
(
'rev_timestamp'
)
->
from
(
'revision'
)
->
where
(
[
'actor_name'
=>
$this
->
target
->
getName
()
]
)
->
join
(
'actor'
,
null
,
'actor_id = rev_actor'
)
->
orderBy
(
'rev_timestamp'
,
SelectQueryBuilder
::
SORT_DESC
)
->
caller
(
__METHOD__
)
->
fetchField
(),
$this
->
mDb
->
newSelectQueryBuilder
()
->
table
(
'logging'
)
->
field
(
'log_timestamp'
)
->
orderBy
(
'log_timestamp'
,
SelectQueryBuilder
::
SORT_DESC
)
->
join
(
'actor'
,
null
,
'actor_id=log_actor'
)
->
where
(
[
'actor_name'
=>
$this
->
target
->
getName
()
]
)
->
caller
(
__METHOD__
)
->
fetchField
()
);
if
(
$lastEdit
)
{
$lastEditTime
=
wfTimestamp
(
TS_MW
,
$lastEdit
);
$lang
=
$this
->
getLanguage
();
$contextUser
=
$this
->
getUser
();
// FIXME: don't pass around parsed messages
return
$this
->
msg
(
'checkuser-nomatch-edits'
,
$lang
->
userDate
(
$lastEditTime
,
$contextUser
),
$lang
->
userTime
(
$lastEditTime
,
$contextUser
)
)->
parseAsBlock
()
.
"
\n
"
;
}
}
return
$this
->
msg
(
'checkuser-nomatch'
)->
parseAsBlock
()
.
"
\n
"
;
}
/**
* @param string $ip
* @param UserIdentity $user
* @return string[]
*/
protected
function
userBlockFlags
(
string
$ip
,
UserIdentity
$user
):
array
{
$flags
=
[];
// Generate the block flag. Only one will be displayed, with the order of priority being local block, then
// global block, then tor exit node block, and finally the account having been previously blocked.
$block
=
DatabaseBlock
::
newFromTarget
(
$user
,
$ip
);
if
(
$block
instanceof
DatabaseBlock
)
{
// Locally blocked
$flags
[]
=
$this
->
getBlockFlag
(
$block
);
}
elseif
(
ExtensionRegistry
::
getInstance
()->
isLoaded
(
'GlobalBlocking'
)
)
{
$globalBlockLookup
=
GlobalBlockingServices
::
wrap
(
MediaWikiServices
::
getInstance
()
)
->
getGlobalBlockLookup
();
$globalBlock
=
$globalBlockLookup
->
getGlobalBlockingBlock
(
$ip
?:
null
,
$this
->
centralIdLookup
->
centralIdFromLocalUser
(
$user
)
);
if
(
$globalBlock
!==
null
)
{
// Globally blocked IP or user
$flags
[]
=
'<strong>('
.
$this
->
msg
(
'checkuser-gblocked'
)->
escaped
()
.
')</strong>'
;
}
}
if
(
!
count
(
$flags
)
&&
$ip
==
$user
->
getName
()
&&
ExtensionRegistry
::
getInstance
()->
isLoaded
(
'TorBlock'
)
&&
TorExitNodes
::
isExitNode
(
$ip
)
)
{
// Tor exit node
$flags
[]
=
Html
::
rawElement
(
'strong'
,
[],
'('
.
$this
->
msg
(
'checkuser-torexitnode'
)->
escaped
()
.
')'
);
}
elseif
(
!
count
(
$flags
)
&&
$this
->
userWasBlocked
(
$user
->
getName
()
)
)
{
// Previously blocked
$blocklog
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
SpecialPage
::
getTitleFor
(
'Log'
),
$this
->
msg
(
'checkuser-wasblocked'
)->
text
(),
[],
[
'type'
=>
'block'
,
// @todo Use TitleFormatter and PageReference to avoid the global state
'page'
=>
Title
::
makeTitle
(
NS_USER
,
$user
->
getName
()
)->
getPrefixedText
()
]
);
$flags
[]
=
Html
::
rawElement
(
'strong'
,
[
'class'
=>
'mw-changeslist-links'
],
$blocklog
);
}
// Show if account is local only
if
(
$user
->
getId
()
&&
$this
->
centralIdLookup
->
centralIdFromLocalUser
(
$user
,
CentralIdLookup
::
AUDIENCE_RAW
)
===
0
)
{
$flags
[]
=
Html
::
rawElement
(
'strong'
,
[
'class'
=>
'mw-changeslist-links'
],
$this
->
msg
(
'checkuser-localonly'
)->
escaped
()
);
}
// Check for extra user rights...
if
(
$user
->
getId
()
)
{
if
(
$this
->
userFactory
->
newFromUserIdentity
(
$user
)->
isLocked
()
)
{
$flags
[]
=
Html
::
rawElement
(
'strong'
,
[
'class'
=>
'mw-changeslist-links'
],
$this
->
msg
(
'checkuser-locked'
)->
escaped
()
);
}
$list
=
[];
foreach
(
$this
->
userGroupManager
->
getUserGroups
(
$user
)
as
$group
)
{
$list
[]
=
self
::
buildGroupLink
(
$group
);
}
$groups
=
$this
->
getLanguage
()->
commaList
(
$list
);
if
(
$groups
)
{
$flags
[]
=
Html
::
rawElement
(
'i'
,
[
'class'
=>
'mw-changeslist-links'
],
$groups
);
}
}
return
$flags
;
}
/**
* Format a link to a group description page
*
* @param string $group
* @return string
*/
protected
static
function
buildGroupLink
(
string
$group
):
string
{
static
$cache
=
[];
if
(
!
isset
(
$cache
[
$group
]
)
)
{
$cache
[
$group
]
=
UserGroupMembership
::
getLinkHTML
(
$group
,
RequestContext
::
getMain
()
);
}
return
$cache
[
$group
];
}
/**
* Get whether the user has ever been blocked.
*
* @param string $name the username
* @return bool whether the user with that username has ever been blocked
*/
protected
function
userWasBlocked
(
string
$name
):
bool
{
$userpage
=
Title
::
makeTitle
(
NS_USER
,
$name
);
return
(
bool
)
$this
->
mDb
->
newSelectQueryBuilder
()
->
table
(
'logging'
)
->
field
(
'1'
)
->
conds
(
[
'log_type'
=>
[
'block'
,
'suppress'
],
'log_action'
=>
'block'
,
'log_namespace'
=>
$userpage
->
getNamespace
(),
'log_title'
=>
$userpage
->
getDBkey
()
]
)
->
useIndex
(
'log_page_time'
)
->
caller
(
__METHOD__
)
->
fetchField
();
}
/** @inheritDoc */
public
function
getIndexField
():
string
{
return
'timestamp'
;
}
/**
* @inheritDoc
*
* If the timestamp field is null, then the caller is either from outside
* CheckUser code or is not aware of what table a row comes from when
* reading results. In both cases returning "timestamp" is best as the
* queries alias the timestamp field to this name.
*
* If a caller uses the result of this method when $table is null as
* a column name in the WHERE conditions, the query will fail.
*
* @param string|null $table The table name. If null, returns the string "timestamp".
* @return string The timestamp field.
*/
public
function
getTimestampField
(
?
string
$table
=
null
):
string
{
if
(
$table
===
null
)
{
return
'timestamp'
;
}
return
self
::
RESULT_TABLE_TO_PREFIX
[
$table
]
.
'timestamp'
;
}
/** @inheritDoc */
protected
function
getStartBody
():
string
{
return
$this
->
getNavigationBar
()
.
'<div id="checkuserresults">'
;
}
/** @inheritDoc */
protected
function
getEndBody
():
string
{
return
'</div>'
.
$this
->
getNavigationBar
();
}
/** @inheritDoc */
public
function
getNavigationBuilder
():
PagerNavigationBuilder
{
$pagingQueries
=
$this
->
getPagingQueries
();
$baseQuery
=
array_merge
(
$this
->
getDefaultQuery
(),
[
// These query parameters are all defined here, even though some are null,
// to ensure consistent order of parameters when they're used.
'dir'
=>
null
,
'offset'
=>
$this
->
getOffsetQuery
(),
'limit'
=>
null
,
]
);
$navBuilder
=
new
CheckUserPagerNavigationBuilder
(
$this
->
getContext
(),
$this
->
tokenQueryManager
,
$this
->
getCsrfTokenSet
(),
$this
->
getRequest
(),
$this
->
opts
,
$this
->
target
);
$navBuilder
->
setPage
(
$this
->
getTitle
()
)
->
setLinkQuery
(
$baseQuery
)
->
setLimits
(
$this
->
mLimitsShown
)
->
setLimitLinkQueryParam
(
'limit'
)
->
setCurrentLimit
(
$this
->
mLimit
)
->
setPrevLinkQuery
(
$pagingQueries
[
'prev'
]
?:
null
)
->
setNextLinkQuery
(
$pagingQueries
[
'next'
]
?:
null
)
->
setFirstLinkQuery
(
$pagingQueries
[
'first'
]
?:
null
)
->
setLastLinkQuery
(
$pagingQueries
[
'last'
]
?:
null
);
return
$navBuilder
;
}
/**
* Used by getCheckUserHelperFieldsetHTML to get the fieldset.
* Seperated to allow testing of this method.
*
* @return ?HTMLFieldsetCheckUser
*/
private
function
getCheckUserHelperFieldset
()
{
if
(
!
$this
->
mResult
->
numRows
()
)
{
return
null
;
}
$collapseByDefault
=
$this
->
userOptionsLookup
->
getOption
(
$this
->
getUser
(),
'checkuser-helper-table-collapse-by-default'
);
if
(
is_numeric
(
$collapseByDefault
)
)
{
$collapseByDefault
=
$this
->
mResult
->
numRows
()
>
$collapseByDefault
;
}
elseif
(
$collapseByDefault
===
Preferences
::
CHECKUSER_HELPER_ALWAYS_COLLAPSE_BY_DEFAULT
)
{
$collapseByDefault
=
true
;
}
elseif
(
$collapseByDefault
===
Preferences
::
CHECKUSER_HELPER_NEVER_COLLAPSE_BY_DEFAULT
)
{
$collapseByDefault
=
false
;
}
else
{
// For Preferences::CHECKUSER_HELPER_USE_CONFIG_TO_COLLAPSE_BY_DEFAULT or any other value,
// use the value from the site config.
$collapseByDefault
=
$this
->
getConfig
()->
get
(
'CheckUserCollapseCheckUserHelperByDefault'
);
if
(
is_int
(
$collapseByDefault
)
)
{
$collapseByDefault
=
$this
->
mResult
->
numRows
()
>
$collapseByDefault
;
}
}
$fieldset
=
new
HTMLFieldsetCheckUser
(
[],
$this
->
getContext
()
);
$fieldset
->
outerClass
=
'mw-checkuser-helper-fieldset'
;
$fieldset
->
setCollapsibleOptions
(
$collapseByDefault
)
->
setWrapperLegendMsg
(
'checkuser-helper-label'
)
->
prepareForm
()
->
suppressDefaultSubmit
(
true
);
return
$fieldset
;
}
/**
*
* Returns a empty HTML OOUI fieldset which is collapsible.
* Used by checkUserHelper.js and it's where the wikitext
* table is added into the results page.
*
* @return string The HTML of the fieldset.
*/
protected
function
getCheckUserHelperFieldsetHTML
()
{
$fieldsetHTML
=
$this
->
getCheckUserHelperFieldset
();
if
(
$fieldsetHTML
)
{
return
$this
->
getCheckUserHelperFieldset
()->
getHTML
(
false
);
}
else
{
return
''
;
}
}
/**
* @inheritDoc
*
* @param string|null $table One of the tables in CheckUserQueryInterface::RESULT_TABLES.
* If set to null, this will throw a LogicException.
* @throws LogicException if $table is null a LogicException is thrown as ::getQueryInfo
* must have this information to return the correct query info.
*/
abstract
public
function
getQueryInfo
(
?
string
$table
=
null
);
/**
* Get the query info specific to cu_changes that will
* be extended with table independent query information
* (such as a actor_name WHERE condition). This
* method should only be called by the ::getQueryInfo
* implementation.
*
* @return array The query info specific to cu_changes
*/
abstract
protected
function
getQueryInfoForCuChanges
():
array
;
/**
* Get the query info specific to cu_log_event that will
* be extended with table independent query information
* (such as a actor_name WHERE condition). This
* method should only be called by the ::getQueryInfo
* implementation.
*
* @return array The query info specific to cu_log_event
*/
abstract
protected
function
getQueryInfoForCuLogEvent
():
array
;
/**
* Get the query info specific to cu_private_event that will
* be extended with table independent query information
* (such as a actor_name WHERE condition). This
* method should only be called by the ::getQueryInfo
* implementation.
*
* @return array The query info specific to cu_private_event
*/
abstract
protected
function
getQueryInfoForCuPrivateEvent
():
array
;
/**
* Take the raw results list and turn it into an array where the
* keys are the index field value and the values are the rows with
* this index field.
*
* Can be used by implementations to combine rows as necessary.
*
* @stable to override
*
* @param stdClass[] $results The results from all three tables as an array
* where the rows are values, keys are numeric, and the order is
* defined as the order returned by the queries and then the query
* results ordered in the same order as self::RESULT_TABLES.
* @return stdClass[] The results grouped by the index field where the
* key is the index field and the value is an array of the rows
* with this index field.
*/
protected
function
groupResultsByIndexField
(
array
$results
):
array
{
// Expand the result set into an array, with the key as the timestamp and
// value as an array of rows that have this timestamp.
$groupedResults
=
[];
$indexField
=
$this
->
getIndexField
();
foreach
(
$results
as
$row
)
{
if
(
array_key_exists
(
$row
->
$indexField
,
$groupedResults
)
)
{
// Use an array of rows as two given rows could have the same
// timestamp value.
$groupedResults
[
$row
->
$indexField
][]
=
$row
;
}
else
{
$groupedResults
[
$row
->
$indexField
]
=
[
$row
];
}
}
return
$groupedResults
;
}
/** @inheritDoc */
public
function
reallyDoQuery
(
$offset
,
$limit
,
$order
):
IResultWrapper
{
$results
=
[];
// Run the three SQL queries for each results table.
foreach
(
$this
->
buildQueryInfo
(
$offset
,
$limit
,
$order
)
as
$queryInfo
)
{
[
$tables
,
$fields
,
$conds
,
$fname
,
$options
,
$join_conds
]
=
$queryInfo
;
$results
=
array_merge
(
$results
,
iterator_to_array
(
$this
->
mDb
->
newSelectQueryBuilder
()
->
tables
(
$tables
)
->
fields
(
$fields
)
->
conds
(
$conds
)
->
caller
(
$fname
)
->
options
(
$options
)
->
joinConds
(
$join_conds
)
->
fetchResultSet
()
)
);
}
$results
=
$this
->
groupResultsByIndexField
(
$results
);
// Make a new result wrapper that combines the results by ordering all
// by their timestamp and then returning the first $limit of the items.
if
(
$order
===
self
::
QUERY_DESCENDING
)
{
krsort
(
$results
);
}
else
{
ksort
(
$results
);
}
// Can remove the keys now that the results have been sorted.
$results
=
array_values
(
$results
);
// Flatten the array. If for a given timestamp more than row is present,
// from the same table then this method will keep the order in which
// they were returned by IDatabase::select. If the rows come from
// different tables, the rows will be ordered by table in the order
// the tables are defined in self::RESULT_TABLES.
$flattened_results
=
[];
array_walk_recursive
(
$results
,
static
function
(
$value
)
use
(
&
$flattened_results
)
{
$flattened_results
[]
=
$value
;
}
);
// Apply the limit to the results.
$flattened_results
=
array_slice
(
$flattened_results
,
0
,
$limit
);
// Return the generated data as a FakeResultWrapper.
return
new
FakeResultWrapper
(
$flattened_results
);
}
/**
* Builds the query information. This is the same code as written in
* IndexPager::buildQueryInfo, ReverseChronologicalPager::buildQueryInfo
* and RangeChronologicalPager::buildQueryInfo, but modifies it to
* provide ::getQueryInfo with the arguments passed to this method.
*
* @inheritDoc
*/
public
function
buildQueryInfo
(
$offset
,
$limit
,
$order
):
array
{
// Copied, with modification, from IndexPager::buildQueryInfo
$fname
=
__METHOD__
.
' ('
.
$this
->
getSqlComment
()
.
')'
;
$queryInfo
=
[];
foreach
(
self
::
RESULT_TABLES
as
$table
)
{
$info
=
$this
->
getQueryInfo
(
$table
);
$tables
=
$info
[
'tables'
];
$fields
=
$info
[
'fields'
];
$conds
=
$info
[
'conds'
]
??
[];
$options
=
$info
[
'options'
]
??
[];
$join_conds
=
$info
[
'join_conds'
]
??
[];
$indexColumns
=
(
array
)
$this
->
mIndexField
;
$sortColumns
=
array_merge
(
$indexColumns
,
$this
->
mExtraSortFields
);
if
(
$order
===
self
::
QUERY_ASCENDING
)
{
$options
[
'ORDER BY'
]
=
$sortColumns
;
$operator
=
$this
->
mIncludeOffset
?
'>='
:
'>'
;
}
else
{
$orderBy
=
[];
foreach
(
$sortColumns
as
$col
)
{
$orderBy
[]
=
$col
.
' DESC'
;
}
$options
[
'ORDER BY'
]
=
$orderBy
;
$operator
=
$this
->
mIncludeOffset
?
'<='
:
'<'
;
}
if
(
$offset
)
{
// From IndexPager
$offsets
=
explode
(
'|'
,
$offset
,
count
(
$indexColumns
)
);
$indexColumns
=
array_slice
(
$indexColumns
,
0
,
count
(
$offsets
)
);
// Replace 'timestamp' with the timestamp column name for the given table.
$timestampField
=
$this
->
getTimestampField
(
$table
);
$indexColumns
=
array_map
(
static
function
(
$value
)
use
(
$timestampField
)
{
return
$value
===
'timestamp'
?
$timestampField
:
$value
;
},
$indexColumns
);
// From IndexPager
$conds
[]
=
$this
->
mDb
->
buildComparison
(
$operator
,
array_combine
(
$indexColumns
,
$offsets
)
);
}
$options
[
'LIMIT'
]
=
intval
(
$limit
);
// Copied from ReverseChronologicalPager::buildQueryInfo
if
(
$this
->
endOffset
)
{
$conds
[]
=
$this
->
mDb
->
buildComparison
(
'<'
,
[
$this
->
getTimestampField
(
$table
)
=>
$this
->
endOffset
]
);
}
// Copied from RangeChronologicalPager::buildQueryInfo
if
(
$this
->
startOffset
)
{
$conds
[]
=
$this
->
mDb
->
buildComparison
(
'>='
,
[
$this
->
getTimestampField
(
$table
)
=>
$this
->
startOffset
]
);
}
// Add the data that would normally be returned by this method to an array
// so that it can be returned for all three tables at once.
$queryInfo
[
$table
]
=
[
$tables
,
$fields
,
$conds
,
$fname
,
$options
,
$join_conds
];
}
return
$queryInfo
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Oct 3, 20:22 (3 d, 16 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
53/24/aca1c2460196a05974ba874dbbec
Default Alt Text
AbstractCheckUserPager.php (26 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment