MediaWiki  master
ApiQueryUsers.php
Go to the documentation of this file.
1 <?php
24 
30 class ApiQueryUsers extends ApiQueryBase {
32 
34 
40  protected static $publicProps = [
41  // everything except 'blockinfo' which might show hidden records if the user
42  // making the request has the appropriate permissions
43  'groups',
44  'groupmemberships',
45  'implicitgroups',
46  'rights',
47  'editcount',
48  'registration',
49  'emailable',
50  'gender',
51  'centralids',
52  'cancreate',
53  ];
54 
55  public function __construct( ApiQuery $query, $moduleName ) {
56  parent::__construct( $query, $moduleName, 'us' );
57  }
58 
66  protected function getTokenFunctions() {
67  // Don't call the hooks twice
68  if ( isset( $this->tokenFunctions ) ) {
69  return $this->tokenFunctions;
70  }
71 
72  // If we're in a mode that breaks the same-origin policy, no tokens can
73  // be obtained
74  if ( $this->lacksSameOriginSecurity() ) {
75  return [];
76  }
77 
78  $this->tokenFunctions = [
79  'userrights' => [ self::class, 'getUserrightsToken' ],
80  ];
81  Hooks::run( 'APIQueryUsersTokens', [ &$this->tokenFunctions ] );
82 
83  return $this->tokenFunctions;
84  }
85 
91  public static function getUserrightsToken( $user ) {
92  global $wgUser;
93 
94  // Since the permissions check for userrights is non-trivial,
95  // don't bother with it here
96  return $wgUser->getEditToken( $user->getName() );
97  }
98 
99  public function execute() {
100  $db = $this->getDB();
101  $commentStore = CommentStore::getStore();
102 
103  $params = $this->extractRequestParams();
104  $this->requireMaxOneParameter( $params, 'userids', 'users' );
105 
106  if ( !is_null( $params['prop'] ) ) {
107  $this->prop = array_flip( $params['prop'] );
108  } else {
109  $this->prop = [];
110  }
111  $useNames = !is_null( $params['users'] );
112 
113  $users = (array)$params['users'];
114  $userids = (array)$params['userids'];
115 
116  $goodNames = $done = [];
117  $result = $this->getResult();
118  // Canonicalize user names
119  foreach ( $users as $u ) {
120  $n = User::getCanonicalName( $u );
121  if ( $n === false || $n === '' ) {
122  $vals = [ 'name' => $u, 'invalid' => true ];
123  $fit = $result->addValue( [ 'query', $this->getModuleName() ],
124  null, $vals );
125  if ( !$fit ) {
126  $this->setContinueEnumParameter( 'users',
127  implode( '|', array_diff( $users, $done ) ) );
128  $goodNames = [];
129  break;
130  }
131  $done[] = $u;
132  } else {
133  $goodNames[] = $n;
134  }
135  }
136 
137  if ( $useNames ) {
138  $parameters = &$goodNames;
139  } else {
140  $parameters = &$userids;
141  }
142 
143  $result = $this->getResult();
144 
145  if ( count( $parameters ) ) {
146  $userQuery = User::getQueryInfo();
147  $this->addTables( $userQuery['tables'] );
148  $this->addFields( $userQuery['fields'] );
149  $this->addJoinConds( $userQuery['joins'] );
150  if ( $useNames ) {
151  $this->addWhereFld( 'user_name', $goodNames );
152  } else {
153  $this->addWhereFld( 'user_id', $userids );
154  }
155 
156  $this->addBlockInfoToQuery( isset( $this->prop['blockinfo'] ) );
157 
158  $data = [];
159  $res = $this->select( __METHOD__ );
160  $this->resetQueryParams();
161 
162  // get user groups if needed
163  if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) {
164  $userGroups = [];
165 
166  $this->addTables( 'user' );
167  if ( $useNames ) {
168  $this->addWhereFld( 'user_name', $goodNames );
169  } else {
170  $this->addWhereFld( 'user_id', $userids );
171  }
172 
173  $this->addTables( 'user_groups' );
174  $this->addJoinConds( [ 'user_groups' => [ 'JOIN', 'ug_user=user_id' ] ] );
175  $this->addFields( [ 'user_name' ] );
177  $this->addWhere( 'ug_expiry IS NULL OR ug_expiry >= ' .
178  $db->addQuotes( $db->timestamp() ) );
179  $userGroupsRes = $this->select( __METHOD__ );
180 
181  foreach ( $userGroupsRes as $row ) {
182  $userGroups[$row->user_name][] = $row;
183  }
184  }
185 
186  foreach ( $res as $row ) {
187  // create user object and pass along $userGroups if set
188  // that reduces the number of database queries needed in User dramatically
189  if ( !isset( $userGroups ) ) {
190  $user = User::newFromRow( $row );
191  } else {
192  if ( !isset( $userGroups[$row->user_name] ) || !is_array( $userGroups[$row->user_name] ) ) {
193  $userGroups[$row->user_name] = [];
194  }
195  $user = User::newFromRow( $row, [ 'user_groups' => $userGroups[$row->user_name] ] );
196  }
197  if ( $useNames ) {
198  $key = $user->getName();
199  } else {
200  $key = $user->getId();
201  }
202  $data[$key]['userid'] = $user->getId();
203  $data[$key]['name'] = $user->getName();
204 
205  if ( isset( $this->prop['editcount'] ) ) {
206  $data[$key]['editcount'] = $user->getEditCount();
207  }
208 
209  if ( isset( $this->prop['registration'] ) ) {
210  $data[$key]['registration'] = wfTimestampOrNull( TS_ISO_8601, $user->getRegistration() );
211  }
212 
213  if ( isset( $this->prop['groups'] ) ) {
214  $data[$key]['groups'] = $user->getEffectiveGroups();
215  }
216 
217  if ( isset( $this->prop['groupmemberships'] ) ) {
218  $data[$key]['groupmemberships'] = array_map( function ( $ugm ) {
219  return [
220  'group' => $ugm->getGroup(),
221  'expiry' => ApiResult::formatExpiry( $ugm->getExpiry() ),
222  ];
223  }, $user->getGroupMemberships() );
224  }
225 
226  if ( isset( $this->prop['implicitgroups'] ) ) {
227  $data[$key]['implicitgroups'] = $user->getAutomaticGroups();
228  }
229 
230  if ( isset( $this->prop['rights'] ) ) {
231  $data[$key]['rights'] = $this->getPermissionManager()
232  ->getUserPermissions( $user );
233  }
234  if ( $row->ipb_deleted ) {
235  $data[$key]['hidden'] = true;
236  }
237  if ( isset( $this->prop['blockinfo'] ) && !is_null( $row->ipb_by_text ) ) {
238  $data[$key] += $this->getBlockDetails( DatabaseBlock::newFromRow( $row ) );
239  }
240 
241  if ( isset( $this->prop['emailable'] ) ) {
242  $data[$key]['emailable'] = $user->canReceiveEmail();
243  }
244 
245  if ( isset( $this->prop['gender'] ) ) {
246  $gender = $user->getOption( 'gender' );
247  if ( strval( $gender ) === '' ) {
248  $gender = 'unknown';
249  }
250  $data[$key]['gender'] = $gender;
251  }
252 
253  if ( isset( $this->prop['centralids'] ) ) {
255  $this->getConfig(), $user, $params['attachedwiki']
256  );
257  }
258 
259  if ( !is_null( $params['token'] ) ) {
261  foreach ( $params['token'] as $t ) {
262  $val = call_user_func( $tokenFunctions[$t], $user );
263  if ( $val === false ) {
264  $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
265  } else {
266  $data[$key][$t . 'token'] = $val;
267  }
268  }
269  }
270  }
271  }
272 
273  $context = $this->getContext();
274  // Second pass: add result data to $retval
275  foreach ( $parameters as $u ) {
276  if ( !isset( $data[$u] ) ) {
277  if ( $useNames ) {
278  $data[$u] = [ 'name' => $u ];
279  $urPage = new UserrightsPage;
280  $urPage->setContext( $context );
281 
282  $iwUser = $urPage->fetchUser( $u );
283 
284  if ( $iwUser instanceof UserRightsProxy ) {
285  $data[$u]['interwiki'] = true;
286 
287  if ( !is_null( $params['token'] ) ) {
289 
290  foreach ( $params['token'] as $t ) {
291  $val = call_user_func( $tokenFunctions[$t], $iwUser );
292  if ( $val === false ) {
293  $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
294  } else {
295  $data[$u][$t . 'token'] = $val;
296  }
297  }
298  }
299  } else {
300  $data[$u]['missing'] = true;
301  if ( isset( $this->prop['cancreate'] ) ) {
302  $status = MediaWiki\Auth\AuthManager::singleton()->canCreateAccount( $u );
303  $data[$u]['cancreate'] = $status->isGood();
304  if ( !$status->isGood() ) {
305  $data[$u]['cancreateerror'] = $this->getErrorFormatter()->arrayFromStatus( $status );
306  }
307  }
308  }
309  } else {
310  $data[$u] = [ 'userid' => $u, 'missing' => true ];
311  }
312 
313  } else {
314  if ( isset( $this->prop['groups'] ) && isset( $data[$u]['groups'] ) ) {
315  ApiResult::setArrayType( $data[$u]['groups'], 'array' );
316  ApiResult::setIndexedTagName( $data[$u]['groups'], 'g' );
317  }
318  if ( isset( $this->prop['groupmemberships'] ) && isset( $data[$u]['groupmemberships'] ) ) {
319  ApiResult::setArrayType( $data[$u]['groupmemberships'], 'array' );
320  ApiResult::setIndexedTagName( $data[$u]['groupmemberships'], 'groupmembership' );
321  }
322  if ( isset( $this->prop['implicitgroups'] ) && isset( $data[$u]['implicitgroups'] ) ) {
323  ApiResult::setArrayType( $data[$u]['implicitgroups'], 'array' );
324  ApiResult::setIndexedTagName( $data[$u]['implicitgroups'], 'g' );
325  }
326  if ( isset( $this->prop['rights'] ) && isset( $data[$u]['rights'] ) ) {
327  ApiResult::setArrayType( $data[$u]['rights'], 'array' );
328  ApiResult::setIndexedTagName( $data[$u]['rights'], 'r' );
329  }
330  }
331 
332  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
333  $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data[$u] );
334  if ( !$fit ) {
335  if ( $useNames ) {
336  $this->setContinueEnumParameter( 'users',
337  implode( '|', array_diff( $users, $done ) ) );
338  } else {
339  $this->setContinueEnumParameter( 'userids',
340  implode( '|', array_diff( $userids, $done ) ) );
341  }
342  break;
343  }
344  $done[] = $u;
345  }
346  $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'user' );
347  }
348 
349  public function getCacheMode( $params ) {
350  if ( isset( $params['token'] ) ) {
351  return 'private';
352  } elseif ( array_diff( (array)$params['prop'], static::$publicProps ) ) {
353  return 'anon-public-user-private';
354  } else {
355  return 'public';
356  }
357  }
358 
359  public function getAllowedParams() {
360  return [
361  'prop' => [
362  ApiBase::PARAM_ISMULTI => true,
364  'blockinfo',
365  'groups',
366  'groupmemberships',
367  'implicitgroups',
368  'rights',
369  'editcount',
370  'registration',
371  'emailable',
372  'gender',
373  'centralids',
374  'cancreate',
375  // When adding a prop, consider whether it should be added
376  // to self::$publicProps
377  ],
379  ],
380  'attachedwiki' => null,
381  'users' => [
383  ],
384  'userids' => [
385  ApiBase::PARAM_ISMULTI => true,
386  ApiBase::PARAM_TYPE => 'integer'
387  ],
388  'token' => [
390  ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
392  ],
393  ];
394  }
395 
396  protected function getExamplesMessages() {
397  return [
398  'action=query&list=users&ususers=Example&usprop=groups|editcount|gender'
399  => 'apihelp-query+users-example-simple',
400  ];
401  }
402 
403  public function getHelpUrls() {
404  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Users';
405  }
406 }
select( $method, $extraQuery=[], array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below...
Definition: ApiBase.php:94
getErrorFormatter()
Get the error formatter.
Definition: ApiBase.php:654
getDB()
Get the Query database connection (read-only)
getResult()
Get the result object.
Definition: ApiBase.php:640
static selectFields()
Returns the list of user_groups fields that should be selected to create a new user group membership...
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
static getUserrightsToken( $user)
Cut-down copy of User interface for local-interwiki-database user rights manipulation.
This is a base class for all Query modules.
lacksSameOriginSecurity()
Returns true if the current request breaks the same-origin policy.
Definition: ApiBase.php:568
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user...
Definition: ApiBase.php:761
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition: ApiBase.php:164
__construct(ApiQuery $query, $moduleName)
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new user object...
Definition: User.php:5324
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
trait ApiQueryBlockInfoTrait
IContextSource $context
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:616
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
Definition: ApiResult.php:1205
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid...
Definition: User.php:1194
setContext( $context)
Sets the context this SpecialPage is executed in.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
static singleton()
Get the global AuthManager.
getContext()
Get the base IContextSource object.
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:520
addFields( $value)
Add a set of fields to select to the internal array.
This is the main query class.
Definition: ApiQuery.php:37
static getCentralUserInfo(Config $config, User $user, $attachedWiki=null)
Get central user info.
static getStore()
static newFromRow( $row, $data=null)
Create a new user object from a user row.
Definition: User.php:717
static array $publicProps
Properties whose contents does not depend on who is looking at them.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
Query module to get information about a list of users.
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition: ApiBase.php:1925
getCacheMode( $params)
const PARAM_ISMULTI
(boolean) Accept multiple pipe-separated values for this parameter (e.g.
Definition: ApiBase.php:58
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks...
Definition: ApiBase.php:710
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
Special page to allow managing user group membership.
addBlockInfoToQuery( $showBlockInfo)
Filters hidden users (where the user doesn&#39;t have the right to view them) Also adds relevant block in...
const PARAM_DEPRECATED
(boolean) Is the parameter deprecated (will show a warning)?
Definition: ApiBase.php:112
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
Definition: ApiResult.php:728
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:928
getTokenFunctions()
Get an array mapping token names to their handler functions.
return true
Definition: router.php:92
resetQueryParams()
Blank the internal arrays with query parameters.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.