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