Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryUsers
0.00% covered (danger)
0.00%
0 / 185
0.00% covered (danger)
0.00%
0 / 6
3660
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 146
0.00% covered (danger)
0.00%
0 / 1
2970
 getCacheMode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2007 Roan Kattouw <roan.kattouw@gmail.com>
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\Auth\AuthManager;
12use MediaWiki\Cache\GenderCache;
13use MediaWiki\User\TempUser\TempUserConfig;
14use MediaWiki\User\TempUser\TempUserDetailsLookup;
15use MediaWiki\User\User;
16use MediaWiki\User\UserFactory;
17use MediaWiki\User\UserGroupManager;
18use MediaWiki\User\UserIdentityValue;
19use MediaWiki\User\UserNameUtils;
20use Wikimedia\ParamValidator\ParamValidator;
21use Wikimedia\Timestamp\TimestampFormat as TS;
22
23/**
24 * Query module to get information about a list of users
25 *
26 * @ingroup API
27 */
28class ApiQueryUsers extends ApiQueryBase {
29    use ApiQueryBlockInfoTrait;
30
31    /** @var array<string,true> */
32    private $prop;
33
34    /**
35     * Properties whose contents does not depend on who is looking at them. If the usprops field
36     * contains anything not listed here, the cache mode will never be public for logged-in users.
37     * @var array
38     */
39    protected static $publicProps = [
40        // everything except 'blockinfo' which might show hidden records if the user
41        // making the request has the appropriate permissions
42        'groups',
43        'groupmemberships',
44        'implicitgroups',
45        'rights',
46        'editcount',
47        'registration',
48        'emailable',
49        'gender',
50        'centralids',
51        'cancreate',
52        'tempexpired',
53    ];
54
55    public function __construct(
56        ApiQuery $query,
57        string $moduleName,
58        private readonly UserNameUtils $userNameUtils,
59        private readonly UserFactory $userFactory,
60        private readonly UserGroupManager $userGroupManager,
61        private readonly GenderCache $genderCache,
62        private readonly AuthManager $authManager,
63        private readonly TempUserConfig $tempUserConfig,
64        private readonly TempUserDetailsLookup $tempUserDetailsLookup
65    ) {
66        parent::__construct( $query, $moduleName, 'us' );
67    }
68
69    public function execute() {
70        $db = $this->getDB();
71        $params = $this->extractRequestParams();
72        $this->requireMaxOneParameter( $params, 'userids', 'users' );
73
74        if ( $params['prop'] !== null ) {
75            $this->prop = array_fill_keys( $params['prop'], true );
76        } else {
77            $this->prop = [];
78        }
79        $useNames = $params['users'] !== null;
80
81        $users = (array)$params['users'];
82        $userids = (array)$params['userids'];
83
84        $goodNames = $done = [];
85        $result = $this->getResult();
86        // Canonicalize user names
87        foreach ( $users as $u ) {
88            $n = $this->userNameUtils->getCanonical( $u );
89            if ( $n === false || $n === '' ) {
90                $vals = [ 'name' => $u, 'invalid' => true ];
91                $fit = $result->addValue( [ 'query', $this->getModuleName() ],
92                    null, $vals );
93                if ( !$fit ) {
94                    $this->setContinueEnumParameter( 'users',
95                        implode( '|', array_diff( $users, $done ) ) );
96                    $goodNames = [];
97                    break;
98                }
99                $done[] = $u;
100            } else {
101                $goodNames[] = $n;
102            }
103        }
104
105        if ( $useNames ) {
106            $parameters = &$goodNames;
107        } else {
108            $parameters = &$userids;
109        }
110
111        $result = $this->getResult();
112
113        if ( count( $parameters ) ) {
114            $this->getQueryBuilder()->merge( User::newQueryBuilder( $db ) );
115            if ( $useNames ) {
116                $this->addWhereFld( 'user_name', $goodNames );
117            } else {
118                $this->addWhereFld( 'user_id', $userids );
119            }
120
121            $this->addDeletedUserFilter();
122
123            $data = [];
124            $res = $this->select( __METHOD__ );
125            $this->resetQueryParams();
126
127            // get user groups if needed
128            if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) {
129                $userGroups = [];
130
131                $this->addTables( 'user' );
132                if ( $useNames ) {
133                    $this->addWhereFld( 'user_name', $goodNames );
134                } else {
135                    $this->addWhereFld( 'user_id', $userids );
136                }
137
138                $this->addTables( 'user_groups' );
139                $this->addJoinConds( [ 'user_groups' => [ 'JOIN', 'ug_user=user_id' ] ] );
140                $this->addFields( [ 'user_name' ] );
141                $this->addFields( [ 'ug_user', 'ug_group', 'ug_expiry' ] );
142                $this->addWhere(
143                    $db->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $db->timestamp() )
144                );
145                $userGroupsRes = $this->select( __METHOD__ );
146
147                foreach ( $userGroupsRes as $row ) {
148                    $userGroups[$row->user_name][] = $row;
149                }
150            }
151            if ( isset( $this->prop['gender'] ) ) {
152                $userNames = [];
153                foreach ( $res as $row ) {
154                    $userNames[] = $row->user_name;
155                }
156                $this->genderCache->doQuery( $userNames, __METHOD__ );
157            }
158
159            if ( isset( $this->prop['blockinfo'] ) ) {
160                $blockInfos = $this->getBlockDetailsForRows( $res );
161            } else {
162                $blockInfos = null;
163            }
164
165            if ( isset( $this->prop['tempexpired'] ) ) {
166                $tempUsers = [];
167                foreach ( $res as $row ) {
168                    if ( $this->tempUserConfig->isTempName( $row->user_name ) ) {
169                        $tempUsers[] = UserIdentityValue::newRegistered( $row->user_id, $row->user_name );
170                    }
171                }
172                $this->tempUserDetailsLookup->preloadExpirationStatus( $tempUsers );
173            }
174
175            foreach ( $res as $row ) {
176                // create user object and pass along $userGroups if set
177                // that reduces the number of database queries needed in User dramatically
178                if ( !isset( $userGroups ) ) {
179                    $user = $this->userFactory->newFromRow( $row );
180                } else {
181                    if ( !isset( $userGroups[$row->user_name] ) || !is_array( $userGroups[$row->user_name] ) ) {
182                        $userGroups[$row->user_name] = [];
183                    }
184                    $user = $this->userFactory->newFromRow( $row, [ 'user_groups' => $userGroups[$row->user_name] ] );
185                }
186                if ( $useNames ) {
187                    $key = $user->getName();
188                } else {
189                    $key = $user->getId();
190                }
191                $data[$key]['userid'] = $user->getId();
192                $data[$key]['name'] = $user->getName();
193
194                if ( $user->isSystemUser() ) {
195                    $data[$key]['systemuser'] = true;
196                }
197
198                if ( isset( $this->prop['editcount'] ) ) {
199                    $data[$key]['editcount'] = $user->getEditCount();
200                }
201
202                if ( isset( $this->prop['registration'] ) ) {
203                    $data[$key]['registration'] = wfTimestampOrNull( TS::ISO_8601, $user->getRegistration() );
204                }
205
206                if ( isset( $this->prop['groups'] ) ) {
207                    $data[$key]['groups'] = $this->userGroupManager->getUserEffectiveGroups( $user );
208                }
209
210                if ( isset( $this->prop['groupmemberships'] ) ) {
211                    $data[$key]['groupmemberships'] = array_map( static function ( $ugm ) {
212                        return [
213                            'group' => $ugm->getGroup(),
214                            'expiry' => ApiResult::formatExpiry( $ugm->getExpiry() ),
215                        ];
216                    }, $this->userGroupManager->getUserGroupMemberships( $user ) );
217                }
218
219                if ( isset( $this->prop['implicitgroups'] ) ) {
220                    $data[$key]['implicitgroups'] = $this->userGroupManager->getUserImplicitGroups( $user );
221                }
222
223                if ( isset( $this->prop['rights'] ) ) {
224                    $data[$key]['rights'] = $this->getPermissionManager()
225                        ->getUserPermissions( $user );
226                }
227                if ( $row->hu_deleted ) {
228                    $data[$key]['hidden'] = true;
229                }
230                if ( isset( $this->prop['blockinfo'] ) && isset( $blockInfos[$row->user_id] ) ) {
231                    $data[$key] += $blockInfos[$row->user_id];
232                }
233
234                if ( isset( $this->prop['emailable'] ) ) {
235                    $data[$key]['emailable'] = $user->canReceiveEmail();
236                }
237
238                if ( isset( $this->prop['gender'] ) ) {
239                    $data[$key]['gender'] = $this->genderCache->getGenderOf( $user, __METHOD__ );
240                }
241
242                if ( isset( $this->prop['centralids'] ) ) {
243                    $data[$key] += ApiQueryUserInfo::getCentralUserInfo(
244                        $this->getConfig(), $user, $params['attachedwiki']
245                    );
246                }
247
248                if ( isset( $this->prop['tempexpired'] ) ) {
249                    if ( $this->tempUserConfig->isTempName( $row->user_name ) ) {
250                        $data[$key]['tempexpired'] = $this->tempUserDetailsLookup->isExpired( $user );
251                    } else {
252                        $data[$key]['tempexpired'] = null;
253                    }
254                }
255            }
256        }
257
258        // Second pass: add result data to $retval
259        foreach ( $parameters as $u ) {
260            if ( !isset( $data[$u] ) ) {
261                if ( $useNames ) {
262                    $data[$u] = [ 'name' => $u, 'missing' => true ];
263                    if ( isset( $this->prop['cancreate'] ) ) {
264                        $status = $this->authManager->canCreateAccount( $u );
265                        $data[$u]['cancreate'] = $status->isGood();
266                        if ( !$status->isGood() ) {
267                            $data[$u]['cancreateerror'] = $this->getErrorFormatter()->arrayFromStatus( $status );
268                        }
269                    }
270                } else {
271                    $data[$u] = [ 'userid' => $u, 'missing' => true ];
272                }
273
274            } else {
275                if ( isset( $this->prop['groups'] ) && isset( $data[$u]['groups'] ) ) {
276                    ApiResult::setArrayType( $data[$u]['groups'], 'array' );
277                    ApiResult::setIndexedTagName( $data[$u]['groups'], 'g' );
278                }
279                if ( isset( $this->prop['groupmemberships'] ) && isset( $data[$u]['groupmemberships'] ) ) {
280                    ApiResult::setArrayType( $data[$u]['groupmemberships'], 'array' );
281                    ApiResult::setIndexedTagName( $data[$u]['groupmemberships'], 'groupmembership' );
282                }
283                if ( isset( $this->prop['implicitgroups'] ) && isset( $data[$u]['implicitgroups'] ) ) {
284                    ApiResult::setArrayType( $data[$u]['implicitgroups'], 'array' );
285                    ApiResult::setIndexedTagName( $data[$u]['implicitgroups'], 'g' );
286                }
287                if ( isset( $this->prop['rights'] ) && isset( $data[$u]['rights'] ) ) {
288                    ApiResult::setArrayType( $data[$u]['rights'], 'array' );
289                    ApiResult::setIndexedTagName( $data[$u]['rights'], 'r' );
290                }
291            }
292
293            $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data[$u] );
294            if ( !$fit ) {
295                if ( $useNames ) {
296                    $this->setContinueEnumParameter( 'users',
297                        implode( '|', array_diff( $users, $done ) ) );
298                } else {
299                    $this->setContinueEnumParameter( 'userids',
300                        implode( '|', array_diff( $userids, $done ) ) );
301                }
302                break;
303            }
304            $done[] = $u;
305        }
306        $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'user' );
307    }
308
309    /** @inheritDoc */
310    public function getCacheMode( $params ) {
311        if ( array_diff( (array)$params['prop'], static::$publicProps ) ) {
312            return 'anon-public-user-private';
313        } else {
314            return 'public';
315        }
316    }
317
318    /** @inheritDoc */
319    public function getAllowedParams() {
320        return [
321            'prop' => [
322                ParamValidator::PARAM_ISMULTI => true,
323                ParamValidator::PARAM_TYPE => [
324                    'blockinfo',
325                    'groups',
326                    'groupmemberships',
327                    'implicitgroups',
328                    'rights',
329                    'editcount',
330                    'registration',
331                    'emailable',
332                    'gender',
333                    'centralids',
334                    'cancreate',
335                    'tempexpired',
336                    // When adding a prop, consider whether it should be added
337                    // to self::$publicProps
338                ],
339                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
340            ],
341            'attachedwiki' => null,
342            'users' => [
343                ParamValidator::PARAM_ISMULTI => true
344            ],
345            'userids' => [
346                ParamValidator::PARAM_ISMULTI => true,
347                ParamValidator::PARAM_TYPE => 'integer'
348            ],
349        ];
350    }
351
352    /** @inheritDoc */
353    protected function getExamplesMessages() {
354        return [
355            'action=query&list=users&ususers=Example&usprop=groups|editcount|gender'
356                => 'apihelp-query+users-example-simple',
357        ];
358    }
359
360    /** @inheritDoc */
361    public function getHelpUrls() {
362        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Users';
363    }
364}
365
366/** @deprecated class alias since 1.43 */
367class_alias( ApiQueryUsers::class, 'ApiQueryUsers' );