MediaWiki master
UsersPager.php
Go to the documentation of this file.
1<?php
26namespace MediaWiki\Pager;
27
49use stdClass;
51
60
64 protected $userGroupCache;
65
68
70 protected $editsOnly;
71
74
76 protected $creationSort;
77
79 protected $including;
80
82 protected $requestedUser;
83
85 protected $hideUserUtils;
86
87 private HookRunner $hookRunner;
88 private LinkBatchFactory $linkBatchFactory;
89 private UserGroupManager $userGroupManager;
90 private UserIdentityLookup $userIdentityLookup;
91
104 public function __construct(
105 IContextSource $context,
106 HookContainer $hookContainer,
107 LinkBatchFactory $linkBatchFactory,
108 IConnectionProvider $dbProvider,
109 UserGroupManager $userGroupManager,
110 UserIdentityLookup $userIdentityLookup,
112 $par,
114 ) {
115 $this->setContext( $context );
116
117 $request = $this->getRequest();
118 $par ??= '';
119 $parms = explode( '/', $par );
120 $symsForAll = [ '*', 'user' ];
121
122 if ( $parms[0] != '' &&
123 ( in_array( $par, $userGroupManager->listAllGroups() ) || in_array( $par, $symsForAll ) )
124 ) {
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' );
133 }
134
135 if ( in_array( $this->requestedGroup, $symsForAll ) ) {
136 $this->requestedGroup = '';
137 }
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' )
145
146 $this->requestedUser = '';
147
148 if ( $un != '' ) {
149 $username = Title::makeTitleSafe( NS_USER, $un );
150
151 if ( $username !== null ) {
152 $this->requestedUser = $username->getText();
153 }
154 }
155
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;
164 }
165
169 public function getIndexField() {
170 return $this->creationSort ? 'user_id' : 'user_name';
171 }
172
176 public function getQueryInfo() {
177 $dbr = $this->getDatabase();
178 $conds = [];
179 $options = [];
180
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.
190
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.
194
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 ) . ')';
200 }
201
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 );
206 }
207 $conds[] = $cond;
208 }
209
210 if ( $this->requestedGroup != '' ) {
211 $conds['ug_group'] = $this->requestedGroup;
212 }
213
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() );
220 }
221 } else {
222 $conds[] = $dbr->expr( 'user_name', '>=', $this->requestedUser );
223 }
224 }
225
226 if ( $this->editsOnly ) {
227 $conds[] = $dbr->expr( 'user_editcount', '>', 0 );
228 }
229
230 $options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name';
231
232 $query = [
233 'tables' => [
234 'user',
235 'user_groups',
236 'block_with_target' => [
237 'block_target',
238 'block'
239 ]
240 ],
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)'
248 ],
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
256 ]
257 ],
258 'block' => [ 'JOIN', 'bl_target=bt_id' ]
259 ],
260 'conds' => $conds
261 ];
262
263 $this->hookRunner->onSpecialListusersQueryInfo( $this, $query );
264
265 return $query;
266 }
267
272 public function formatRow( $row ) {
273 if ( $row->user_id == 0 ) { # T18487
274 return '';
275 }
276
277 $userName = $row->user_name;
278
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
286 );
287
288 $lang = $this->getLanguage();
289
290 $groups = '';
291 $userIdentity = new UserIdentityValue( intval( $row->user_id ), $userName );
292 $ugms = $this->getGroupMemberships( $userIdentity );
293
294 if ( !$this->including && count( $ugms ) > 0 ) {
295 $list = [];
296 foreach ( $ugms as $ugm ) {
297 $list[] = $this->buildGroupLink( $ugm, $userName );
298 }
299 $groups = $lang->commaList( $list );
300 }
301
302 $item = $lang->specialList( $ulinks, $groups );
303
304 if ( $row->deleted ) {
305 $item = "<span class=\"deleted\">$item</span>";
306 }
307
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();
312 }
313
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();
322 }
323
324 $blocked = $row->deleted !== null && $row->sitewide === '1' ?
325 ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() :
326 '';
327
328 $this->hookRunner->onSpecialListusersFormatRow( $item, $row );
329
330 return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" );
331 }
332
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;
341 }
342
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;
355 }
356 }
357
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 );
361
362 $this->userGroupCache = $cache;
363
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 );
369 }
370 }
371
372 $batch->execute();
373 $this->mResult->rewind();
374 }
375
379 public function getPageHeader() {
380 $self = explode( '/', $this->getTitle()->getPrefixedDBkey(), 2 )[0];
381
382 $groupOptions = [ $this->msg( 'group-all' )->text() => '' ];
383 foreach ( $this->getAllGroups() as $group => $groupText ) {
384 if ( array_key_exists( $groupText, $groupOptions ) ) {
385 LoggerFactory::getInstance( 'error' )->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.',
388 [
389 'group_one' => $group,
390 'group_two' => $groupOptions[$groupText],
391 'lang' => $this->getLanguage()->getCode(),
392 ]
393 );
394 continue;
395 }
396 $groupOptions[ $groupText ] = $group;
397 }
398
399 $formDescriptor = [
400 'user' => [
401 'class' => HTMLUserTextField::class,
402 'label' => $this->msg( 'listusersfrom' )->text(),
403 'name' => 'username',
404 'default' => $this->requestedUser,
405 ],
406 'dropdown' => [
407 'label' => $this->msg( 'group' )->text(),
408 'name' => 'group',
409 'default' => $this->requestedGroup,
410 'class' => HTMLSelectField::class,
411 'options' => $groupOptions,
412 ],
413 'editsOnly' => [
414 'type' => 'check',
415 'label' => $this->msg( 'listusers-editsonly' )->text(),
416 'name' => 'editsOnly',
417 'id' => 'editsOnly',
418 'default' => $this->editsOnly
419 ],
420 'temporaryGroupsOnly' => [
421 'type' => 'check',
422 'label' => $this->msg( 'listusers-temporarygroupsonly' )->text(),
423 'name' => 'temporaryGroupsOnly',
424 'id' => 'temporaryGroupsOnly',
425 'default' => $this->temporaryGroupsOnly
426 ],
427 'creationSort' => [
428 'type' => 'check',
429 'label' => $this->msg( 'listusers-creationsort' )->text(),
430 'name' => 'creationSort',
431 'id' => 'creationSort',
432 'default' => $this->creationSort
433 ],
434 'desc' => [
435 'type' => 'check',
436 'label' => $this->msg( 'listusers-desc' )->text(),
437 'name' => 'desc',
438 'id' => 'desc',
439 'default' => $this->mDefaultDirection
440 ],
441 'limithiddenfield' => [
442 'class' => HTMLHiddenField::class,
443 'name' => 'limit',
444 'default' => $this->mLimit
445 ]
446 ];
447
448 $beforeSubmitButtonHookOut = '';
449 $this->hookRunner->onSpecialListusersHeaderForm( $this, $beforeSubmitButtonHookOut );
450
451 if ( $beforeSubmitButtonHookOut !== '' ) {
452 $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
453 'class' => HTMLInfoField::class,
454 'raw' => true,
455 'default' => $beforeSubmitButtonHookOut
456 ];
457 }
458
459 $formDescriptor[ 'submit' ] = [
460 'class' => HTMLSubmitField::class,
461 'buttonlabel-message' => 'listusers-submit',
462 ];
463
464 $beforeClosingFieldsetHookOut = '';
465 $this->hookRunner->onSpecialListusersHeader( $this, $beforeClosingFieldsetHookOut );
466
467 if ( $beforeClosingFieldsetHookOut !== '' ) {
468 $formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [
469 'class' => HTMLInfoField::class,
470 'raw' => true,
471 'default' => $beforeClosingFieldsetHookOut
472 ];
473 }
474
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 );
484 }
485
486 protected function canSeeHideuser() {
487 return $this->getAuthority()->isAllowed( 'hideuser' );
488 }
489
494 private function getAllGroups() {
495 $result = [];
496 $lang = $this->getLanguage();
497 foreach ( $this->userGroupManager->listAllGroups() as $group ) {
498 $result[$group] = $lang->getGroupName( $group );
499 }
500 asort( $result );
501
502 return $result;
503 }
504
509 public function getDefaultQuery() {
510 $query = parent::getDefaultQuery();
511 if ( $this->requestedGroup != '' ) {
512 $query['group'] = $this->requestedGroup;
513 }
514 if ( $this->requestedUser != '' ) {
515 $query['username'] = $this->requestedUser;
516 }
517 $this->hookRunner->onSpecialListusersDefaultQuery( $this, $query );
518
519 return $query;
520 }
521
529 protected function getGroupMemberships( $user ) {
530 if ( $this->userGroupCache === null ) {
531 return $this->userGroupManager->getUserGroupMemberships( $user );
532 } else {
533 return $this->userGroupCache[$user->getId()] ?? [];
534 }
535 }
536
544 protected function buildGroupLink( $group, $username ) {
545 return UserGroupMembership::getLinkHTML( $group, $this->getContext(), $username );
546 }
547}
548
553class_alias( UsersPager::class, 'UsersPager' );
const NS_USER
Definition Defines.php:67
const NS_USER_TALK
Definition Defines.php:68
Helpers for building queries that determine whether a user is hidden.
setContext(IContextSource $context)
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
An information field (text blob), not a proper input.
Add a submit button inline in the form (as opposed to HTMLForm::addButton(), which will add it at the...
Implements a text input field for user names.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:208
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Some internal bits split of from Skin.php.
Definition Linker.php:63
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const Edititis
Name constant for the Edititis setting, for use with Config::get()
IndexPager with an alphabetic list and a formatted navigation bar.
getDatabase()
Get the Database object in use.
int $mLimit
The maximum number of entries to show.
const DIR_ASCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
bool $mDefaultDirection
$mDefaultDirection gives the direction to use when sorting results: DIR_ASCENDING or DIR_DESCENDING.
This class is used to get a list of user.
getGroupMemberships( $user)
Get an associative array containing groups the specified user belongs to, and the relevant UserGroupM...
__construct(IContextSource $context, HookContainer $hookContainer, LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, UserGroupManager $userGroupManager, UserIdentityLookup $userIdentityLookup, HideUserUtils $hideUserUtils, $par, $including)
buildGroupLink( $group, $username)
Format a link to a group description page.
HideUserUtils $hideUserUtils
getDefaultQuery()
Preserve group and username offset parameters when paging.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
array[] $userGroupCache
A array with user ids as key and a array of groups as value.
Represents a title within MediaWiki.
Definition Title.php:79
listAllGroups()
Return the set of defined explicit groups.
Represents a "user group membership" – a specific instance of a user belonging to a group.
Value object representing a user's identity.
Interface for objects which can provide a MediaWiki context on request.
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
getReplicaDatabase( $domain=false, $group=null)
Get connection to a replica database.