MediaWiki master
UsersPager.php
Go to the documentation of this file.
1<?php
26namespace MediaWiki\Pager;
27
28use HTMLForm;
47use stdClass;
49
58
62 protected $userGroupCache;
63
66
68 protected $editsOnly;
69
72
74 protected $creationSort;
75
77 protected $including;
78
80 protected $requestedUser;
81
84
85 private HookRunner $hookRunner;
86 private LinkBatchFactory $linkBatchFactory;
87 private UserGroupManager $userGroupManager;
88 private UserIdentityLookup $userIdentityLookup;
89
101 public function __construct(
102 IContextSource $context,
103 HookContainer $hookContainer,
104 LinkBatchFactory $linkBatchFactory,
105 IConnectionProvider $dbProvider,
106 UserGroupManager $userGroupManager,
107 UserIdentityLookup $userIdentityLookup,
108 $par,
110 ) {
111 $this->setContext( $context );
112
113 $request = $this->getRequest();
114 $par ??= '';
115 $parms = explode( '/', $par );
116 $symsForAll = [ '*', 'user' ];
117
118 if ( $parms[0] != '' &&
119 ( in_array( $par, $userGroupManager->listAllGroups() ) || in_array( $par, $symsForAll ) )
120 ) {
121 $this->requestedGroup = $par;
122 $un = $request->getText( 'username' );
123 } elseif ( count( $parms ) == 2 ) {
124 $this->requestedGroup = $parms[0];
125 $un = $parms[1];
126 } else {
127 $this->requestedGroup = $request->getVal( 'group' );
128 $un = ( $par != '' ) ? $par : $request->getText( 'username' );
129 }
130
131 if ( in_array( $this->requestedGroup, $symsForAll ) ) {
132 $this->requestedGroup = '';
133 }
134 $this->editsOnly = $request->getBool( 'editsOnly' );
135 $this->temporaryGroupsOnly = $request->getBool( 'temporaryGroupsOnly' );
136 $this->creationSort = $request->getBool( 'creationSort' );
137 $this->including = $including;
138 $this->mDefaultDirection = $request->getBool( 'desc' )
141
142 $this->requestedUser = '';
143
144 if ( $un != '' ) {
145 $username = Title::makeTitleSafe( NS_USER, $un );
146
147 if ( $username !== null ) {
148 $this->requestedUser = $username->getText();
149 }
150 }
151
152 // Set database before parent constructor to avoid setting it there with wfGetDB
153 $this->mDb = $dbProvider->getReplicaDatabase();
154 parent::__construct();
155 $this->userGroupManager = $userGroupManager;
156 $this->hookRunner = new HookRunner( $hookContainer );
157 $this->linkBatchFactory = $linkBatchFactory;
158 $this->userIdentityLookup = $userIdentityLookup;
159 $this->blockTargetReadStage = $this->getConfig()
161 }
162
166 public function getIndexField() {
167 return $this->creationSort ? 'user_id' : 'user_name';
168 }
169
173 public function getQueryInfo() {
174 $dbr = $this->getDatabase();
175 $conds = [];
176 $options = [];
177
178 // Don't show hidden names
179 if ( !$this->canSeeHideuser() ) {
180 if ( $this->blockTargetReadStage === SCHEMA_COMPAT_READ_OLD ) {
181 $conds['ipb_deleted'] = [ null, 0 ];
182 } else {
183 // With multiblocks a target might be blocked with both deleted
184 // and non-deleted options. WHERE bl_deleted=0 would filter out
185 // deleted blocks but we would still see the user with a
186 // non-deleted block. So we need HAVING.
187 $options['HAVING']['deleted'] = [ null, 0 ];
188 }
189 }
190
191 if ( $this->requestedGroup != '' || $this->temporaryGroupsOnly ) {
192 $cond = $dbr->expr( 'ug_expiry', '>=', $dbr->timestamp() );
193 if ( !$this->temporaryGroupsOnly ) {
194 $cond = $cond->or( 'ug_expiry', '=', null );
195 }
196 $conds[] = $cond;
197 }
198
199 if ( $this->requestedGroup != '' ) {
200 $conds['ug_group'] = $this->requestedGroup;
201 }
202
203 if ( $this->requestedUser != '' ) {
204 # Sorted either by account creation or name
205 if ( $this->creationSort ) {
206 $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $this->requestedUser );
207 if ( $userIdentity && $userIdentity->isRegistered() ) {
208 $conds[] = 'user_id >= ' . $userIdentity->getId();
209 }
210 } else {
211 $conds[] = $dbr->expr( 'user_name', '>=', $this->requestedUser );
212 }
213 }
214
215 if ( $this->editsOnly ) {
216 $conds[] = 'user_editcount > 0';
217 }
218
219 $options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name';
220
221 if ( $this->blockTargetReadStage === SCHEMA_COMPAT_READ_OLD ) {
222 $query = [
223 'tables' => [ 'user', 'user_groups', 'ipblocks' ],
224 'fields' => [
225 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name',
226 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)',
227 'edits' => 'MAX(user_editcount)',
228 'creation' => 'MIN(user_registration)',
229 'deleted' => 'MAX(ipb_deleted)', // block/hide status
230 'sitewide' => 'MAX(ipb_sitewide)'
231 ],
232 'options' => $options,
233 'join_conds' => [
234 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ],
235 'ipblocks' => [
236 'LEFT JOIN', [
237 'user_id=ipb_user',
238 'ipb_auto' => 0
239 ]
240 ],
241 ],
242 'conds' => $conds
243 ];
244 } else {
245 $query = [
246 'tables' => [
247 'user',
248 'user_groups',
249 'block_with_target' => [
250 'block_target',
251 'block'
252 ]
253 ],
254 'fields' => [
255 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name',
256 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)',
257 'edits' => 'MAX(user_editcount)',
258 'creation' => 'MIN(user_registration)',
259 'deleted' => 'MAX(bl_deleted)', // block/hide status
260 'sitewide' => 'MAX(bl_sitewide)'
261 ],
262 'options' => $options,
263 'join_conds' => [
264 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ],
265 'block_with_target' => [
266 'LEFT JOIN', [
267 'user_id=bt_user',
268 'bt_auto' => 0
269 ]
270 ],
271 'block' => [ 'JOIN', 'bl_target=bt_id' ]
272 ],
273 'conds' => $conds
274 ];
275 }
276
277 $this->hookRunner->onSpecialListusersQueryInfo( $this, $query );
278
279 return $query;
280 }
281
286 public function formatRow( $row ) {
287 if ( $row->user_id == 0 ) { # T18487
288 return '';
289 }
290
291 $userName = $row->user_name;
292
293 $ulinks = Linker::userLink( $row->user_id, $userName );
294 $ulinks .= Linker::userToolLinksRedContribs(
295 $row->user_id,
296 $userName,
297 (int)$row->edits,
298 // don't render parentheses in HTML markup (CSS will provide)
299 false
300 );
301
302 $lang = $this->getLanguage();
303
304 $groups = '';
305 $userIdentity = new UserIdentityValue( intval( $row->user_id ), $userName );
306 $ugms = $this->getGroupMemberships( $userIdentity );
307
308 if ( !$this->including && count( $ugms ) > 0 ) {
309 $list = [];
310 foreach ( $ugms as $ugm ) {
311 $list[] = $this->buildGroupLink( $ugm, $userName );
312 }
313 $groups = $lang->commaList( $list );
314 }
315
316 $item = $lang->specialList( $ulinks, $groups );
317
318 if ( $row->deleted ) {
319 $item = "<span class=\"deleted\">$item</span>";
320 }
321
322 $edits = '';
323 if ( !$this->including && $this->getConfig()->get( MainConfigNames::Edititis ) ) {
324 $count = $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped();
325 $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped();
326 }
327
328 $created = '';
329 # Some rows may be null
330 if ( !$this->including && $row->creation ) {
331 $user = $this->getUser();
332 $d = $lang->userDate( $row->creation, $user );
333 $t = $lang->userTime( $row->creation, $user );
334 $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped();
335 $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped();
336 }
337
338 $blocked = $row->deleted !== null && $row->sitewide === '1' ?
339 ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() :
340 '';
341
342 $this->hookRunner->onSpecialListusersFormatRow( $item, $row );
343
344 return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" );
345 }
346
347 protected function doBatchLookups() {
348 $batch = $this->linkBatchFactory->newLinkBatch();
349 $userIds = [];
350 # Give some pointers to make user links
351 foreach ( $this->mResult as $row ) {
352 $batch->add( NS_USER, $row->user_name );
353 $batch->add( NS_USER_TALK, $row->user_name );
354 $userIds[] = (int)$row->user_id;
355 }
356
357 // Lookup groups for all the users
358 $queryBuilder = $this->userGroupManager->newQueryBuilder( $this->getDatabase() );
359 $groupRes = $queryBuilder->where( [ 'ug_user' => $userIds ] )
360 ->caller( __METHOD__ )
361 ->fetchResultSet();
362 $cache = [];
363 $groups = [];
364 foreach ( $groupRes as $row ) {
365 $ugm = $this->userGroupManager->newGroupMembershipFromRow( $row );
366 if ( !$ugm->isExpired() ) {
367 $cache[$row->ug_user][$row->ug_group] = $ugm;
368 $groups[$row->ug_group] = true;
369 }
370 }
371
372 // Give extensions a chance to add things like global user group data
373 // into the cache array to ensure proper output later on
374 $this->hookRunner->onUsersPagerDoBatchLookups( $this->getDatabase(), $userIds, $cache, $groups );
375
376 $this->userGroupCache = $cache;
377
378 // Add page of groups to link batch
379 foreach ( $groups as $group => $unused ) {
380 $groupPage = UserGroupMembership::getGroupPage( $group );
381 if ( $groupPage ) {
382 $batch->addObj( $groupPage );
383 }
384 }
385
386 $batch->execute();
387 $this->mResult->rewind();
388 }
389
393 public function getPageHeader() {
394 $self = explode( '/', $this->getTitle()->getPrefixedDBkey(), 2 )[0];
395
396 $groupOptions = [ $this->msg( 'group-all' )->text() => '' ];
397 foreach ( $this->getAllGroups() as $group => $groupText ) {
398 $groupOptions[ $groupText ] = $group;
399 }
400
401 $formDescriptor = [
402 'user' => [
403 'class' => HTMLUserTextField::class,
404 'label' => $this->msg( 'listusersfrom' )->text(),
405 'name' => 'username',
406 'default' => $this->requestedUser,
407 ],
408 'dropdown' => [
409 'label' => $this->msg( 'group' )->text(),
410 'name' => 'group',
411 'default' => $this->requestedGroup,
412 'class' => HTMLSelectField::class,
413 'options' => $groupOptions,
414 ],
415 'editsOnly' => [
416 'type' => 'check',
417 'label' => $this->msg( 'listusers-editsonly' )->text(),
418 'name' => 'editsOnly',
419 'id' => 'editsOnly',
420 'default' => $this->editsOnly
421 ],
422 'temporaryGroupsOnly' => [
423 'type' => 'check',
424 'label' => $this->msg( 'listusers-temporarygroupsonly' )->text(),
425 'name' => 'temporaryGroupsOnly',
426 'id' => 'temporaryGroupsOnly',
427 'default' => $this->temporaryGroupsOnly
428 ],
429 'creationSort' => [
430 'type' => 'check',
431 'label' => $this->msg( 'listusers-creationsort' )->text(),
432 'name' => 'creationSort',
433 'id' => 'creationSort',
434 'default' => $this->creationSort
435 ],
436 'desc' => [
437 'type' => 'check',
438 'label' => $this->msg( 'listusers-desc' )->text(),
439 'name' => 'desc',
440 'id' => 'desc',
441 'default' => $this->mDefaultDirection
442 ],
443 'limithiddenfield' => [
444 'class' => HTMLHiddenField::class,
445 'name' => 'limit',
446 'default' => $this->mLimit
447 ]
448 ];
449
450 $beforeSubmitButtonHookOut = '';
451 $this->hookRunner->onSpecialListusersHeaderForm( $this, $beforeSubmitButtonHookOut );
452
453 if ( $beforeSubmitButtonHookOut !== '' ) {
454 $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
455 'class' => HTMLInfoField::class,
456 'raw' => true,
457 'default' => $beforeSubmitButtonHookOut
458 ];
459 }
460
461 $formDescriptor[ 'submit' ] = [
462 'class' => HTMLSubmitField::class,
463 'buttonlabel-message' => 'listusers-submit',
464 ];
465
466 $beforeClosingFieldsetHookOut = '';
467 $this->hookRunner->onSpecialListusersHeader( $this, $beforeClosingFieldsetHookOut );
468
469 if ( $beforeClosingFieldsetHookOut !== '' ) {
470 $formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [
471 'class' => HTMLInfoField::class,
472 'raw' => true,
473 'default' => $beforeClosingFieldsetHookOut
474 ];
475 }
476
477 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
478 $htmlForm
479 ->setMethod( 'get' )
480 ->setTitle( Title::newFromText( $self ) )
481 ->setId( 'mw-listusers-form' )
482 ->setFormIdentifier( 'mw-listusers-form' )
483 ->suppressDefaultSubmit()
484 ->setWrapperLegendMsg( 'listusers' );
485 return $htmlForm->prepareForm()->getHTML( true );
486 }
487
488 protected function canSeeHideuser() {
489 return $this->getAuthority()->isAllowed( 'hideuser' );
490 }
491
496 private function getAllGroups() {
497 $result = [];
498 $lang = $this->getLanguage();
499 foreach ( $this->userGroupManager->listAllGroups() as $group ) {
500 $result[$group] = $lang->getGroupName( $group );
501 }
502 asort( $result );
503
504 return $result;
505 }
506
511 public function getDefaultQuery() {
512 $query = parent::getDefaultQuery();
513 if ( $this->requestedGroup != '' ) {
514 $query['group'] = $this->requestedGroup;
515 }
516 if ( $this->requestedUser != '' ) {
517 $query['username'] = $this->requestedUser;
518 }
519 $this->hookRunner->onSpecialListusersDefaultQuery( $this, $query );
520
521 return $query;
522 }
523
531 protected function getGroupMemberships( $user ) {
532 if ( $this->userGroupCache === null ) {
533 return $this->userGroupManager->getUserGroupMemberships( $user );
534 } else {
535 return $this->userGroupCache[$user->getId()] ?? [];
536 }
537 }
538
546 protected function buildGroupLink( $group, $username ) {
547 return UserGroupMembership::getLinkHTML( $group, $this->getContext(), $username );
548 }
549}
550
555class_alias( UsersPager::class, 'UsersPager' );
const NS_USER
Definition Defines.php:66
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:264
const NS_USER_TALK
Definition Defines.php:67
const SCHEMA_COMPAT_READ_MASK
Definition Defines.php:270
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
setContext(IContextSource $context)
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:158
An information field (text blob), not a proper input.
A select dropdown field.
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.
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:65
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.
__construct(IContextSource $context, HookContainer $hookContainer, LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, UserGroupManager $userGroupManager, UserIdentityLookup $userIdentityLookup, $par, $including)
getGroupMemberships( $user)
Get an associative array containing groups the specified user belongs to, and the relevant UserGroupM...
buildGroupLink( $group, $username)
Format a link to a group description page.
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.