Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 180
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 / 179
0.00% covered (danger)
0.00%
0 / 6
3080
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 1
2450
 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 / 29
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 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23namespace MediaWiki\Api;
24
25use MediaWiki\Auth\AuthManager;
26use MediaWiki\Cache\GenderCache;
27use MediaWiki\User\User;
28use MediaWiki\User\UserFactory;
29use MediaWiki\User\UserGroupManager;
30use MediaWiki\User\UserNameUtils;
31use Wikimedia\ParamValidator\ParamValidator;
32
33/**
34 * Query module to get information about a list of users
35 *
36 * @ingroup API
37 */
38class ApiQueryUsers extends ApiQueryBase {
39    use ApiQueryBlockInfoTrait;
40
41    /** @var array<string,true> */
42    private $prop;
43
44    private UserNameUtils $userNameUtils;
45    private UserFactory $userFactory;
46    private UserGroupManager $userGroupManager;
47    private GenderCache $genderCache;
48    private AuthManager $authManager;
49
50    /**
51     * Properties whose contents does not depend on who is looking at them. If the usprops field
52     * contains anything not listed here, the cache mode will never be public for logged-in users.
53     * @var array
54     */
55    protected static $publicProps = [
56        // everything except 'blockinfo' which might show hidden records if the user
57        // making the request has the appropriate permissions
58        'groups',
59        'groupmemberships',
60        'implicitgroups',
61        'rights',
62        'editcount',
63        'registration',
64        'emailable',
65        'gender',
66        'centralids',
67        'cancreate',
68    ];
69
70    public function __construct(
71        ApiQuery $query,
72        string $moduleName,
73        UserNameUtils $userNameUtils,
74        UserFactory $userFactory,
75        UserGroupManager $userGroupManager,
76        GenderCache $genderCache,
77        AuthManager $authManager
78    ) {
79        parent::__construct( $query, $moduleName, 'us' );
80        $this->userNameUtils = $userNameUtils;
81        $this->userFactory = $userFactory;
82        $this->userGroupManager = $userGroupManager;
83        $this->genderCache = $genderCache;
84        $this->authManager = $authManager;
85    }
86
87    public function execute() {
88        $db = $this->getDB();
89        $params = $this->extractRequestParams();
90        $this->requireMaxOneParameter( $params, 'userids', 'users' );
91
92        if ( $params['prop'] !== null ) {
93            $this->prop = array_fill_keys( $params['prop'], true );
94        } else {
95            $this->prop = [];
96        }
97        $useNames = $params['users'] !== null;
98
99        $users = (array)$params['users'];
100        $userids = (array)$params['userids'];
101
102        $goodNames = $done = [];
103        $result = $this->getResult();
104        // Canonicalize user names
105        foreach ( $users as $u ) {
106            $n = $this->userNameUtils->getCanonical( $u );
107            if ( $n === false || $n === '' ) {
108                $vals = [ 'name' => $u, 'invalid' => true ];
109                $fit = $result->addValue( [ 'query', $this->getModuleName() ],
110                    null, $vals );
111                if ( !$fit ) {
112                    $this->setContinueEnumParameter( 'users',
113                        implode( '|', array_diff( $users, $done ) ) );
114                    $goodNames = [];
115                    break;
116                }
117                $done[] = $u;
118            } else {
119                $goodNames[] = $n;
120            }
121        }
122
123        if ( $useNames ) {
124            $parameters = &$goodNames;
125        } else {
126            $parameters = &$userids;
127        }
128
129        $result = $this->getResult();
130
131        if ( count( $parameters ) ) {
132            $this->getQueryBuilder()->merge( User::newQueryBuilder( $db ) );
133            if ( $useNames ) {
134                $this->addWhereFld( 'user_name', $goodNames );
135            } else {
136                $this->addWhereFld( 'user_id', $userids );
137            }
138
139            $this->addDeletedUserFilter();
140
141            $data = [];
142            $res = $this->select( __METHOD__ );
143            $this->resetQueryParams();
144
145            // get user groups if needed
146            if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) {
147                $userGroups = [];
148
149                $this->addTables( 'user' );
150                if ( $useNames ) {
151                    $this->addWhereFld( 'user_name', $goodNames );
152                } else {
153                    $this->addWhereFld( 'user_id', $userids );
154                }
155
156                $this->addTables( 'user_groups' );
157                $this->addJoinConds( [ 'user_groups' => [ 'JOIN', 'ug_user=user_id' ] ] );
158                $this->addFields( [ 'user_name' ] );
159                $this->addFields( [ 'ug_user', 'ug_group', 'ug_expiry' ] );
160                $this->addWhere(
161                    $db->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $db->timestamp() )
162                );
163                $userGroupsRes = $this->select( __METHOD__ );
164
165                foreach ( $userGroupsRes as $row ) {
166                    $userGroups[$row->user_name][] = $row;
167                }
168            }
169            if ( isset( $this->prop['gender'] ) ) {
170                $userNames = [];
171                foreach ( $res as $row ) {
172                    $userNames[] = $row->user_name;
173                }
174                $this->genderCache->doQuery( $userNames, __METHOD__ );
175            }
176
177            if ( isset( $this->prop['blockinfo'] ) ) {
178                $blockInfos = $this->getBlockDetailsForRows( $res );
179            } else {
180                $blockInfos = null;
181            }
182
183            foreach ( $res as $row ) {
184                // create user object and pass along $userGroups if set
185                // that reduces the number of database queries needed in User dramatically
186                if ( !isset( $userGroups ) ) {
187                    $user = $this->userFactory->newFromRow( $row );
188                } else {
189                    if ( !isset( $userGroups[$row->user_name] ) || !is_array( $userGroups[$row->user_name] ) ) {
190                        $userGroups[$row->user_name] = [];
191                    }
192                    $user = $this->userFactory->newFromRow( $row, [ 'user_groups' => $userGroups[$row->user_name] ] );
193                }
194                if ( $useNames ) {
195                    $key = $user->getName();
196                } else {
197                    $key = $user->getId();
198                }
199                $data[$key]['userid'] = $user->getId();
200                $data[$key]['name'] = $user->getName();
201
202                if ( $user->isSystemUser() ) {
203                    $data[$key]['systemuser'] = true;
204                }
205
206                if ( isset( $this->prop['editcount'] ) ) {
207                    $data[$key]['editcount'] = $user->getEditCount();
208                }
209
210                if ( isset( $this->prop['registration'] ) ) {
211                    $data[$key]['registration'] = wfTimestampOrNull( TS_ISO_8601, $user->getRegistration() );
212                }
213
214                if ( isset( $this->prop['groups'] ) ) {
215                    $data[$key]['groups'] = $this->userGroupManager->getUserEffectiveGroups( $user );
216                }
217
218                if ( isset( $this->prop['groupmemberships'] ) ) {
219                    $data[$key]['groupmemberships'] = array_map( static function ( $ugm ) {
220                        return [
221                            'group' => $ugm->getGroup(),
222                            'expiry' => ApiResult::formatExpiry( $ugm->getExpiry() ),
223                        ];
224                    }, $this->userGroupManager->getUserGroupMemberships( $user ) );
225                }
226
227                if ( isset( $this->prop['implicitgroups'] ) ) {
228                    $data[$key]['implicitgroups'] = $this->userGroupManager->getUserImplicitGroups( $user );
229                }
230
231                if ( isset( $this->prop['rights'] ) ) {
232                    $data[$key]['rights'] = $this->getPermissionManager()
233                        ->getUserPermissions( $user );
234                }
235                if ( $row->hu_deleted ) {
236                    $data[$key]['hidden'] = true;
237                }
238                if ( isset( $this->prop['blockinfo'] ) && isset( $blockInfos[$row->user_id] ) ) {
239                    $data[$key] += $blockInfos[$row->user_id];
240                }
241
242                if ( isset( $this->prop['emailable'] ) ) {
243                    $data[$key]['emailable'] = $user->canReceiveEmail();
244                }
245
246                if ( isset( $this->prop['gender'] ) ) {
247                    $data[$key]['gender'] = $this->genderCache->getGenderOf( $user, __METHOD__ );
248                }
249
250                if ( isset( $this->prop['centralids'] ) ) {
251                    $data[$key] += ApiQueryUserInfo::getCentralUserInfo(
252                        $this->getConfig(), $user, $params['attachedwiki']
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    public function getCacheMode( $params ) {
310        if ( array_diff( (array)$params['prop'], static::$publicProps ) ) {
311            return 'anon-public-user-private';
312        } else {
313            return 'public';
314        }
315    }
316
317    public function getAllowedParams() {
318        return [
319            'prop' => [
320                ParamValidator::PARAM_ISMULTI => true,
321                ParamValidator::PARAM_TYPE => [
322                    'blockinfo',
323                    'groups',
324                    'groupmemberships',
325                    'implicitgroups',
326                    'rights',
327                    'editcount',
328                    'registration',
329                    'emailable',
330                    'gender',
331                    'centralids',
332                    'cancreate',
333                    // When adding a prop, consider whether it should be added
334                    // to self::$publicProps
335                ],
336                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
337            ],
338            'attachedwiki' => null,
339            'users' => [
340                ParamValidator::PARAM_ISMULTI => true
341            ],
342            'userids' => [
343                ParamValidator::PARAM_ISMULTI => true,
344                ParamValidator::PARAM_TYPE => 'integer'
345            ],
346        ];
347    }
348
349    protected function getExamplesMessages() {
350        return [
351            'action=query&list=users&ususers=Example&usprop=groups|editcount|gender'
352                => 'apihelp-query+users-example-simple',
353        ];
354    }
355
356    public function getHelpUrls() {
357        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Users';
358    }
359}
360
361/** @deprecated class alias since 1.43 */
362class_alias( ApiQueryUsers::class, 'ApiQueryUsers' );