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