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