MediaWiki master
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
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[] = 'user_id >= ' . $userIdentity->getId();
220 }
221 } else {
222 $conds[] = $dbr->expr( 'user_name', '>=', $this->requestedUser );
223 }
224 }
225
226 if ( $this->editsOnly ) {
227 $conds[] = '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:66
const NS_USER_TALK
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.
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()
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.