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