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