Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 164 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
GlobalUsersPager | |
0.00% |
0 / 164 |
|
0.00% |
0 / 11 |
1332 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setGroup | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
setUsername | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getIndexField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultQuery | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getQueryInfo | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
12 | |||
formatRow | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
doBatchLookups | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
132 | |||
getPageHeader | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
6 | |||
getUserGroups | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
getAllGroups | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth\Special; |
4 | |
5 | use HTMLForm; |
6 | use IContextSource; |
7 | use MediaWiki\Cache\LinkBatchFactory; |
8 | use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager; |
9 | use MediaWiki\Extension\CentralAuth\GlobalGroup\GlobalGroupLookup; |
10 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
11 | use MediaWiki\Extension\CentralAuth\WikiSet; |
12 | use MediaWiki\Html\Html; |
13 | use MediaWiki\Pager\AlphabeticPager; |
14 | use MediaWiki\Title\Title; |
15 | use MediaWiki\User\UserGroupMembership; |
16 | use MediaWiki\WikiMap\WikiMap; |
17 | use stdClass; |
18 | |
19 | class 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 | } |