Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 285
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
UsersPager
0.00% covered (danger)
0.00%
0 / 284
0.00% covered (danger)
0.00%
0 / 11
3782
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
110
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
272
 formatRow
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
156
 doBatchLookups
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 getPageHeader
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 1
42
 canSeeHideuser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllGroups
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getDefaultQuery
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupMemberships
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 buildGroupLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2004 Brooke Vibber, lcrocker, Tim Starling,
4 * Domas Mituzas, Antoine Musso, Jens Frank, Zhengzhu,
5 * 2006 Rob Church <robchur@gmail.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * http://www.gnu.org/copyleft/gpl.html
21 *
22 * @file
23 * @ingroup Pager
24 */
25
26namespace MediaWiki\Pager;
27
28use MediaWiki\Block\HideUserUtils;
29use MediaWiki\Cache\LinkBatchFactory;
30use MediaWiki\Context\IContextSource;
31use MediaWiki\HookContainer\HookContainer;
32use MediaWiki\HookContainer\HookRunner;
33use MediaWiki\Html\Html;
34use MediaWiki\HTMLForm\Field\HTMLHiddenField;
35use MediaWiki\HTMLForm\Field\HTMLInfoField;
36use MediaWiki\HTMLForm\Field\HTMLSelectField;
37use MediaWiki\HTMLForm\Field\HTMLSubmitField;
38use MediaWiki\HTMLForm\Field\HTMLUserTextField;
39use MediaWiki\HTMLForm\HTMLForm;
40use MediaWiki\Linker\Linker;
41use MediaWiki\Logger\LoggerFactory;
42use MediaWiki\MainConfigNames;
43use MediaWiki\MediaWikiServices;
44use MediaWiki\Title\Title;
45use MediaWiki\User\TempUser\TempUserConfig;
46use MediaWiki\User\UserGroupManager;
47use MediaWiki\User\UserGroupMembership;
48use MediaWiki\User\UserIdentity;
49use MediaWiki\User\UserIdentityLookup;
50use MediaWiki\User\UserIdentityValue;
51use stdClass;
52use Wikimedia\Rdbms\IConnectionProvider;
53use Wikimedia\Rdbms\IExpression;
54
55/**
56 * This class is used to get a list of user. The ones with specials
57 * rights (sysop, bureaucrat, developer) will have them displayed
58 * next to their names.
59 *
60 * @ingroup Pager
61 */
62class UsersPager extends AlphabeticPager {
63
64    /**
65     * @var array[] A array with user ids as key and a array of groups as value
66     */
67    protected $userGroupCache;
68
69    /** @var string */
70    public $requestedGroup;
71
72    /** @var bool */
73    protected $editsOnly;
74
75    /** @var bool */
76    protected $temporaryGroupsOnly;
77
78    /** @var bool */
79    protected $temporaryAccountsOnly;
80
81    /** @var bool */
82    protected $creationSort;
83
84    /** @var bool|null */
85    protected $including;
86
87    /** @var string */
88    protected $requestedUser;
89
90    /** @var HideUserUtils */
91    protected $hideUserUtils;
92
93    private HookRunner $hookRunner;
94    private LinkBatchFactory $linkBatchFactory;
95    private UserGroupManager $userGroupManager;
96    private UserIdentityLookup $userIdentityLookup;
97    private TempUserConfig $tempUserConfig;
98
99    /**
100     * @param IContextSource $context
101     * @param HookContainer $hookContainer
102     * @param LinkBatchFactory $linkBatchFactory
103     * @param IConnectionProvider $dbProvider
104     * @param UserGroupManager $userGroupManager
105     * @param UserIdentityLookup $userIdentityLookup
106     * @param HideUserUtils $hideUserUtils
107     * @param string|null $par
108     * @param bool|null $including Whether this page is being transcluded in
109     * another page
110     */
111    public function __construct(
112        IContextSource $context,
113        HookContainer $hookContainer,
114        LinkBatchFactory $linkBatchFactory,
115        IConnectionProvider $dbProvider,
116        UserGroupManager $userGroupManager,
117        UserIdentityLookup $userIdentityLookup,
118        HideUserUtils $hideUserUtils,
119        $par,
120        $including
121    ) {
122        $this->setContext( $context );
123
124        $request = $this->getRequest();
125        $par ??= '';
126        $parms = explode( '/', $par );
127        $symsForAll = [ '*', 'user' ];
128
129        if ( $parms[0] != '' &&
130            ( in_array( $par, $userGroupManager->listAllGroups() ) || in_array( $par, $symsForAll ) )
131        ) {
132            $this->requestedGroup = $par;
133            $un = $request->getText( 'username' );
134        } elseif ( count( $parms ) == 2 ) {
135            $this->requestedGroup = $parms[0];
136            $un = $parms[1];
137        } else {
138            $this->requestedGroup = $request->getVal( 'group' );
139            $un = ( $par != '' ) ? $par : $request->getText( 'username' );
140        }
141
142        if ( in_array( $this->requestedGroup, $symsForAll ) ) {
143            $this->requestedGroup = '';
144        }
145        $this->editsOnly = $request->getBool( 'editsOnly' );
146        $this->temporaryGroupsOnly = $request->getBool( 'temporaryGroupsOnly' );
147        $this->temporaryAccountsOnly = $request->getBool( 'temporaryAccountsOnly' );
148        $this->creationSort = $request->getBool( 'creationSort' );
149        $this->including = $including;
150        $this->mDefaultDirection = $request->getBool( 'desc' )
151            ? IndexPager::DIR_DESCENDING
152            : IndexPager::DIR_ASCENDING;
153
154        $this->requestedUser = '';
155
156        if ( $un != '' ) {
157            $username = Title::makeTitleSafe( NS_USER, $un );
158
159            if ( $username !== null ) {
160                $this->requestedUser = $username->getText();
161            }
162        }
163
164        // Set database before parent constructor to avoid setting it there
165        $this->mDb = $dbProvider->getReplicaDatabase();
166        parent::__construct();
167        $this->userGroupManager = $userGroupManager;
168        $this->hookRunner = new HookRunner( $hookContainer );
169        $this->linkBatchFactory = $linkBatchFactory;
170        $this->userIdentityLookup = $userIdentityLookup;
171        $this->hideUserUtils = $hideUserUtils;
172        $this->tempUserConfig = MediaWikiServices::getInstance()->getTempUserConfig();
173    }
174
175    /**
176     * @return string
177     */
178    public function getIndexField() {
179        return $this->creationSort ? 'user_id' : 'user_name';
180    }
181
182    /**
183     * @return array
184     */
185    public function getQueryInfo() {
186        $dbr = $this->getDatabase();
187        $conds = [];
188        $options = [];
189
190        // Don't show hidden names
191        if ( !$this->canSeeHideuser() ) {
192            $conds[] = $this->hideUserUtils->getExpression( $dbr );
193            $deleted = '1=0';
194        } else {
195            // In MySQL, there's no separate boolean type so getExpression()
196            // effectively returns an integer, and MAX() works on the result of it.
197            // In PostgreSQL, getExpression() returns a special boolean type which
198            // can't go into MAX(). So we have to cast it to support PostgreSQL.
199
200            // A neater PostgreSQL-only solution would be bool_or(), but MySQL
201            // doesn't have that or need it. We could add a wrapper to SQLPlatform
202            // which returns MAX() on MySQL and bool_or() on PostgreSQL.
203
204            // This would not be necessary if we used "GROUP BY user_name,user_id",
205            // but MariaDB forgets how to use indexes if you do that.
206            $deleted = 'MAX(' . $dbr->buildIntegerCast(
207                $this->hideUserUtils->getExpression( $dbr, 'user_id', HideUserUtils::HIDDEN_USERS )
208            ) . ')';
209        }
210
211        if ( $this->requestedGroup != '' || $this->temporaryGroupsOnly ) {
212            $cond = $dbr->expr( 'ug_expiry', '>=', $dbr->timestamp() );
213            if ( !$this->temporaryGroupsOnly ) {
214                $cond = $cond->or( 'ug_expiry', '=', null );
215            }
216            $conds[] = $cond;
217        }
218
219        if ( $this->temporaryAccountsOnly && $this->tempUserConfig->isKnown() ) {
220            $conds[] = $this->tempUserConfig->getMatchCondition(
221                $dbr, 'user_name', IExpression::LIKE
222            );
223        }
224
225        if ( $this->requestedGroup != '' ) {
226            $conds['ug_group'] = $this->requestedGroup;
227        }
228
229        if ( $this->requestedUser != '' ) {
230            # Sorted either by account creation or name
231            if ( $this->creationSort ) {
232                $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $this->requestedUser );
233                if ( $userIdentity && $userIdentity->isRegistered() ) {
234                    $conds[] = $dbr->expr( 'user_id', '>=', $userIdentity->getId() );
235                }
236            } else {
237                $conds[] = $dbr->expr( 'user_name', '>=', $this->requestedUser );
238            }
239        }
240
241        if ( $this->editsOnly ) {
242            $conds[] = $dbr->expr( 'user_editcount', '>', 0 );
243        }
244
245        $options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name';
246
247        $query = [
248            'tables' => [
249                'user',
250                'user_groups',
251                'block_with_target' => [
252                    'block_target',
253                    'block'
254                ]
255            ],
256            'fields' => [
257                'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name',
258                'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)',
259                'edits' => 'MAX(user_editcount)',
260                'creation' => 'MIN(user_registration)',
261                'deleted' => $deleted, // block/hide status
262                'sitewide' => 'MAX(bl_sitewide)'
263            ],
264            'options' => $options,
265            'join_conds' => [
266                'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ],
267                'block_with_target' => [
268                    'LEFT JOIN', [
269                        'user_id=bt_user',
270                        'bt_auto' => 0
271                    ]
272                ],
273                'block' => [ 'JOIN', 'bl_target=bt_id' ]
274            ],
275            'conds' => $conds
276        ];
277
278        $this->hookRunner->onSpecialListusersQueryInfo( $this, $query );
279
280        return $query;
281    }
282
283    /**
284     * @param stdClass $row
285     * @return string
286     */
287    public function formatRow( $row ) {
288        if ( $row->user_id == 0 ) { # T18487
289            return '';
290        }
291
292        $userName = $row->user_name;
293
294        $ulinks = Linker::userLink( $row->user_id, $userName );
295        $ulinks .= Linker::userToolLinksRedContribs(
296            $row->user_id,
297            $userName,
298            (int)$row->edits,
299            // don't render parentheses in HTML markup (CSS will provide)
300            false
301        );
302
303        $lang = $this->getLanguage();
304
305        $groups = '';
306        $userIdentity = new UserIdentityValue( intval( $row->user_id ), $userName );
307        $ugms = $this->getGroupMemberships( $userIdentity );
308
309        if ( !$this->including && count( $ugms ) > 0 ) {
310            $list = [];
311            foreach ( $ugms as $ugm ) {
312                $list[] = $this->buildGroupLink( $ugm, $userName );
313            }
314            $groups = $lang->commaList( $list );
315        }
316
317        $item = $lang->specialList( $ulinks, $groups );
318
319        if ( $row->deleted ) {
320            $item = "<span class=\"deleted\">$item</span>";
321        }
322
323        $edits = '';
324        if ( !$this->including && $this->getConfig()->get( MainConfigNames::Edititis ) ) {
325            $count = $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped();
326            $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped();
327        }
328
329        $created = '';
330        # Some rows may be null
331        if ( !$this->including && $row->creation ) {
332            $user = $this->getUser();
333            $d = $lang->userDate( $row->creation, $user );
334            $t = $lang->userTime( $row->creation, $user );
335            $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped();
336            $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped();
337        }
338
339        $blocked = $row->deleted !== null && $row->sitewide === '1' ?
340            ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() :
341            '';
342
343        $this->hookRunner->onSpecialListusersFormatRow( $item, $row );
344
345        return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" );
346    }
347
348    protected function doBatchLookups() {
349        $batch = $this->linkBatchFactory->newLinkBatch();
350        $userIds = [];
351        # Give some pointers to make user links
352        foreach ( $this->mResult as $row ) {
353            $batch->add( NS_USER, $row->user_name );
354            $batch->add( NS_USER_TALK, $row->user_name );
355            $userIds[] = (int)$row->user_id;
356        }
357
358        // Lookup groups for all the users
359        $queryBuilder = $this->userGroupManager->newQueryBuilder( $this->getDatabase() );
360        $groupRes = $queryBuilder->where( [ 'ug_user' => $userIds ] )
361            ->caller( __METHOD__ )
362            ->fetchResultSet();
363        $cache = [];
364        $groups = [];
365        foreach ( $groupRes as $row ) {
366            $ugm = $this->userGroupManager->newGroupMembershipFromRow( $row );
367            if ( !$ugm->isExpired() ) {
368                $cache[$row->ug_user][$row->ug_group] = $ugm;
369                $groups[$row->ug_group] = true;
370            }
371        }
372
373        // Give extensions a chance to add things like global user group data
374        // into the cache array to ensure proper output later on
375        $this->hookRunner->onUsersPagerDoBatchLookups( $this->getDatabase(), $userIds, $cache, $groups );
376
377        $this->userGroupCache = $cache;
378
379        // Add page of groups to link batch
380        foreach ( $groups as $group => $unused ) {
381            $groupPage = UserGroupMembership::getGroupPage( $group );
382            if ( $groupPage ) {
383                $batch->addObj( $groupPage );
384            }
385        }
386
387        $batch->execute();
388        $this->mResult->rewind();
389    }
390
391    /**
392     * @return string
393     */
394    public function getPageHeader() {
395        $self = explode( '/', $this->getTitle()->getPrefixedDBkey(), 2 )[0];
396
397        $groupOptions = [ $this->msg( 'group-all' )->text() => '' ];
398        foreach ( $this->getAllGroups() as $group => $groupText ) {
399            if ( array_key_exists( $groupText, $groupOptions ) ) {
400                LoggerFactory::getInstance( 'translation-problem' )->error(
401                    'The group {group_one} has the same translation as {group_two} for {lang}. ' .
402                    '{group_one} will not be displayed in group dropdown of the UsersPager.',
403                    [
404                        'group_one' => $group,
405                        'group_two' => $groupOptions[$groupText],
406                        'lang' => $this->getLanguage()->getCode(),
407                    ]
408                );
409                continue;
410            }
411            $groupOptions[ $groupText ] = $group;
412        }
413
414        $formDescriptor = [
415            'user' => [
416                'class' => HTMLUserTextField::class,
417                'label' => $this->msg( 'listusersfrom' )->text(),
418                'name' => 'username',
419                'default' => $this->requestedUser,
420            ],
421            'dropdown' => [
422                'label' => $this->msg( 'group' )->text(),
423                'name' => 'group',
424                'default' => $this->requestedGroup,
425                'class' => HTMLSelectField::class,
426                'options' => $groupOptions,
427            ],
428            'editsOnly' => [
429                'type' => 'check',
430                'label' => $this->msg( 'listusers-editsonly' )->text(),
431                'name' => 'editsOnly',
432                'id' => 'editsOnly',
433                'default' => $this->editsOnly
434            ],
435            'temporaryGroupsOnly' => [
436                'type' => 'check',
437                'label' => $this->msg( 'listusers-temporarygroupsonly' )->text(),
438                'name' => 'temporaryGroupsOnly',
439                'id' => 'temporaryGroupsOnly',
440                'default' => $this->temporaryGroupsOnly
441            ],
442        ];
443
444        // If temporary accounts are known, add an option to filter for them
445        if ( $this->tempUserConfig->isKnown() ) {
446            $formDescriptor = array_merge( $formDescriptor, [
447                'temporaryAccountsOnly' => [
448                    'type' => 'check',
449                    'label' => $this->msg( 'listusers-temporaryaccountsonly' )->text(),
450                    'name' => 'temporaryAccountsOnly',
451                    'id' => 'temporaryAccountsOnly',
452                    'default' => $this->temporaryAccountsOnly
453                ]
454            ] );
455        }
456
457        // Add sort options
458        $formDescriptor = array_merge( $formDescriptor, [
459            'creationSort' => [
460                'type' => 'check',
461                'label' => $this->msg( 'listusers-creationsort' )->text(),
462                'name' => 'creationSort',
463                'id' => 'creationSort',
464                'default' => $this->creationSort
465            ],
466            'desc' => [
467                'type' => 'check',
468                'label' => $this->msg( 'listusers-desc' )->text(),
469                'name' => 'desc',
470                'id' => 'desc',
471                'default' => $this->mDefaultDirection
472            ],
473            'limithiddenfield' => [
474                'class' => HTMLHiddenField::class,
475                'name' => 'limit',
476                'default' => $this->mLimit
477            ],
478        ] );
479
480        $beforeSubmitButtonHookOut = '';
481        $this->hookRunner->onSpecialListusersHeaderForm( $this, $beforeSubmitButtonHookOut );
482
483        if ( $beforeSubmitButtonHookOut !== '' ) {
484            $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
485                'class' => HTMLInfoField::class,
486                'raw' => true,
487                'default' => $beforeSubmitButtonHookOut
488            ];
489        }
490
491        $formDescriptor[ 'submit' ] = [
492            'class' => HTMLSubmitField::class,
493            'buttonlabel-message' => 'listusers-submit',
494        ];
495
496        $beforeClosingFieldsetHookOut = '';
497        $this->hookRunner->onSpecialListusersHeader( $this, $beforeClosingFieldsetHookOut );
498
499        if ( $beforeClosingFieldsetHookOut !== '' ) {
500            $formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [
501                'class' => HTMLInfoField::class,
502                'raw' => true,
503                'default' => $beforeClosingFieldsetHookOut
504            ];
505        }
506
507        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
508        $htmlForm
509            ->setMethod( 'get' )
510            ->setTitle( Title::newFromText( $self ) )
511            ->setId( 'mw-listusers-form' )
512            ->setFormIdentifier( 'mw-listusers-form' )
513            ->suppressDefaultSubmit()
514            ->setWrapperLegendMsg( 'listusers' );
515        return $htmlForm->prepareForm()->getHTML( true );
516    }
517
518    protected function canSeeHideuser() {
519        return $this->getAuthority()->isAllowed( 'hideuser' );
520    }
521
522    /**
523     * Get a list of all explicit groups
524     * @return array
525     */
526    private function getAllGroups() {
527        $result = [];
528        $lang = $this->getLanguage();
529        foreach ( $this->userGroupManager->listAllGroups() as $group ) {
530            $result[$group] = $lang->getGroupName( $group );
531        }
532        asort( $result );
533
534        return $result;
535    }
536
537    /**
538     * Preserve group and username offset parameters when paging
539     * @return array
540     */
541    public function getDefaultQuery() {
542        $query = parent::getDefaultQuery();
543        if ( $this->requestedGroup != '' ) {
544            $query['group'] = $this->requestedGroup;
545        }
546        if ( $this->requestedUser != '' ) {
547            $query['username'] = $this->requestedUser;
548        }
549        $this->hookRunner->onSpecialListusersDefaultQuery( $this, $query );
550
551        return $query;
552    }
553
554    /**
555     * Get an associative array containing groups the specified user belongs to,
556     * and the relevant UserGroupMembership objects
557     *
558     * @param UserIdentity $user
559     * @return UserGroupMembership[] (group name => UserGroupMembership object)
560     */
561    protected function getGroupMemberships( $user ) {
562        if ( $this->userGroupCache === null ) {
563            return $this->userGroupManager->getUserGroupMemberships( $user );
564        } else {
565            return $this->userGroupCache[$user->getId()] ?? [];
566        }
567    }
568
569    /**
570     * Format a link to a group description page
571     *
572     * @param string|UserGroupMembership $group Group name or UserGroupMembership object
573     * @param string $username
574     * @return string
575     */
576    protected function buildGroupLink( $group, $username ) {
577        return UserGroupMembership::getLinkHTML( $group, $this->getContext(), $username );
578    }
579}
580
581/**
582 * Retain the old class name for backwards compatibility.
583 * @deprecated since 1.41
584 */
585class_alias( UsersPager::class, 'UsersPager' );