Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / specials / pagers / UsersPager.php
bloba386aa3715bac06ac801edc7253bc4e516804323
1 <?php
2 /**
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
22 * @file
23 * @ingroup Pager
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;
49 use stdClass;
50 use Wikimedia\Rdbms\IConnectionProvider;
52 /**
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.
57 * @ingroup Pager
59 class UsersPager extends AlphabeticPager {
61 /**
62 * @var array[] A array with user ids as key and a array of groups as value
64 protected $userGroupCache;
66 /** @var string */
67 public $requestedGroup;
69 /** @var bool */
70 protected $editsOnly;
72 /** @var bool */
73 protected $temporaryGroupsOnly;
75 /** @var bool */
76 protected $creationSort;
78 /** @var bool|null */
79 protected $including;
81 /** @var string */
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;
92 /**
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
102 * another page
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,
112 $par,
113 $including
115 $this->setContext( $context );
117 $request = $this->getRequest();
118 $par ??= '';
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];
129 $un = $parms[1];
130 } else {
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 = '';
148 if ( $un != '' ) {
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;
167 * @return string
169 public function getIndexField() {
170 return $this->creationSort ? 'user_id' : 'user_name';
174 * @return array
176 public function getQueryInfo() {
177 $dbr = $this->getDatabase();
178 $conds = [];
179 $options = [];
181 // Don't show hidden names
182 if ( !$this->canSeeHideuser() ) {
183 $conds[] = $this->hideUserUtils->getExpression( $dbr );
184 $deleted = '1=0';
185 } else {
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 )
199 ) . ')';
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 );
207 $conds[] = $cond;
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() );
221 } else {
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';
232 $query = [
233 'tables' => [
234 'user',
235 'user_groups',
236 'block_with_target' => [
237 'block_target',
238 'block'
241 'fields' => [
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,
250 'join_conds' => [
251 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ],
252 'block_with_target' => [
253 'LEFT JOIN', [
254 'user_id=bt_user',
255 'bt_auto' => 0
258 'block' => [ 'JOIN', 'bl_target=bt_id' ]
260 'conds' => $conds
263 $this->hookRunner->onSpecialListusersQueryInfo( $this, $query );
265 return $query;
269 * @param stdClass $row
270 * @return string
272 public function formatRow( $row ) {
273 if ( $row->user_id == 0 ) { # T18487
274 return '';
277 $userName = $row->user_name;
279 $ulinks = Linker::userLink( $row->user_id, $userName );
280 $ulinks .= Linker::userToolLinksRedContribs(
281 $row->user_id,
282 $userName,
283 (int)$row->edits,
284 // don't render parentheses in HTML markup (CSS will provide)
285 false
288 $lang = $this->getLanguage();
290 $groups = '';
291 $userIdentity = new UserIdentityValue( intval( $row->user_id ), $userName );
292 $ugms = $this->getGroupMemberships( $userIdentity );
294 if ( !$this->including && count( $ugms ) > 0 ) {
295 $list = [];
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>";
308 $edits = '';
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();
314 $created = '';
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();
335 $userIds = [];
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__ )
347 ->fetchResultSet();
348 $cache = [];
349 $groups = [];
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 );
367 if ( $groupPage ) {
368 $batch->addObj( $groupPage );
372 $batch->execute();
373 $this->mResult->rewind();
377 * @return string
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(),
394 continue;
396 $groupOptions[ $groupText ] = $group;
399 $formDescriptor = [
400 'user' => [
401 'class' => HTMLUserTextField::class,
402 'label' => $this->msg( 'listusersfrom' )->text(),
403 'name' => 'username',
404 'default' => $this->requestedUser,
406 'dropdown' => [
407 'label' => $this->msg( 'group' )->text(),
408 'name' => 'group',
409 'default' => $this->requestedGroup,
410 'class' => HTMLSelectField::class,
411 'options' => $groupOptions,
413 'editsOnly' => [
414 'type' => 'check',
415 'label' => $this->msg( 'listusers-editsonly' )->text(),
416 'name' => 'editsOnly',
417 'id' => 'editsOnly',
418 'default' => $this->editsOnly
420 'temporaryGroupsOnly' => [
421 'type' => 'check',
422 'label' => $this->msg( 'listusers-temporarygroupsonly' )->text(),
423 'name' => 'temporaryGroupsOnly',
424 'id' => 'temporaryGroupsOnly',
425 'default' => $this->temporaryGroupsOnly
427 'creationSort' => [
428 'type' => 'check',
429 'label' => $this->msg( 'listusers-creationsort' )->text(),
430 'name' => 'creationSort',
431 'id' => 'creationSort',
432 'default' => $this->creationSort
434 'desc' => [
435 'type' => 'check',
436 'label' => $this->msg( 'listusers-desc' )->text(),
437 'name' => 'desc',
438 'id' => 'desc',
439 'default' => $this->mDefaultDirection
441 'limithiddenfield' => [
442 'class' => HTMLHiddenField::class,
443 'name' => 'limit',
444 'default' => $this->mLimit
448 $beforeSubmitButtonHookOut = '';
449 $this->hookRunner->onSpecialListusersHeaderForm( $this, $beforeSubmitButtonHookOut );
451 if ( $beforeSubmitButtonHookOut !== '' ) {
452 $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
453 'class' => HTMLInfoField::class,
454 'raw' => true,
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,
470 'raw' => true,
471 'default' => $beforeClosingFieldsetHookOut
475 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
476 $htmlForm
477 ->setMethod( 'get' )
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
492 * @return array
494 private function getAllGroups() {
495 $result = [];
496 $lang = $this->getLanguage();
497 foreach ( $this->userGroupManager->listAllGroups() as $group ) {
498 $result[$group] = $lang->getGroupName( $group );
500 asort( $result );
502 return $result;
506 * Preserve group and username offset parameters when paging
507 * @return array
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 );
519 return $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 );
532 } else {
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
542 * @return string
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' );