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