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