MediaWiki master
UsersPager.php
Go to the documentation of this file.
1<?php
26namespace MediaWiki\Pager;
27
51use stdClass;
54
63
67 protected $userGroupCache;
68
71
73 protected $editsOnly;
74
77
80
82 protected $creationSort;
83
85 protected $including;
86
88 protected $requestedUser;
89
91 protected $hideUserUtils;
92
93 private HookRunner $hookRunner;
94 private LinkBatchFactory $linkBatchFactory;
95 private UserGroupManager $userGroupManager;
96 private UserIdentityLookup $userIdentityLookup;
97 private TempUserConfig $tempUserConfig;
98
111 public function __construct(
112 IContextSource $context,
113 HookContainer $hookContainer,
114 LinkBatchFactory $linkBatchFactory,
115 IConnectionProvider $dbProvider,
116 UserGroupManager $userGroupManager,
117 UserIdentityLookup $userIdentityLookup,
119 $par,
121 ) {
122 $this->setContext( $context );
123
124 $request = $this->getRequest();
125 $par ??= '';
126 $parms = explode( '/', $par );
127 $symsForAll = [ '*', 'user' ];
128
129 if ( $parms[0] != '' &&
130 ( in_array( $par, $userGroupManager->listAllGroups() ) || in_array( $par, $symsForAll ) )
131 ) {
132 $this->requestedGroup = $par;
133 $un = $request->getText( 'username' );
134 } elseif ( count( $parms ) == 2 ) {
135 $this->requestedGroup = $parms[0];
136 $un = $parms[1];
137 } else {
138 $this->requestedGroup = $request->getVal( 'group' );
139 $un = ( $par != '' ) ? $par : $request->getText( 'username' );
140 }
141
142 if ( in_array( $this->requestedGroup, $symsForAll ) ) {
143 $this->requestedGroup = '';
144 }
145 $this->editsOnly = $request->getBool( 'editsOnly' );
146 $this->temporaryGroupsOnly = $request->getBool( 'temporaryGroupsOnly' );
147 $this->temporaryAccountsOnly = $request->getBool( 'temporaryAccountsOnly' );
148 $this->creationSort = $request->getBool( 'creationSort' );
149 $this->including = $including;
150 $this->mDefaultDirection = $request->getBool( 'desc' )
153
154 $this->requestedUser = '';
155
156 if ( $un != '' ) {
157 $username = Title::makeTitleSafe( NS_USER, $un );
158
159 if ( $username !== null ) {
160 $this->requestedUser = $username->getText();
161 }
162 }
163
164 // Set database before parent constructor to avoid setting it there
165 $this->mDb = $dbProvider->getReplicaDatabase();
166 parent::__construct();
167 $this->userGroupManager = $userGroupManager;
168 $this->hookRunner = new HookRunner( $hookContainer );
169 $this->linkBatchFactory = $linkBatchFactory;
170 $this->userIdentityLookup = $userIdentityLookup;
171 $this->hideUserUtils = $hideUserUtils;
172 $this->tempUserConfig = MediaWikiServices::getInstance()->getTempUserConfig();
173 }
174
178 public function getIndexField() {
179 return $this->creationSort ? 'user_id' : 'user_name';
180 }
181
185 public function getQueryInfo() {
186 $dbr = $this->getDatabase();
187 $conds = [];
188 $options = [];
189
190 // Don't show hidden names
191 if ( !$this->canSeeHideuser() ) {
192 $conds[] = $this->hideUserUtils->getExpression( $dbr );
193 $deleted = '1=0';
194 } else {
195 // In MySQL, there's no separate boolean type so getExpression()
196 // effectively returns an integer, and MAX() works on the result of it.
197 // In PostgreSQL, getExpression() returns a special boolean type which
198 // can't go into MAX(). So we have to cast it to support PostgreSQL.
199
200 // A neater PostgreSQL-only solution would be bool_or(), but MySQL
201 // doesn't have that or need it. We could add a wrapper to SQLPlatform
202 // which returns MAX() on MySQL and bool_or() on PostgreSQL.
203
204 // This would not be necessary if we used "GROUP BY user_name,user_id",
205 // but MariaDB forgets how to use indexes if you do that.
206 $deleted = 'MAX(' . $dbr->buildIntegerCast(
207 $this->hideUserUtils->getExpression( $dbr, 'user_id', HideUserUtils::HIDDEN_USERS )
208 ) . ')';
209 }
210
211 if ( $this->requestedGroup != '' || $this->temporaryGroupsOnly ) {
212 $cond = $dbr->expr( 'ug_expiry', '>=', $dbr->timestamp() );
213 if ( !$this->temporaryGroupsOnly ) {
214 $cond = $cond->or( 'ug_expiry', '=', null );
215 }
216 $conds[] = $cond;
217 }
218
219 if ( $this->temporaryAccountsOnly && $this->tempUserConfig->isKnown() ) {
220 $conds[] = $this->tempUserConfig->getMatchCondition(
221 $dbr, 'user_name', IExpression::LIKE
222 );
223 }
224
225 if ( $this->requestedGroup != '' ) {
226 $conds['ug_group'] = $this->requestedGroup;
227 }
228
229 if ( $this->requestedUser != '' ) {
230 # Sorted either by account creation or name
231 if ( $this->creationSort ) {
232 $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $this->requestedUser );
233 if ( $userIdentity && $userIdentity->isRegistered() ) {
234 $conds[] = $dbr->expr( 'user_id', '>=', $userIdentity->getId() );
235 }
236 } else {
237 $conds[] = $dbr->expr( 'user_name', '>=', $this->requestedUser );
238 }
239 }
240
241 if ( $this->editsOnly ) {
242 $conds[] = $dbr->expr( 'user_editcount', '>', 0 );
243 }
244
245 $options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name';
246
247 $query = [
248 'tables' => [
249 'user',
250 'user_groups',
251 'block_with_target' => [
252 'block_target',
253 'block'
254 ]
255 ],
256 'fields' => [
257 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name',
258 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)',
259 'edits' => 'MAX(user_editcount)',
260 'creation' => 'MIN(user_registration)',
261 'deleted' => $deleted, // block/hide status
262 'sitewide' => 'MAX(bl_sitewide)'
263 ],
264 'options' => $options,
265 'join_conds' => [
266 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ],
267 'block_with_target' => [
268 'LEFT JOIN', [
269 'user_id=bt_user',
270 'bt_auto' => 0
271 ]
272 ],
273 'block' => [ 'JOIN', 'bl_target=bt_id' ]
274 ],
275 'conds' => $conds
276 ];
277
278 $this->hookRunner->onSpecialListusersQueryInfo( $this, $query );
279
280 return $query;
281 }
282
287 public function formatRow( $row ) {
288 if ( $row->user_id == 0 ) { # T18487
289 return '';
290 }
291
292 $userName = $row->user_name;
293
294 $ulinks = Linker::userLink( $row->user_id, $userName );
295 $ulinks .= Linker::userToolLinksRedContribs(
296 $row->user_id,
297 $userName,
298 (int)$row->edits,
299 // don't render parentheses in HTML markup (CSS will provide)
300 false
301 );
302
303 $lang = $this->getLanguage();
304
305 $groups = '';
306 $userIdentity = new UserIdentityValue( intval( $row->user_id ), $userName );
307 $ugms = $this->getGroupMemberships( $userIdentity );
308
309 if ( !$this->including && count( $ugms ) > 0 ) {
310 $list = [];
311 foreach ( $ugms as $ugm ) {
312 $list[] = $this->buildGroupLink( $ugm, $userName );
313 }
314 $groups = $lang->commaList( $list );
315 }
316
317 $item = $lang->specialList( $ulinks, $groups );
318
319 if ( $row->deleted ) {
320 $item = "<span class=\"deleted\">$item</span>";
321 }
322
323 $edits = '';
324 if ( !$this->including && $this->getConfig()->get( MainConfigNames::Edititis ) ) {
325 $count = $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped();
326 $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped();
327 }
328
329 $created = '';
330 # Some rows may be null
331 if ( !$this->including && $row->creation ) {
332 $user = $this->getUser();
333 $d = $lang->userDate( $row->creation, $user );
334 $t = $lang->userTime( $row->creation, $user );
335 $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped();
336 $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped();
337 }
338
339 $blocked = $row->deleted !== null && $row->sitewide === '1' ?
340 ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() :
341 '';
342
343 $this->hookRunner->onSpecialListusersFormatRow( $item, $row );
344
345 return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" );
346 }
347
348 protected function doBatchLookups() {
349 $batch = $this->linkBatchFactory->newLinkBatch();
350 $userIds = [];
351 # Give some pointers to make user links
352 foreach ( $this->mResult as $row ) {
353 $batch->add( NS_USER, $row->user_name );
354 $batch->add( NS_USER_TALK, $row->user_name );
355 $userIds[] = (int)$row->user_id;
356 }
357
358 // Lookup groups for all the users
359 $queryBuilder = $this->userGroupManager->newQueryBuilder( $this->getDatabase() );
360 $groupRes = $queryBuilder->where( [ 'ug_user' => $userIds ] )
361 ->caller( __METHOD__ )
362 ->fetchResultSet();
363 $cache = [];
364 $groups = [];
365 foreach ( $groupRes as $row ) {
366 $ugm = $this->userGroupManager->newGroupMembershipFromRow( $row );
367 if ( !$ugm->isExpired() ) {
368 $cache[$row->ug_user][$row->ug_group] = $ugm;
369 $groups[$row->ug_group] = true;
370 }
371 }
372
373 // Give extensions a chance to add things like global user group data
374 // into the cache array to ensure proper output later on
375 $this->hookRunner->onUsersPagerDoBatchLookups( $this->getDatabase(), $userIds, $cache, $groups );
376
377 $this->userGroupCache = $cache;
378
379 // Add page of groups to link batch
380 foreach ( $groups as $group => $unused ) {
381 $groupPage = UserGroupMembership::getGroupPage( $group );
382 if ( $groupPage ) {
383 $batch->addObj( $groupPage );
384 }
385 }
386
387 $batch->execute();
388 $this->mResult->rewind();
389 }
390
394 public function getPageHeader() {
395 $self = explode( '/', $this->getTitle()->getPrefixedDBkey(), 2 )[0];
396
397 $groupOptions = [ $this->msg( 'group-all' )->text() => '' ];
398 foreach ( $this->getAllGroups() as $group => $groupText ) {
399 if ( array_key_exists( $groupText, $groupOptions ) ) {
400 LoggerFactory::getInstance( 'translation-problem' )->error(
401 'The group {group_one} has the same translation as {group_two} for {lang}. ' .
402 '{group_one} will not be displayed in group dropdown of the UsersPager.',
403 [
404 'group_one' => $group,
405 'group_two' => $groupOptions[$groupText],
406 'lang' => $this->getLanguage()->getCode(),
407 ]
408 );
409 continue;
410 }
411 $groupOptions[ $groupText ] = $group;
412 }
413
414 $formDescriptor = [
415 'user' => [
416 'class' => HTMLUserTextField::class,
417 'label' => $this->msg( 'listusersfrom' )->text(),
418 'name' => 'username',
419 'default' => $this->requestedUser,
420 ],
421 'dropdown' => [
422 'label' => $this->msg( 'group' )->text(),
423 'name' => 'group',
424 'default' => $this->requestedGroup,
425 'class' => HTMLSelectField::class,
426 'options' => $groupOptions,
427 ],
428 'editsOnly' => [
429 'type' => 'check',
430 'label' => $this->msg( 'listusers-editsonly' )->text(),
431 'name' => 'editsOnly',
432 'id' => 'editsOnly',
433 'default' => $this->editsOnly
434 ],
435 'temporaryGroupsOnly' => [
436 'type' => 'check',
437 'label' => $this->msg( 'listusers-temporarygroupsonly' )->text(),
438 'name' => 'temporaryGroupsOnly',
439 'id' => 'temporaryGroupsOnly',
440 'default' => $this->temporaryGroupsOnly
441 ],
442 ];
443
444 // If temporary accounts are known, add an option to filter for them
445 if ( $this->tempUserConfig->isKnown() ) {
446 $formDescriptor = array_merge( $formDescriptor, [
447 'temporaryAccountsOnly' => [
448 'type' => 'check',
449 'label' => $this->msg( 'listusers-temporaryaccountsonly' )->text(),
450 'name' => 'temporaryAccountsOnly',
451 'id' => 'temporaryAccountsOnly',
452 'default' => $this->temporaryAccountsOnly
453 ]
454 ] );
455 }
456
457 // Add sort options
458 $formDescriptor = array_merge( $formDescriptor, [
459 'creationSort' => [
460 'type' => 'check',
461 'label' => $this->msg( 'listusers-creationsort' )->text(),
462 'name' => 'creationSort',
463 'id' => 'creationSort',
464 'default' => $this->creationSort
465 ],
466 'desc' => [
467 'type' => 'check',
468 'label' => $this->msg( 'listusers-desc' )->text(),
469 'name' => 'desc',
470 'id' => 'desc',
471 'default' => $this->mDefaultDirection
472 ],
473 'limithiddenfield' => [
474 'class' => HTMLHiddenField::class,
475 'name' => 'limit',
476 'default' => $this->mLimit
477 ],
478 ] );
479
480 $beforeSubmitButtonHookOut = '';
481 $this->hookRunner->onSpecialListusersHeaderForm( $this, $beforeSubmitButtonHookOut );
482
483 if ( $beforeSubmitButtonHookOut !== '' ) {
484 $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
485 'class' => HTMLInfoField::class,
486 'raw' => true,
487 'default' => $beforeSubmitButtonHookOut
488 ];
489 }
490
491 $formDescriptor[ 'submit' ] = [
492 'class' => HTMLSubmitField::class,
493 'buttonlabel-message' => 'listusers-submit',
494 ];
495
496 $beforeClosingFieldsetHookOut = '';
497 $this->hookRunner->onSpecialListusersHeader( $this, $beforeClosingFieldsetHookOut );
498
499 if ( $beforeClosingFieldsetHookOut !== '' ) {
500 $formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [
501 'class' => HTMLInfoField::class,
502 'raw' => true,
503 'default' => $beforeClosingFieldsetHookOut
504 ];
505 }
506
507 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
508 $htmlForm
509 ->setMethod( 'get' )
510 ->setTitle( Title::newFromText( $self ) )
511 ->setId( 'mw-listusers-form' )
512 ->setFormIdentifier( 'mw-listusers-form' )
513 ->suppressDefaultSubmit()
514 ->setWrapperLegendMsg( 'listusers' );
515 return $htmlForm->prepareForm()->getHTML( true );
516 }
517
518 protected function canSeeHideuser() {
519 return $this->getAuthority()->isAllowed( 'hideuser' );
520 }
521
526 private function getAllGroups() {
527 $result = [];
528 $lang = $this->getLanguage();
529 foreach ( $this->userGroupManager->listAllGroups() as $group ) {
530 $result[$group] = $lang->getGroupName( $group );
531 }
532 asort( $result );
533
534 return $result;
535 }
536
541 public function getDefaultQuery() {
542 $query = parent::getDefaultQuery();
543 if ( $this->requestedGroup != '' ) {
544 $query['group'] = $this->requestedGroup;
545 }
546 if ( $this->requestedUser != '' ) {
547 $query['username'] = $this->requestedUser;
548 }
549 $this->hookRunner->onSpecialListusersDefaultQuery( $this, $query );
550
551 return $query;
552 }
553
561 protected function getGroupMemberships( $user ) {
562 if ( $this->userGroupCache === null ) {
563 return $this->userGroupManager->getUserGroupMemberships( $user );
564 } else {
565 return $this->userGroupCache[$user->getId()] ?? [];
566 }
567 }
568
576 protected function buildGroupLink( $group, $username ) {
577 return UserGroupMembership::getLinkHTML( $group, $this->getContext(), $username );
578 }
579}
580
585class_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:209
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()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
IndexPager with an alphabetic list and a formatted navigation bar.
getDatabase()
Get the Database object in use.
const DIR_ASCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
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:78
Manage user group memberships.
listAllGroups()
Return the set of defined explicit groups.
Represents the membership of one user in one user group.
Value object representing a user's identity.
Interface for objects which can provide a MediaWiki context on request.
Interface for temporary user creation config and name matching.
Service for looking up UserIdentity.
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
getReplicaDatabase( $domain=false, $group=null)
Get connection to a replica database.