MediaWiki master
ApiQueryAllUsers.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Api;
24
35
43
44 private UserFactory $userFactory;
45 private UserGroupManager $userGroupManager;
46 private GroupPermissionsLookup $groupPermissionsLookup;
47 private Language $contentLanguage;
48 private TempUserConfig $tempUserConfig;
49
50 public function __construct(
51 ApiQuery $query,
52 string $moduleName,
53 UserFactory $userFactory,
54 UserGroupManager $userGroupManager,
55 GroupPermissionsLookup $groupPermissionsLookup,
56 Language $contentLanguage,
57 TempUserConfig $tempUserConfig
58 ) {
59 parent::__construct( $query, $moduleName, 'au' );
60 $this->userFactory = $userFactory;
61 $this->userGroupManager = $userGroupManager;
62 $this->groupPermissionsLookup = $groupPermissionsLookup;
63 $this->contentLanguage = $contentLanguage;
64 $this->tempUserConfig = $tempUserConfig;
65 }
66
73 private function getCanonicalUserName( $name ) {
74 $name = $this->contentLanguage->ucfirst( $name );
75 return strtr( $name, '_', ' ' );
76 }
77
78 public function execute() {
80 $activeUserDays = $this->getConfig()->get( MainConfigNames::ActiveUserDays );
81
82 $db = $this->getDB();
83
84 $prop = $params['prop'];
85 if ( $prop !== null ) {
86 $prop = array_fill_keys( $prop, true );
87 $fld_blockinfo = isset( $prop['blockinfo'] );
88 $fld_editcount = isset( $prop['editcount'] );
89 $fld_groups = isset( $prop['groups'] );
90 $fld_rights = isset( $prop['rights'] );
91 $fld_registration = isset( $prop['registration'] );
92 $fld_implicitgroups = isset( $prop['implicitgroups'] );
93 $fld_centralids = isset( $prop['centralids'] );
94 } else {
95 $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration =
96 $fld_rights = $fld_implicitgroups = $fld_centralids = false;
97 }
98
99 $limit = $params['limit'];
100
101 $this->addTables( 'user' );
102
103 $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
104 $from = $params['from'] === null ? null : $this->getCanonicalUserName( $params['from'] );
105 $to = $params['to'] === null ? null : $this->getCanonicalUserName( $params['to'] );
106
107 # MySQL can't figure out that 'user_name' and 'qcc_title' are the same
108 # despite the JOIN condition, so manually sort on the correct one.
109 $userFieldToSort = $params['activeusers'] ? 'qcc_title' : 'user_name';
110
111 # Some of these subtable joins are going to give us duplicate rows, so
112 # calculate the maximum number of duplicates we might see.
113 $maxDuplicateRows = 1;
114
115 $this->addWhereRange( $userFieldToSort, $dir, $from, $to );
116
117 if ( $params['prefix'] !== null ) {
118 $this->addWhere(
119 $db->expr(
120 $userFieldToSort,
121 IExpression::LIKE,
122 new LikeValue( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() )
123 )
124 );
125 }
126
127 $excludeNamed = $params['excludenamed'];
128 $excludeTemp = $params['excludetemp'];
129
130 if ( $this->tempUserConfig->isKnown() ) {
131 if ( $excludeTemp ) {
132 $this->addWhere(
133 $this->tempUserConfig->getMatchCondition( $db, 'user_name', IExpression::NOT_LIKE )
134 );
135 }
136 if ( $excludeNamed ) {
137 $this->addWhere(
138 $this->tempUserConfig->getMatchCondition( $db, 'user_name', IExpression::LIKE )
139 );
140 }
141 }
142
143 if ( $params['rights'] !== null && count( $params['rights'] ) ) {
144 $groups = [];
145 // TODO: this does not properly account for $wgRevokePermissions
146 foreach ( $params['rights'] as $r ) {
147 if ( in_array( $r, $this->getPermissionManager()->getImplicitRights(), true ) ) {
148 $groups[] = '*';
149 } else {
150 $groups = array_merge(
151 $groups,
152 $this->groupPermissionsLookup->getGroupsWithPermission( $r )
153 );
154 }
155 }
156
157 if ( $groups === [] ) {
158 // No group with the given right(s) exists, no need for a query
159 $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], '' );
160
161 return;
162 }
163
164 $groups = array_unique( $groups );
165 if ( in_array( '*', $groups, true ) || in_array( 'user', $groups, true ) ) {
166 // All user rows logically match but there are no "*"/"user" user_groups rows
167 $groups = [];
168 }
169
170 if ( $params['group'] === null ) {
171 $params['group'] = $groups;
172 } else {
173 $params['group'] = array_unique( array_merge( $params['group'], $groups ) );
174 }
175 }
176
177 $this->requireMaxOneParameter( $params, 'group', 'excludegroup' );
178
179 if ( $params['group'] !== null && count( $params['group'] ) ) {
180 // Filter only users that belong to a given group. This might
181 // produce as many rows-per-user as there are groups being checked.
182 $this->addTables( 'user_groups', 'ug1' );
183 $this->addJoinConds( [
184 'ug1' => [
185 'JOIN',
186 [
187 'ug1.ug_user=user_id',
188 'ug1.ug_group' => $params['group'],
189 $db->expr( 'ug1.ug_expiry', '=', null )->or( 'ug1.ug_expiry', '>=', $db->timestamp() ),
190 ]
191 ]
192 ] );
193 $maxDuplicateRows *= count( $params['group'] );
194 }
195
196 if ( $params['excludegroup'] !== null && count( $params['excludegroup'] ) ) {
197 // Filter only users don't belong to a given group. This can only
198 // produce one row-per-user, because we only keep on "no match".
199 $this->addTables( 'user_groups', 'ug1' );
200
201 $this->addJoinConds( [ 'ug1' => [ 'LEFT JOIN',
202 [
203 'ug1.ug_user=user_id',
204 $db->expr( 'ug1.ug_expiry', '=', null )->or( 'ug1.ug_expiry', '>=', $db->timestamp() ),
205 'ug1.ug_group' => $params['excludegroup'],
206 ]
207 ] ] );
208 $this->addWhere( [ 'ug1.ug_user' => null ] );
209 }
210
211 if ( $params['witheditsonly'] ) {
212 $this->addWhere( $db->expr( 'user_editcount', '>', 0 ) );
213 }
214
215 $this->addDeletedUserFilter();
216
217 if ( $fld_groups || $fld_rights ) {
218 $this->addFields( [ 'groups' =>
219 $db->newSelectQueryBuilder()
220 ->table( 'user_groups' )
221 ->field( 'ug_group' )
222 ->where( [
223 'ug_user=user_id',
224 $db->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $db->timestamp() )
225 ] )
226 ->buildGroupConcatField( '|' )
227 ] );
228 }
229
230 if ( $params['activeusers'] ) {
231 $activeUserSeconds = $activeUserDays * 86400;
232
233 // Filter query to only include users in the active users cache.
234 // There shouldn't be any duplicate rows in querycachetwo here.
235 $this->addTables( 'querycachetwo' );
236 $this->addJoinConds( [ 'querycachetwo' => [
237 'JOIN', [
238 'qcc_type' => 'activeusers',
239 'qcc_namespace' => NS_USER,
240 'qcc_title=user_name',
241 ],
242 ] ] );
243
244 // Actually count the actions using a subquery (T66505 and T66507)
245 $timestamp = $db->timestamp( (int)wfTimestamp( TS_UNIX ) - $activeUserSeconds );
246 $subqueryBuilder = $db->newSelectQueryBuilder()
247 ->select( 'COUNT(*)' )
248 ->from( 'recentchanges' )
249 ->join( 'actor', null, 'rc_actor = actor_id' )
250 ->where( [
251 'actor_user = user_id',
252 $db->expr( 'rc_type', '!=', RC_EXTERNAL ), // no wikidata
253 $db->expr( 'rc_log_type', '=', null )
254 ->or( 'rc_log_type', '!=', 'newusers' ),
255 $db->expr( 'rc_timestamp', '>=', $timestamp ),
256 ] );
257 $this->addFields( [
258 'recentactions' => '(' . $subqueryBuilder->caller( __METHOD__ )->getSQL() . ')'
259 ] );
260 }
261
262 $sqlLimit = $limit + $maxDuplicateRows;
263 $this->addOption( 'LIMIT', $sqlLimit );
264
265 $this->addFields( [
266 'user_name',
267 'user_id'
268 ] );
269 $this->addFieldsIf( 'user_editcount', $fld_editcount );
270 $this->addFieldsIf( 'user_registration', $fld_registration );
271
272 $res = $this->select( __METHOD__ );
273 $count = 0;
274 $countDuplicates = 0;
275 $lastUser = false;
276 $result = $this->getResult();
277 $blockInfos = $fld_blockinfo ? $this->getBlockDetailsForRows( $res ) : null;
278 foreach ( $res as $row ) {
279 $count++;
280
281 if ( $lastUser === $row->user_name ) {
282 // Duplicate row due to one of the needed subtable joins.
283 // Ignore it, but count the number of them to sensibly handle
284 // miscalculation of $maxDuplicateRows.
285 $countDuplicates++;
286 if ( $countDuplicates == $maxDuplicateRows ) {
287 ApiBase::dieDebug( __METHOD__, 'Saw more duplicate rows than expected' );
288 }
289 continue;
290 }
291
292 $countDuplicates = 0;
293 $lastUser = $row->user_name;
294
295 if ( $count > $limit ) {
296 // We've reached the one extra which shows that there are
297 // additional pages to be had. Stop here...
298 $this->setContinueEnumParameter( 'from', $row->user_name );
299 break;
300 }
301
302 if ( $count == $sqlLimit ) {
303 // Should never hit this (either the $countDuplicates check or
304 // the $count > $limit check should hit first), but check it
305 // anyway just in case.
306 ApiBase::dieDebug( __METHOD__, 'Saw more duplicate rows than expected' );
307 }
308
309 if ( $params['activeusers'] && (int)$row->recentactions === 0 ) {
310 // activeusers cache was out of date
311 continue;
312 }
313
314 $data = [
315 'userid' => (int)$row->user_id,
316 'name' => $row->user_name,
317 ];
318
319 if ( $fld_centralids ) {
321 $this->getConfig(), $this->userFactory->newFromId( (int)$row->user_id ), $params['attachedwiki']
322 );
323 }
324
325 if ( $fld_blockinfo && isset( $blockInfos[$row->user_id] ) ) {
326 $data += $blockInfos[$row->user_id];
327 }
328 if ( $row->hu_deleted ) {
329 $data['hidden'] = true;
330 }
331 if ( $fld_editcount ) {
332 $data['editcount'] = (int)$row->user_editcount;
333 }
334 if ( $params['activeusers'] ) {
335 $data['recentactions'] = (int)$row->recentactions;
336 }
337 if ( $fld_registration ) {
338 $data['registration'] = $row->user_registration ?
339 wfTimestamp( TS_ISO_8601, $row->user_registration ) : '';
340 }
341
342 if ( $fld_implicitgroups || $fld_groups || $fld_rights ) {
343 $implicitGroups = $this->userGroupManager
344 ->getUserImplicitGroups( $this->userFactory->newFromId( (int)$row->user_id ) );
345 if ( isset( $row->groups ) && $row->groups !== '' ) {
346 $groups = array_merge( $implicitGroups, explode( '|', $row->groups ) );
347 } else {
348 $groups = $implicitGroups;
349 }
350
351 if ( $fld_groups ) {
352 $data['groups'] = $groups;
353 ApiResult::setIndexedTagName( $data['groups'], 'g' );
354 ApiResult::setArrayType( $data['groups'], 'array' );
355 }
356
357 if ( $fld_implicitgroups ) {
358 $data['implicitgroups'] = $implicitGroups;
359 ApiResult::setIndexedTagName( $data['implicitgroups'], 'g' );
360 ApiResult::setArrayType( $data['implicitgroups'], 'array' );
361 }
362
363 if ( $fld_rights ) {
364 $user = $this->userFactory->newFromId( (int)$row->user_id );
365 $data['rights'] = $this->getPermissionManager()->getUserPermissions( $user );
366 ApiResult::setIndexedTagName( $data['rights'], 'r' );
367 ApiResult::setArrayType( $data['rights'], 'array' );
368 }
369 }
370
371 $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data );
372 if ( !$fit ) {
373 $this->setContinueEnumParameter( 'from', $data['name'] );
374 break;
375 }
376 }
377
378 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'u' );
379 }
380
381 public function getCacheMode( $params ) {
382 return 'anon-public-user-private';
383 }
384
385 public function getAllowedParams( $flags = 0 ) {
386 $userGroups = $this->userGroupManager->listAllGroups();
387
388 if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
389 sort( $userGroups );
390 }
391
392 return [
393 'from' => null,
394 'to' => null,
395 'prefix' => null,
396 'dir' => [
397 ParamValidator::PARAM_DEFAULT => 'ascending',
398 ParamValidator::PARAM_TYPE => [
399 'ascending',
400 'descending'
401 ],
402 ],
403 'group' => [
404 ParamValidator::PARAM_TYPE => $userGroups,
405 ParamValidator::PARAM_ISMULTI => true,
406 ],
407 'excludegroup' => [
408 ParamValidator::PARAM_TYPE => $userGroups,
409 ParamValidator::PARAM_ISMULTI => true,
410 ],
411 'rights' => [
412 ParamValidator::PARAM_TYPE => array_unique( array_merge(
413 $this->getPermissionManager()->getAllPermissions(),
414 $this->getPermissionManager()->getImplicitRights()
415 ) ),
416 ParamValidator::PARAM_ISMULTI => true,
417 ],
418 'prop' => [
419 ParamValidator::PARAM_ISMULTI => true,
420 ParamValidator::PARAM_TYPE => [
421 'blockinfo',
422 'groups',
423 'implicitgroups',
424 'rights',
425 'editcount',
426 'registration',
427 'centralids',
428 ],
430 ],
431 'limit' => [
432 ParamValidator::PARAM_DEFAULT => 10,
433 ParamValidator::PARAM_TYPE => 'limit',
434 IntegerDef::PARAM_MIN => 1,
435 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
436 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
437 ],
438 'witheditsonly' => false,
439 'activeusers' => [
440 ParamValidator::PARAM_DEFAULT => false,
442 'apihelp-query+allusers-param-activeusers',
444 ],
445 ],
446 'attachedwiki' => null,
447 'excludenamed' => [
448 ParamValidator::PARAM_TYPE => 'boolean',
449 ],
450 'excludetemp' => [
451 ParamValidator::PARAM_TYPE => 'boolean',
452 ],
453 ];
454 }
455
456 protected function getExamplesMessages() {
457 return [
458 'action=query&list=allusers&aufrom=Y'
459 => 'apihelp-query+allusers-example-y',
460 ];
461 }
462
463 public function getHelpUrls() {
464 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allusers';
465 }
466}
467
469class_alias( ApiQueryAllUsers::class, 'ApiQueryAllUsers' );
const NS_USER
Definition Defines.php:67
const RC_EXTERNAL
Definition Defines.php:120
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
array $params
The job parameters.
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:571
getResult()
Get the result object.
Definition ApiBase.php:710
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:224
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
Definition ApiBase.php:1025
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1820
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:184
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:251
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:851
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition ApiBase.php:770
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When this is set, the result could take longer to generate,...
Definition ApiBase.php:262
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:249
Query module to enumerate all registered users.
getExamplesMessages()
Returns usage examples for this module.
getHelpUrls()
Return links to more detailed help pages about the module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
getCacheMode( $params)
Get the cache mode for the data generated by this module.
__construct(ApiQuery $query, string $moduleName, UserFactory $userFactory, UserGroupManager $userGroupManager, GroupPermissionsLookup $groupPermissionsLookup, Language $contentLanguage, TempUserConfig $tempUserConfig)
This is a base class for all Query modules.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addFieldsIf( $value, $condition)
Same as addFields(), but add the fields only if a condition is met.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
getDB()
Get the Query database connection (read-only)
select( $method, $extraQuery=[], ?array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
addFields( $value)
Add a set of fields to select to the internal array.
addWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, and an ORDER BY clause to sort in the right direction.
static getCentralUserInfo(Config $config, UserIdentity $user, $attachedWiki=UserIdentity::LOCAL)
Get central user info.
This is the main query class.
Definition ApiQuery.php:48
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
Base class for language-specific code.
Definition Language.php:80
A class containing constants representing the names of configuration variables.
const ActiveUserDays
Name constant for the ActiveUserDays setting, for use with Config::get()
Creates User objects.
Service for formatting and validating API parameters.
Type definition for integer types.
Content of like value.
Definition LikeValue.php:14
Interface for temporary user creation config and name matching.