3 * Copyright © 2004 Brooke Vibber, lcrocker, Tim Starling,
4 * Domas Mituzas, Antoine Musso, Jens Frank, Zhengzhu,
5 * 2006 Rob Church <robchur@gmail.com>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * http://www.gnu.org/copyleft/gpl.html
26 namespace MediaWiki\Pager
;
28 use MediaWiki\Block\HideUserUtils
;
29 use MediaWiki\Cache\LinkBatchFactory
;
30 use MediaWiki\Context\IContextSource
;
31 use MediaWiki\HookContainer\HookContainer
;
32 use MediaWiki\HookContainer\HookRunner
;
33 use MediaWiki\Html\Html
;
34 use MediaWiki\HTMLForm\Field\HTMLHiddenField
;
35 use MediaWiki\HTMLForm\Field\HTMLInfoField
;
36 use MediaWiki\HTMLForm\Field\HTMLSelectField
;
37 use MediaWiki\HTMLForm\Field\HTMLSubmitField
;
38 use MediaWiki\HTMLForm\Field\HTMLUserTextField
;
39 use MediaWiki\HTMLForm\HTMLForm
;
40 use MediaWiki\Linker\Linker
;
41 use MediaWiki\Logger\LoggerFactory
;
42 use MediaWiki\MainConfigNames
;
43 use MediaWiki\Title\Title
;
44 use MediaWiki\User\UserGroupManager
;
45 use MediaWiki\User\UserGroupMembership
;
46 use MediaWiki\User\UserIdentity
;
47 use MediaWiki\User\UserIdentityLookup
;
48 use MediaWiki\User\UserIdentityValue
;
50 use Wikimedia\Rdbms\IConnectionProvider
;
53 * This class is used to get a list of user. The ones with specials
54 * rights (sysop, bureaucrat, developer) will have them displayed
55 * next to their names.
59 class UsersPager
extends AlphabeticPager
{
62 * @var array[] A array with user ids as key and a array of groups as value
64 protected $userGroupCache;
67 public $requestedGroup;
73 protected $temporaryGroupsOnly;
76 protected $creationSort;
82 protected $requestedUser;
84 /** @var HideUserUtils */
85 protected $hideUserUtils;
87 private HookRunner
$hookRunner;
88 private LinkBatchFactory
$linkBatchFactory;
89 private UserGroupManager
$userGroupManager;
90 private UserIdentityLookup
$userIdentityLookup;
93 * @param IContextSource $context
94 * @param HookContainer $hookContainer
95 * @param LinkBatchFactory $linkBatchFactory
96 * @param IConnectionProvider $dbProvider
97 * @param UserGroupManager $userGroupManager
98 * @param UserIdentityLookup $userIdentityLookup
99 * @param HideUserUtils $hideUserUtils
100 * @param string|null $par
101 * @param bool|null $including Whether this page is being transcluded in
104 public function __construct(
105 IContextSource
$context,
106 HookContainer
$hookContainer,
107 LinkBatchFactory
$linkBatchFactory,
108 IConnectionProvider
$dbProvider,
109 UserGroupManager
$userGroupManager,
110 UserIdentityLookup
$userIdentityLookup,
111 HideUserUtils
$hideUserUtils,
115 $this->setContext( $context );
117 $request = $this->getRequest();
119 $parms = explode( '/', $par );
120 $symsForAll = [ '*', 'user' ];
122 if ( $parms[0] != '' &&
123 ( in_array( $par, $userGroupManager->listAllGroups() ) ||
in_array( $par, $symsForAll ) )
125 $this->requestedGroup
= $par;
126 $un = $request->getText( 'username' );
127 } elseif ( count( $parms ) == 2 ) {
128 $this->requestedGroup
= $parms[0];
131 $this->requestedGroup
= $request->getVal( 'group' );
132 $un = ( $par != '' ) ?
$par : $request->getText( 'username' );
135 if ( in_array( $this->requestedGroup
, $symsForAll ) ) {
136 $this->requestedGroup
= '';
138 $this->editsOnly
= $request->getBool( 'editsOnly' );
139 $this->temporaryGroupsOnly
= $request->getBool( 'temporaryGroupsOnly' );
140 $this->creationSort
= $request->getBool( 'creationSort' );
141 $this->including
= $including;
142 $this->mDefaultDirection
= $request->getBool( 'desc' )
143 ? IndexPager
::DIR_DESCENDING
144 : IndexPager
::DIR_ASCENDING
;
146 $this->requestedUser
= '';
149 $username = Title
::makeTitleSafe( NS_USER
, $un );
151 if ( $username !== null ) {
152 $this->requestedUser
= $username->getText();
156 // Set database before parent constructor to avoid setting it there
157 $this->mDb
= $dbProvider->getReplicaDatabase();
158 parent
::__construct();
159 $this->userGroupManager
= $userGroupManager;
160 $this->hookRunner
= new HookRunner( $hookContainer );
161 $this->linkBatchFactory
= $linkBatchFactory;
162 $this->userIdentityLookup
= $userIdentityLookup;
163 $this->hideUserUtils
= $hideUserUtils;
169 public function getIndexField() {
170 return $this->creationSort ?
'user_id' : 'user_name';
176 public function getQueryInfo() {
177 $dbr = $this->getDatabase();
181 // Don't show hidden names
182 if ( !$this->canSeeHideuser() ) {
183 $conds[] = $this->hideUserUtils
->getExpression( $dbr );
186 // In MySQL, there's no separate boolean type so getExpression()
187 // effectively returns an integer, and MAX() works on the result of it.
188 // In PostgreSQL, getExpression() returns a special boolean type which
189 // can't go into MAX(). So we have to cast it to support PostgreSQL.
191 // A neater PostgreSQL-only solution would be bool_or(), but MySQL
192 // doesn't have that or need it. We could add a wrapper to SQLPlatform
193 // which returns MAX() on MySQL and bool_or() on PostgreSQL.
195 // This would not be necessary if we used "GROUP BY user_name,user_id",
196 // but MariaDB forgets how to use indexes if you do that.
197 $deleted = 'MAX(' . $dbr->buildIntegerCast(
198 $this->hideUserUtils
->getExpression( $dbr, 'user_id', HideUserUtils
::HIDDEN_USERS
)
202 if ( $this->requestedGroup
!= '' ||
$this->temporaryGroupsOnly
) {
203 $cond = $dbr->expr( 'ug_expiry', '>=', $dbr->timestamp() );
204 if ( !$this->temporaryGroupsOnly
) {
205 $cond = $cond->or( 'ug_expiry', '=', null );
210 if ( $this->requestedGroup
!= '' ) {
211 $conds['ug_group'] = $this->requestedGroup
;
214 if ( $this->requestedUser
!= '' ) {
215 # Sorted either by account creation or name
216 if ( $this->creationSort
) {
217 $userIdentity = $this->userIdentityLookup
->getUserIdentityByName( $this->requestedUser
);
218 if ( $userIdentity && $userIdentity->isRegistered() ) {
219 $conds[] = $dbr->expr( 'user_id', '>=', $userIdentity->getId() );
222 $conds[] = $dbr->expr( 'user_name', '>=', $this->requestedUser
);
226 if ( $this->editsOnly
) {
227 $conds[] = $dbr->expr( 'user_editcount', '>', 0 );
230 $options['GROUP BY'] = $this->creationSort ?
'user_id' : 'user_name';
236 'block_with_target' => [
242 'user_name' => $this->creationSort ?
'MAX(user_name)' : 'user_name',
243 'user_id' => $this->creationSort ?
'user_id' : 'MAX(user_id)',
244 'edits' => 'MAX(user_editcount)',
245 'creation' => 'MIN(user_registration)',
246 'deleted' => $deleted, // block/hide status
247 'sitewide' => 'MAX(bl_sitewide)'
249 'options' => $options,
251 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ],
252 'block_with_target' => [
258 'block' => [ 'JOIN', 'bl_target=bt_id' ]
263 $this->hookRunner
->onSpecialListusersQueryInfo( $this, $query );
269 * @param stdClass $row
272 public function formatRow( $row ) {
273 if ( $row->user_id
== 0 ) { # T18487
277 $userName = $row->user_name
;
279 $ulinks = Linker
::userLink( $row->user_id
, $userName );
280 $ulinks .= Linker
::userToolLinksRedContribs(
284 // don't render parentheses in HTML markup (CSS will provide)
288 $lang = $this->getLanguage();
291 $userIdentity = new UserIdentityValue( intval( $row->user_id
), $userName );
292 $ugms = $this->getGroupMemberships( $userIdentity );
294 if ( !$this->including
&& count( $ugms ) > 0 ) {
296 foreach ( $ugms as $ugm ) {
297 $list[] = $this->buildGroupLink( $ugm, $userName );
299 $groups = $lang->commaList( $list );
302 $item = $lang->specialList( $ulinks, $groups );
304 if ( $row->deleted
) {
305 $item = "<span class=\"deleted\">$item</span>";
309 if ( !$this->including
&& $this->getConfig()->get( MainConfigNames
::Edititis
) ) {
310 $count = $this->msg( 'usereditcount' )->numParams( $row->edits
)->escaped();
311 $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped();
315 # Some rows may be null
316 if ( !$this->including
&& $row->creation
) {
317 $user = $this->getUser();
318 $d = $lang->userDate( $row->creation
, $user );
319 $t = $lang->userTime( $row->creation
, $user );
320 $created = $this->msg( 'usercreated', $d, $t, $row->user_name
)->escaped();
321 $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped();
324 $blocked = $row->deleted
!== null && $row->sitewide
=== '1' ?
325 ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() :
328 $this->hookRunner
->onSpecialListusersFormatRow( $item, $row );
330 return Html
::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" );
333 protected function doBatchLookups() {
334 $batch = $this->linkBatchFactory
->newLinkBatch();
336 # Give some pointers to make user links
337 foreach ( $this->mResult
as $row ) {
338 $batch->add( NS_USER
, $row->user_name
);
339 $batch->add( NS_USER_TALK
, $row->user_name
);
340 $userIds[] = (int)$row->user_id
;
343 // Lookup groups for all the users
344 $queryBuilder = $this->userGroupManager
->newQueryBuilder( $this->getDatabase() );
345 $groupRes = $queryBuilder->where( [ 'ug_user' => $userIds ] )
346 ->caller( __METHOD__
)
350 foreach ( $groupRes as $row ) {
351 $ugm = $this->userGroupManager
->newGroupMembershipFromRow( $row );
352 if ( !$ugm->isExpired() ) {
353 $cache[$row->ug_user
][$row->ug_group
] = $ugm;
354 $groups[$row->ug_group
] = true;
358 // Give extensions a chance to add things like global user group data
359 // into the cache array to ensure proper output later on
360 $this->hookRunner
->onUsersPagerDoBatchLookups( $this->getDatabase(), $userIds, $cache, $groups );
362 $this->userGroupCache
= $cache;
364 // Add page of groups to link batch
365 foreach ( $groups as $group => $unused ) {
366 $groupPage = UserGroupMembership
::getGroupPage( $group );
368 $batch->addObj( $groupPage );
373 $this->mResult
->rewind();
379 public function getPageHeader() {
380 $self = explode( '/', $this->getTitle()->getPrefixedDBkey(), 2 )[0];
382 $groupOptions = [ $this->msg( 'group-all' )->text() => '' ];
383 foreach ( $this->getAllGroups() as $group => $groupText ) {
384 if ( array_key_exists( $groupText, $groupOptions ) ) {
385 LoggerFactory
::getInstance( 'translation-problem' )->error(
386 'The group {group_one} has the same translation as {group_two} for {lang}. ' .
387 '{group_one} will not be displayed in group dropdown of the UsersPager.',
389 'group_one' => $group,
390 'group_two' => $groupOptions[$groupText],
391 'lang' => $this->getLanguage()->getCode(),
396 $groupOptions[ $groupText ] = $group;
401 'class' => HTMLUserTextField
::class,
402 'label' => $this->msg( 'listusersfrom' )->text(),
403 'name' => 'username',
404 'default' => $this->requestedUser
,
407 'label' => $this->msg( 'group' )->text(),
409 'default' => $this->requestedGroup
,
410 'class' => HTMLSelectField
::class,
411 'options' => $groupOptions,
415 'label' => $this->msg( 'listusers-editsonly' )->text(),
416 'name' => 'editsOnly',
418 'default' => $this->editsOnly
420 'temporaryGroupsOnly' => [
422 'label' => $this->msg( 'listusers-temporarygroupsonly' )->text(),
423 'name' => 'temporaryGroupsOnly',
424 'id' => 'temporaryGroupsOnly',
425 'default' => $this->temporaryGroupsOnly
429 'label' => $this->msg( 'listusers-creationsort' )->text(),
430 'name' => 'creationSort',
431 'id' => 'creationSort',
432 'default' => $this->creationSort
436 'label' => $this->msg( 'listusers-desc' )->text(),
439 'default' => $this->mDefaultDirection
441 'limithiddenfield' => [
442 'class' => HTMLHiddenField
::class,
444 'default' => $this->mLimit
448 $beforeSubmitButtonHookOut = '';
449 $this->hookRunner
->onSpecialListusersHeaderForm( $this, $beforeSubmitButtonHookOut );
451 if ( $beforeSubmitButtonHookOut !== '' ) {
452 $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
453 'class' => HTMLInfoField
::class,
455 'default' => $beforeSubmitButtonHookOut
459 $formDescriptor[ 'submit' ] = [
460 'class' => HTMLSubmitField
::class,
461 'buttonlabel-message' => 'listusers-submit',
464 $beforeClosingFieldsetHookOut = '';
465 $this->hookRunner
->onSpecialListusersHeader( $this, $beforeClosingFieldsetHookOut );
467 if ( $beforeClosingFieldsetHookOut !== '' ) {
468 $formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [
469 'class' => HTMLInfoField
::class,
471 'default' => $beforeClosingFieldsetHookOut
475 $htmlForm = HTMLForm
::factory( 'ooui', $formDescriptor, $this->getContext() );
478 ->setTitle( Title
::newFromText( $self ) )
479 ->setId( 'mw-listusers-form' )
480 ->setFormIdentifier( 'mw-listusers-form' )
481 ->suppressDefaultSubmit()
482 ->setWrapperLegendMsg( 'listusers' );
483 return $htmlForm->prepareForm()->getHTML( true );
486 protected function canSeeHideuser() {
487 return $this->getAuthority()->isAllowed( 'hideuser' );
491 * Get a list of all explicit groups
494 private function getAllGroups() {
496 $lang = $this->getLanguage();
497 foreach ( $this->userGroupManager
->listAllGroups() as $group ) {
498 $result[$group] = $lang->getGroupName( $group );
506 * Preserve group and username offset parameters when paging
509 public function getDefaultQuery() {
510 $query = parent
::getDefaultQuery();
511 if ( $this->requestedGroup
!= '' ) {
512 $query['group'] = $this->requestedGroup
;
514 if ( $this->requestedUser
!= '' ) {
515 $query['username'] = $this->requestedUser
;
517 $this->hookRunner
->onSpecialListusersDefaultQuery( $this, $query );
523 * Get an associative array containing groups the specified user belongs to,
524 * and the relevant UserGroupMembership objects
526 * @param UserIdentity $user
527 * @return UserGroupMembership[] (group name => UserGroupMembership object)
529 protected function getGroupMemberships( $user ) {
530 if ( $this->userGroupCache
=== null ) {
531 return $this->userGroupManager
->getUserGroupMemberships( $user );
533 return $this->userGroupCache
[$user->getId()] ??
[];
538 * Format a link to a group description page
540 * @param string|UserGroupMembership $group Group name or UserGroupMembership object
541 * @param string $username
544 protected function buildGroupLink( $group, $username ) {
545 return UserGroupMembership
::getLinkHTML( $group, $this->getContext(), $username );
550 * Retain the old class name for backwards compatibility.
551 * @deprecated since 1.41
553 class_alias( UsersPager
::class, 'UsersPager' );