Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
74.70% |
189 / 253 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryAllUsers | |
74.70% |
189 / 253 |
|
42.86% |
3 / 7 |
103.97 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getCanonicalUserName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
67.96% |
123 / 181 |
|
0.00% |
0 / 1 |
123.81 | |||
getCacheMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedParams | |
98.31% |
58 / 59 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | use MediaWiki\MainConfigNames; |
24 | use MediaWiki\Permissions\GroupPermissionsLookup; |
25 | use MediaWiki\User\UserFactory; |
26 | use MediaWiki\User\UserGroupManager; |
27 | use Wikimedia\ParamValidator\ParamValidator; |
28 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
29 | use Wikimedia\Rdbms\IExpression; |
30 | use Wikimedia\Rdbms\LikeValue; |
31 | |
32 | /** |
33 | * Query module to enumerate all registered users. |
34 | * |
35 | * @ingroup API |
36 | */ |
37 | class ApiQueryAllUsers extends ApiQueryBase { |
38 | use ApiQueryBlockInfoTrait; |
39 | |
40 | private UserFactory $userFactory; |
41 | private UserGroupManager $userGroupManager; |
42 | private GroupPermissionsLookup $groupPermissionsLookup; |
43 | private Language $contentLanguage; |
44 | |
45 | /** |
46 | * @param ApiQuery $query |
47 | * @param string $moduleName |
48 | * @param UserFactory $userFactory |
49 | * @param UserGroupManager $userGroupManager |
50 | * @param GroupPermissionsLookup $groupPermissionsLookup |
51 | * @param Language $contentLanguage |
52 | */ |
53 | public function __construct( |
54 | ApiQuery $query, |
55 | $moduleName, |
56 | UserFactory $userFactory, |
57 | UserGroupManager $userGroupManager, |
58 | GroupPermissionsLookup $groupPermissionsLookup, |
59 | Language $contentLanguage |
60 | ) { |
61 | parent::__construct( $query, $moduleName, 'au' ); |
62 | $this->userFactory = $userFactory; |
63 | $this->userGroupManager = $userGroupManager; |
64 | $this->groupPermissionsLookup = $groupPermissionsLookup; |
65 | $this->contentLanguage = $contentLanguage; |
66 | } |
67 | |
68 | /** |
69 | * This function converts the user name to a canonical form |
70 | * which is stored in the database. |
71 | * @param string $name |
72 | * @return string |
73 | */ |
74 | private function getCanonicalUserName( $name ) { |
75 | $name = $this->contentLanguage->ucfirst( $name ); |
76 | return strtr( $name, '_', ' ' ); |
77 | } |
78 | |
79 | public function execute() { |
80 | $params = $this->extractRequestParams(); |
81 | $activeUserDays = $this->getConfig()->get( MainConfigNames::ActiveUserDays ); |
82 | |
83 | $db = $this->getDB(); |
84 | |
85 | $prop = $params['prop']; |
86 | if ( $prop !== null ) { |
87 | $prop = array_fill_keys( $prop, true ); |
88 | $fld_blockinfo = isset( $prop['blockinfo'] ); |
89 | $fld_editcount = isset( $prop['editcount'] ); |
90 | $fld_groups = isset( $prop['groups'] ); |
91 | $fld_rights = isset( $prop['rights'] ); |
92 | $fld_registration = isset( $prop['registration'] ); |
93 | $fld_implicitgroups = isset( $prop['implicitgroups'] ); |
94 | $fld_centralids = isset( $prop['centralids'] ); |
95 | } else { |
96 | $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration = |
97 | $fld_rights = $fld_implicitgroups = $fld_centralids = false; |
98 | } |
99 | |
100 | $limit = $params['limit']; |
101 | |
102 | $this->addTables( 'user' ); |
103 | |
104 | $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); |
105 | $from = $params['from'] === null ? null : $this->getCanonicalUserName( $params['from'] ); |
106 | $to = $params['to'] === null ? null : $this->getCanonicalUserName( $params['to'] ); |
107 | |
108 | # MySQL can't figure out that 'user_name' and 'qcc_title' are the same |
109 | # despite the JOIN condition, so manually sort on the correct one. |
110 | $userFieldToSort = $params['activeusers'] ? 'qcc_title' : 'user_name'; |
111 | |
112 | # Some of these subtable joins are going to give us duplicate rows, so |
113 | # calculate the maximum number of duplicates we might see. |
114 | $maxDuplicateRows = 1; |
115 | |
116 | $this->addWhereRange( $userFieldToSort, $dir, $from, $to ); |
117 | |
118 | if ( $params['prefix'] !== null ) { |
119 | $this->addWhere( |
120 | $db->expr( |
121 | $userFieldToSort, |
122 | IExpression::LIKE, |
123 | new LikeValue( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) |
124 | ) |
125 | ); |
126 | } |
127 | |
128 | if ( $params['rights'] !== null && count( $params['rights'] ) ) { |
129 | $groups = []; |
130 | // TODO: this does not properly account for $wgRevokePermissions |
131 | foreach ( $params['rights'] as $r ) { |
132 | if ( in_array( $r, $this->getPermissionManager()->getImplicitRights(), true ) ) { |
133 | $groups[] = '*'; |
134 | } else { |
135 | $groups = array_merge( |
136 | $groups, |
137 | $this->groupPermissionsLookup->getGroupsWithPermission( $r ) |
138 | ); |
139 | } |
140 | } |
141 | |
142 | if ( $groups === [] ) { |
143 | // No group with the given right(s) exists, no need for a query |
144 | $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], '' ); |
145 | |
146 | return; |
147 | } |
148 | |
149 | $groups = array_unique( $groups ); |
150 | if ( in_array( '*', $groups, true ) || in_array( 'user', $groups, true ) ) { |
151 | // All user rows logically match but there are no "*"/"user" user_groups rows |
152 | $groups = []; |
153 | } |
154 | |
155 | if ( $params['group'] === null ) { |
156 | $params['group'] = $groups; |
157 | } else { |
158 | $params['group'] = array_unique( array_merge( $params['group'], $groups ) ); |
159 | } |
160 | } |
161 | |
162 | $this->requireMaxOneParameter( $params, 'group', 'excludegroup' ); |
163 | |
164 | if ( $params['group'] !== null && count( $params['group'] ) ) { |
165 | // Filter only users that belong to a given group. This might |
166 | // produce as many rows-per-user as there are groups being checked. |
167 | $this->addTables( 'user_groups', 'ug1' ); |
168 | $this->addJoinConds( [ |
169 | 'ug1' => [ |
170 | 'JOIN', |
171 | [ |
172 | 'ug1.ug_user=user_id', |
173 | 'ug1.ug_group' => $params['group'], |
174 | 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) |
175 | ] |
176 | ] |
177 | ] ); |
178 | $maxDuplicateRows *= count( $params['group'] ); |
179 | } |
180 | |
181 | if ( $params['excludegroup'] !== null && count( $params['excludegroup'] ) ) { |
182 | // Filter only users don't belong to a given group. This can only |
183 | // produce one row-per-user, because we only keep on "no match". |
184 | $this->addTables( 'user_groups', 'ug1' ); |
185 | |
186 | $this->addJoinConds( [ 'ug1' => [ 'LEFT JOIN', |
187 | [ |
188 | 'ug1.ug_user=user_id', |
189 | 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $db->addQuotes( $db->timestamp() ), |
190 | 'ug1.ug_group' => $params['excludegroup'], |
191 | ] |
192 | ] ] ); |
193 | $this->addWhere( [ 'ug1.ug_user' => null ] ); |
194 | } |
195 | |
196 | if ( $params['witheditsonly'] ) { |
197 | $this->addWhere( 'user_editcount > 0' ); |
198 | } |
199 | |
200 | $this->addDeletedUserFilter(); |
201 | |
202 | if ( $fld_groups || $fld_rights ) { |
203 | $this->addFields( [ 'groups' => |
204 | $db->buildGroupConcatField( '|', 'user_groups', 'ug_group', [ |
205 | 'ug_user=user_id', |
206 | $db->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $db->timestamp() ) |
207 | ] ) |
208 | ] ); |
209 | } |
210 | |
211 | if ( $params['activeusers'] ) { |
212 | $activeUserSeconds = $activeUserDays * 86400; |
213 | |
214 | // Filter query to only include users in the active users cache. |
215 | // There shouldn't be any duplicate rows in querycachetwo here. |
216 | $this->addTables( 'querycachetwo' ); |
217 | $this->addJoinConds( [ 'querycachetwo' => [ |
218 | 'JOIN', [ |
219 | 'qcc_type' => 'activeusers', |
220 | 'qcc_namespace' => NS_USER, |
221 | 'qcc_title=user_name', |
222 | ], |
223 | ] ] ); |
224 | |
225 | // Actually count the actions using a subquery (T66505 and T66507) |
226 | $timestamp = $db->timestamp( (int)wfTimestamp( TS_UNIX ) - $activeUserSeconds ); |
227 | $subqueryBuilder = $db->newSelectQueryBuilder() |
228 | ->select( 'COUNT(*)' ) |
229 | ->from( 'recentchanges' ) |
230 | ->join( 'actor', null, 'rc_actor = actor_id' ) |
231 | ->where( [ |
232 | 'actor_user = user_id', |
233 | $db->expr( 'rc_type', '!=', RC_EXTERNAL ), // no wikidata |
234 | $db->expr( 'rc_log_type', '=', null ) |
235 | ->or( 'rc_log_type', '!=', 'newusers' ), |
236 | $db->expr( 'rc_timestamp', '>=', $timestamp ), |
237 | ] ); |
238 | $this->addFields( [ |
239 | 'recentactions' => '(' . $subqueryBuilder->caller( __METHOD__ )->getSQL() . ')' |
240 | ] ); |
241 | } |
242 | |
243 | $sqlLimit = $limit + $maxDuplicateRows; |
244 | $this->addOption( 'LIMIT', $sqlLimit ); |
245 | |
246 | $this->addFields( [ |
247 | 'user_name', |
248 | 'user_id' |
249 | ] ); |
250 | $this->addFieldsIf( 'user_editcount', $fld_editcount ); |
251 | $this->addFieldsIf( 'user_registration', $fld_registration ); |
252 | |
253 | $res = $this->select( __METHOD__ ); |
254 | $count = 0; |
255 | $countDuplicates = 0; |
256 | $lastUser = false; |
257 | $result = $this->getResult(); |
258 | $blockInfos = $fld_blockinfo ? $this->getBlockDetailsForRows( $res ) : null; |
259 | foreach ( $res as $row ) { |
260 | $count++; |
261 | |
262 | if ( $lastUser === $row->user_name ) { |
263 | // Duplicate row due to one of the needed subtable joins. |
264 | // Ignore it, but count the number of them to sensibly handle |
265 | // miscalculation of $maxDuplicateRows. |
266 | $countDuplicates++; |
267 | if ( $countDuplicates == $maxDuplicateRows ) { |
268 | ApiBase::dieDebug( __METHOD__, 'Saw more duplicate rows than expected' ); |
269 | } |
270 | continue; |
271 | } |
272 | |
273 | $countDuplicates = 0; |
274 | $lastUser = $row->user_name; |
275 | |
276 | if ( $count > $limit ) { |
277 | // We've reached the one extra which shows that there are |
278 | // additional pages to be had. Stop here... |
279 | $this->setContinueEnumParameter( 'from', $row->user_name ); |
280 | break; |
281 | } |
282 | |
283 | if ( $count == $sqlLimit ) { |
284 | // Should never hit this (either the $countDuplicates check or |
285 | // the $count > $limit check should hit first), but check it |
286 | // anyway just in case. |
287 | ApiBase::dieDebug( __METHOD__, 'Saw more duplicate rows than expected' ); |
288 | } |
289 | |
290 | if ( $params['activeusers'] && (int)$row->recentactions === 0 ) { |
291 | // activeusers cache was out of date |
292 | continue; |
293 | } |
294 | |
295 | $data = [ |
296 | 'userid' => (int)$row->user_id, |
297 | 'name' => $row->user_name, |
298 | ]; |
299 | |
300 | if ( $fld_centralids ) { |
301 | $data += ApiQueryUserInfo::getCentralUserInfo( |
302 | $this->getConfig(), $this->userFactory->newFromId( (int)$row->user_id ), $params['attachedwiki'] |
303 | ); |
304 | } |
305 | |
306 | if ( $fld_blockinfo && isset( $blockInfos[$row->user_id] ) ) { |
307 | $data += $blockInfos[$row->user_id]; |
308 | } |
309 | if ( $row->hu_deleted ) { |
310 | $data['hidden'] = true; |
311 | } |
312 | if ( $fld_editcount ) { |
313 | $data['editcount'] = (int)$row->user_editcount; |
314 | } |
315 | if ( $params['activeusers'] ) { |
316 | $data['recentactions'] = (int)$row->recentactions; |
317 | } |
318 | if ( $fld_registration ) { |
319 | $data['registration'] = $row->user_registration ? |
320 | wfTimestamp( TS_ISO_8601, $row->user_registration ) : ''; |
321 | } |
322 | |
323 | if ( $fld_implicitgroups || $fld_groups || $fld_rights ) { |
324 | $implicitGroups = $this->userGroupManager |
325 | ->getUserImplicitGroups( $this->userFactory->newFromId( (int)$row->user_id ) ); |
326 | if ( isset( $row->groups ) && $row->groups !== '' ) { |
327 | $groups = array_merge( $implicitGroups, explode( '|', $row->groups ) ); |
328 | } else { |
329 | $groups = $implicitGroups; |
330 | } |
331 | |
332 | if ( $fld_groups ) { |
333 | $data['groups'] = $groups; |
334 | ApiResult::setIndexedTagName( $data['groups'], 'g' ); |
335 | ApiResult::setArrayType( $data['groups'], 'array' ); |
336 | } |
337 | |
338 | if ( $fld_implicitgroups ) { |
339 | $data['implicitgroups'] = $implicitGroups; |
340 | ApiResult::setIndexedTagName( $data['implicitgroups'], 'g' ); |
341 | ApiResult::setArrayType( $data['implicitgroups'], 'array' ); |
342 | } |
343 | |
344 | if ( $fld_rights ) { |
345 | $user = $this->userFactory->newFromId( (int)$row->user_id ); |
346 | $data['rights'] = $this->getPermissionManager()->getUserPermissions( $user ); |
347 | ApiResult::setIndexedTagName( $data['rights'], 'r' ); |
348 | ApiResult::setArrayType( $data['rights'], 'array' ); |
349 | } |
350 | } |
351 | |
352 | $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data ); |
353 | if ( !$fit ) { |
354 | $this->setContinueEnumParameter( 'from', $data['name'] ); |
355 | break; |
356 | } |
357 | } |
358 | |
359 | $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'u' ); |
360 | } |
361 | |
362 | public function getCacheMode( $params ) { |
363 | return 'anon-public-user-private'; |
364 | } |
365 | |
366 | public function getAllowedParams( $flags = 0 ) { |
367 | $userGroups = $this->userGroupManager->listAllGroups(); |
368 | |
369 | if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) { |
370 | sort( $userGroups ); |
371 | } |
372 | |
373 | return [ |
374 | 'from' => null, |
375 | 'to' => null, |
376 | 'prefix' => null, |
377 | 'dir' => [ |
378 | ParamValidator::PARAM_DEFAULT => 'ascending', |
379 | ParamValidator::PARAM_TYPE => [ |
380 | 'ascending', |
381 | 'descending' |
382 | ], |
383 | ], |
384 | 'group' => [ |
385 | ParamValidator::PARAM_TYPE => $userGroups, |
386 | ParamValidator::PARAM_ISMULTI => true, |
387 | ], |
388 | 'excludegroup' => [ |
389 | ParamValidator::PARAM_TYPE => $userGroups, |
390 | ParamValidator::PARAM_ISMULTI => true, |
391 | ], |
392 | 'rights' => [ |
393 | ParamValidator::PARAM_TYPE => array_unique( array_merge( |
394 | $this->getPermissionManager()->getAllPermissions(), |
395 | $this->getPermissionManager()->getImplicitRights() |
396 | ) ), |
397 | ParamValidator::PARAM_ISMULTI => true, |
398 | ], |
399 | 'prop' => [ |
400 | ParamValidator::PARAM_ISMULTI => true, |
401 | ParamValidator::PARAM_TYPE => [ |
402 | 'blockinfo', |
403 | 'groups', |
404 | 'implicitgroups', |
405 | 'rights', |
406 | 'editcount', |
407 | 'registration', |
408 | 'centralids', |
409 | ], |
410 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [], |
411 | ], |
412 | 'limit' => [ |
413 | ParamValidator::PARAM_DEFAULT => 10, |
414 | ParamValidator::PARAM_TYPE => 'limit', |
415 | IntegerDef::PARAM_MIN => 1, |
416 | IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1, |
417 | IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2 |
418 | ], |
419 | 'witheditsonly' => false, |
420 | 'activeusers' => [ |
421 | ParamValidator::PARAM_DEFAULT => false, |
422 | ApiBase::PARAM_HELP_MSG => [ |
423 | 'apihelp-query+allusers-param-activeusers', |
424 | $this->getConfig()->get( MainConfigNames::ActiveUserDays ) |
425 | ], |
426 | ], |
427 | 'attachedwiki' => null, |
428 | ]; |
429 | } |
430 | |
431 | protected function getExamplesMessages() { |
432 | return [ |
433 | 'action=query&list=allusers&aufrom=Y' |
434 | => 'apihelp-query+allusers-example-y', |
435 | ]; |
436 | } |
437 | |
438 | public function getHelpUrls() { |
439 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allusers'; |
440 | } |
441 | } |