Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 158
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
GlobalUsersPager
0.00% covered (danger)
0.00%
0 / 158
0.00% covered (danger)
0.00%
0 / 11
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 setGroup
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setUsername
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultQuery
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 formatRow
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 doBatchLookups
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
56
 getPageHeader
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
6
 getUserGroups
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 getAllGroups
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\CentralAuth\Special;
4
5use MediaWiki\Cache\LinkBatchFactory;
6use MediaWiki\Context\IContextSource;
7use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager;
8use MediaWiki\Extension\CentralAuth\GlobalGroup\GlobalGroupLookup;
9use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
10use MediaWiki\Extension\CentralAuth\WikiSet;
11use MediaWiki\Html\Html;
12use MediaWiki\HTMLForm\HTMLForm;
13use MediaWiki\Pager\AlphabeticPager;
14use MediaWiki\Title\Title;
15use MediaWiki\User\UserGroupMembership;
16use MediaWiki\WikiMap\WikiMap;
17use stdClass;
18
19class GlobalUsersPager extends AlphabeticPager {
20
21    /** @var string|false */
22    protected $requestedGroup = false;
23    /** @var string|false */
24    protected $requestedUser = false;
25    /** @var array[] */
26    protected $globalIDGroups = [];
27    /** @var string[] */
28    private $localWikisets = [];
29
30    private GlobalGroupLookup $globalGroupLookup;
31    private LinkBatchFactory $linkBatchFactory;
32
33    public function __construct(
34        IContextSource $context,
35        CentralAuthDatabaseManager $dbManager,
36        GlobalGroupLookup $globalGroupLookup,
37        LinkBatchFactory $linkBatchFactory
38    ) {
39        $this->mDb = $dbManager->getCentralReplicaDB();
40        parent::__construct( $context );
41        $this->mDefaultDirection = $this->getRequest()->getBool( 'desc' );
42        $this->globalGroupLookup = $globalGroupLookup;
43        $this->linkBatchFactory = $linkBatchFactory;
44    }
45
46    /**
47     * @param string $group
48     */
49    public function setGroup( string $group = '' ) {
50        if ( $group === '' ) {
51            $this->requestedGroup = false;
52            return;
53        }
54        $this->requestedGroup = $group;
55    }
56
57    /**
58     * @param string $username
59     */
60    public function setUsername( string $username = '' ) {
61        if ( $username === '' ) {
62            $this->requestedUser = false;
63            return;
64        }
65        $this->requestedUser = $username;
66    }
67
68    /**
69     * @return string
70     */
71    public function getIndexField() {
72        return 'gu_name';
73    }
74
75    /**
76     * @return array
77     */
78    public function getDefaultQuery() {
79        $query = parent::getDefaultQuery();
80        if ( !isset( $query['group'] ) && $this->requestedGroup !== false ) {
81            $query['group'] = $this->requestedGroup;
82        }
83        $this->mDefaultQuery = $query;
84        return $this->mDefaultQuery;
85    }
86
87    /**
88     * @return array
89     */
90    public function getQueryInfo() {
91        $tables = [ 'globaluser', 'localuser' ];
92
93        $conds = [ 'gu_hidden_level' => CentralAuthUser::HIDDEN_LEVEL_NONE ];
94
95        $join_conds = [
96            'localuser' => [ 'LEFT JOIN', [ 'gu_name = lu_name', 'lu_wiki' => WikiMap::getCurrentWikiId() ] ],
97        ];
98
99        if ( $this->requestedGroup !== false ) {
100            $tables[] = 'global_user_groups';
101            $conds['gug_group'] = $this->requestedGroup;
102            $join_conds['global_user_groups'] = [
103                'LEFT JOIN',
104                'gu_id = gug_user'
105            ];
106
107            $conds[] = $this->mDb->expr( 'gug_expiry', '=', null )->or( 'gug_expiry', '>=', $this->mDb->timestamp() );
108        }
109
110        if ( $this->requestedUser !== false ) {
111            $conds[] = $this->mDb->expr( 'gu_name', '>=', $this->requestedUser );
112        }
113
114        return [
115            'tables' => $tables,
116            'fields' => [
117                'gu_name',
118                'gu_id' => 'MAX(gu_id)',
119                'gu_locked' => 'MAX(gu_locked)',
120                'lu_attached_method' => 'MAX(lu_attached_method)',
121            ],
122            'conds' => $conds,
123            'options' => [ 'GROUP BY' => 'gu_name' ],
124            'join_conds' => $join_conds,
125        ];
126    }
127
128    /**
129     * Formats a row
130     * @param stdClass $row The row to be formatted for output
131     * @return string HTML li element with username and info about this user
132     */
133    public function formatRow( $row ) {
134        $user = htmlspecialchars( $row->gu_name );
135        $info = [];
136        if ( $row->gu_locked ) {
137            $info[] = $this->msg( 'centralauth-listusers-locked' )->text();
138        }
139        if ( $row->lu_attached_method ) {
140            $info[] = $this->msg( 'centralauth-listusers-attached', $row->gu_name )->text();
141        } else {
142            array_unshift( $info, $this->msg( 'centralauth-listusers-nolocal' )->text() );
143        }
144
145        $groups = $this->getUserGroups( $row->gu_id, $row->gu_name );
146        if ( $groups ) {
147            $info[] = $groups;
148        }
149
150        $info = $this->getLanguage()->commaList( $info );
151        return Html::rawElement( 'li', [],
152            $this->msg( 'centralauth-listusers-item', $user, $info )->parse() );
153    }
154
155    protected function doBatchLookups() {
156        $batch = $this->linkBatchFactory->newLinkBatch();
157
158        foreach ( $this->mResult as $row ) {
159            // userpage existence link cache
160            $batch->addObj( Title::makeTitleSafe( NS_USER, $row->gu_name ) );
161            $this->globalIDGroups[$row->gu_id] = [];
162        }
163
164        $batch->execute();
165
166        $groups = $this->mDb->newSelectQueryBuilder()
167            ->select( [ 'gug_user', 'gug_group', 'gug_expiry' ] )
168            ->from( 'global_user_groups' )
169            ->where( [
170                'gug_user' => array_keys( $this->globalIDGroups ),
171                $this->mDb->expr( 'gug_expiry', '=', null )->or( 'gug_expiry', '>=', $this->mDb->timestamp() )
172            ] )
173            ->caller( __METHOD__ )
174            ->fetchResultSet();
175
176        // Make an array of global groups for all users in the current result set
177        $allGroups = [];
178
179        foreach ( $groups as $row ) {
180            $this->globalIDGroups[$row->gug_user][$row->gug_group] = $row->gug_expiry;
181            $allGroups[] = $row->gug_group;
182        }
183
184        foreach ( $this->globalIDGroups as $user => &$groups ) {
185            // Ensure temporary groups are displayed first, to avoid ambiguity like
186            // "first, second (expires at some point)" (unclear if only second expires or if both expire)
187            uasort( $groups, static fn ( $first, $second ) => (bool)$second <=> (bool)$first );
188        }
189
190        if ( count( $allGroups ) > 0 ) {
191            $wsQuery = $this->mDb->newSelectQueryBuilder()
192                ->select( [ 'ggr_group', 'ws_id', 'ws_name', 'ws_type', 'ws_wikis' ] )
193                ->from( 'global_group_restrictions' )
194                ->join( 'wikiset', null, 'ggr_set=ws_id' )
195                ->where( [ 'ggr_group' => array_unique( $allGroups ) ] )
196                ->caller( __METHOD__ )
197                ->fetchResultSet();
198
199            $notLocalWikiSets = [];
200
201            // Make an array of locally enabled wikisets
202            foreach ( $wsQuery as $wsRow ) {
203                if ( !WikiSet::newFromRow( $wsRow )->inSet() ) {
204                    $notLocalWikiSets[] = $wsRow->ggr_group;
205                }
206            }
207
208            // This is reversed so that wiki sets active everywhere (without
209            // global_group_restrictions rows) are shown as enabled everywhere
210            $this->localWikisets = array_diff(
211                array_unique( $allGroups ),
212                $notLocalWikiSets
213            );
214        }
215
216        $this->mResult->rewind();
217    }
218
219    /**
220     * @return bool
221     */
222    public function getPageHeader() {
223        $options = [];
224        $options[$this->msg( 'group-all' )->text()] = '';
225        foreach ( $this->getAllGroups() as $group => $groupText ) {
226            $options[$groupText] = $group;
227        }
228
229        $formDescriptor = [
230            'usernameText' => [
231                'type' => 'text',
232                'name' => 'username',
233                'id' => 'offset',
234                'label' => $this->msg( 'listusersfrom' )->text(),
235                'size' => 20,
236                'default' => $this->requestedUser,
237                'autofocus' => true,
238            ],
239            'groupSelect' => [
240                'type' => 'select',
241                'name' => 'group',
242                'id' => 'group',
243                'label-message' => 'group',
244                'options' => $options,
245                'default' => $this->requestedGroup,
246            ],
247            'descCheck' => [
248                'type' => 'check',
249                'name' => 'desc',
250                'id' => 'desc',
251                'label-message' => 'listusers-desc',
252                'default' => $this->mDefaultDirection,
253            ]
254        ];
255
256        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
257        $htmlForm
258            ->addHiddenField( 'limit', $this->mLimit )
259            ->setMethod( 'get' )
260            ->setId( 'mw-listusers-form' )
261            ->setSubmitTextMsg( 'allpagessubmit' )
262            ->setWrapperLegendMsg( 'listusers' )
263            ->prepareForm()
264            ->displayForm( false );
265
266        return true;
267    }
268
269    /**
270     * Note: Works only for users with $this->globalIDGroups set
271     *
272     * @param string $id
273     * @param string $username
274     * @return string|null
275     */
276    protected function getUserGroups( $id, $username ): ?string {
277        $rights = [];
278        foreach ( $this->globalIDGroups[$id] as $group => $expiry ) {
279            $ugm = new UserGroupMembership(
280                (int)$id,
281                $group,
282                $expiry !== 'null' ? $expiry : null
283            );
284
285            $wikitextLink = UserGroupMembership::getLinkWiki( $ugm, $this->getContext(), $username );
286
287            if ( !in_array( $group, $this->localWikisets ) ) {
288                // Mark if the group is not applied on this wiki
289                $rights[] = Html::rawElement( 'span',
290                    [ 'class' => 'groupnotappliedhere' ],
291                    $wikitextLink
292                );
293            } else {
294                $rights[] = $wikitextLink;
295            }
296        }
297
298        if ( count( $rights ) > 0 ) {
299            return $this->getLanguage()->listToText( $rights );
300        }
301
302        return null;
303    }
304
305    /**
306     * @return string[]
307     */
308    public function getAllGroups() {
309        $result = [];
310        foreach ( $this->globalGroupLookup->getDefinedGroups() as $group ) {
311            $result[$group] = $this->getLanguage()->getGroupName( $group );
312        }
313        asort( $result );
314        return $result;
315    }
316}