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