Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
42.57% covered (danger)
42.57%
86 / 202
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryUserInfo
42.79% covered (danger)
42.79%
86 / 201
44.44% covered (danger)
44.44%
4 / 9
621.54
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getCentralUserInfo
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 getCurrentUserInfo
37.14% covered (danger)
37.14%
39 / 105
0.00% covered (danger)
0.00%
0 / 1
321.09
 getRateLimits
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
90
 getLatestContributionTime
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getAllowedParams
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
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 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\Config\Config;
12use MediaWiki\MainConfigNames;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Permissions\PermissionStatus;
15use MediaWiki\SpecialPage\SpecialPage;
16use MediaWiki\User\Options\UserOptionsLookup;
17use MediaWiki\User\TalkPageNotificationManager;
18use MediaWiki\User\UserEditTracker;
19use MediaWiki\User\UserGroupManager;
20use MediaWiki\User\UserIdentity;
21use MediaWiki\Utils\MWTimestamp;
22use MediaWiki\Watchlist\WatchedItemStore;
23use MediaWiki\Watchlist\WatchlistLabelStore;
24use Wikimedia\ParamValidator\ParamValidator;
25use Wikimedia\Timestamp\TimestampFormat as TS;
26
27/**
28 * Query module to get information about the currently logged-in user
29 *
30 * @ingroup API
31 */
32class ApiQueryUserInfo extends ApiQueryBase {
33
34    use ApiBlockInfoTrait;
35
36    private const WL_UNREAD_LIMIT = 1000;
37
38    /** @var array */
39    private $params = [];
40
41    /** @var array */
42    private $prop = [];
43
44    public function __construct(
45        ApiQuery $query,
46        string $moduleName,
47        private readonly TalkPageNotificationManager $talkPageNotificationManager,
48        private readonly WatchedItemStore $watchedItemStore,
49        private readonly UserEditTracker $userEditTracker,
50        private readonly UserOptionsLookup $userOptionsLookup,
51        private readonly UserGroupManager $userGroupManager,
52        private readonly WatchlistLabelStore $watchlistLabelStore,
53    ) {
54        parent::__construct( $query, $moduleName, 'ui' );
55    }
56
57    public function execute() {
58        $this->params = $this->extractRequestParams();
59        $result = $this->getResult();
60
61        if ( $this->params['prop'] !== null ) {
62            $this->prop = array_fill_keys( $this->params['prop'], true );
63        }
64
65        $r = $this->getCurrentUserInfo();
66        $result->addValue( 'query', $this->getModuleName(), $r );
67    }
68
69    /**
70     * Get central user info
71     * @param Config $config
72     * @param UserIdentity $user
73     * @param string|false $attachedWiki
74     * @return array Central user info
75     *  - centralids: Array mapping non-local Central ID provider names to IDs
76     *  - attachedlocal: Array mapping Central ID provider names to booleans
77     *    indicating whether the local user is attached.
78     *  - attachedwiki: Array mapping Central ID provider names to booleans
79     *    indicating whether the user is attached to $attachedWiki.
80     */
81    public static function getCentralUserInfo(
82        Config $config,
83        UserIdentity $user,
84        $attachedWiki = UserIdentity::LOCAL
85    ) {
86        $providerIds = array_keys( $config->get( MainConfigNames::CentralIdLookupProviders ) );
87
88        $ret = [
89            'centralids' => [],
90            'attachedlocal' => [],
91        ];
92        ApiResult::setArrayType( $ret['centralids'], 'assoc' );
93        ApiResult::setArrayType( $ret['attachedlocal'], 'assoc' );
94        if ( $attachedWiki ) {
95            $ret['attachedwiki'] = [];
96            ApiResult::setArrayType( $ret['attachedwiki'], 'assoc' );
97        }
98
99        $name = $user->getName();
100        $centralIdLookupFactory = MediaWikiServices::getInstance()
101            ->getCentralIdLookupFactory();
102        foreach ( $providerIds as $providerId ) {
103            $provider = $centralIdLookupFactory->getLookup( $providerId );
104            $ret['centralids'][$providerId] = $provider->centralIdFromName( $name );
105            $ret['attachedlocal'][$providerId] = $provider->isAttached( $user );
106            if ( $attachedWiki ) {
107                $ret['attachedwiki'][$providerId] = $provider->isAttached( $user, $attachedWiki );
108            }
109        }
110
111        return $ret;
112    }
113
114    protected function getCurrentUserInfo(): array {
115        $user = $this->getUser();
116        $vals = [];
117        $vals['id'] = $user->getId();
118        $vals['name'] = $user->getName();
119
120        if ( !$user->isRegistered() ) {
121            $vals['anon'] = true;
122        }
123
124        if ( $user->isTemp() ) {
125            $vals['temp'] = true;
126        }
127
128        if ( isset( $this->prop['blockinfo'] ) ) {
129            $block = $user->getBlock();
130            if ( $block ) {
131                $vals = array_merge( $vals, $this->getBlockDetails( $block ) );
132            }
133        }
134
135        if ( isset( $this->prop['hasmsg'] ) ) {
136            $vals['messages'] = $this->talkPageNotificationManager->userHasNewMessages( $user );
137        }
138
139        if ( isset( $this->prop['groups'] ) ) {
140            $vals['groups'] = $this->userGroupManager->getUserEffectiveGroups( $user );
141            ApiResult::setArrayType( $vals['groups'], 'array' ); // even if empty
142            ApiResult::setIndexedTagName( $vals['groups'], 'g' ); // even if empty
143        }
144
145        if ( isset( $this->prop['groupmemberships'] ) ) {
146            $ugms = $this->userGroupManager->getUserGroupMemberships( $user );
147            $vals['groupmemberships'] = [];
148            foreach ( $ugms as $group => $ugm ) {
149                $vals['groupmemberships'][] = [
150                    'group' => $group,
151                    'expiry' => ApiResult::formatExpiry( $ugm->getExpiry() ),
152                ];
153            }
154            ApiResult::setArrayType( $vals['groupmemberships'], 'array' ); // even if empty
155            ApiResult::setIndexedTagName( $vals['groupmemberships'], 'groupmembership' ); // even if empty
156        }
157
158        if ( isset( $this->prop['implicitgroups'] ) ) {
159            $vals['implicitgroups'] = $this->userGroupManager->getUserImplicitGroups( $user );
160            ApiResult::setArrayType( $vals['implicitgroups'], 'array' ); // even if empty
161            ApiResult::setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty
162        }
163
164        if ( isset( $this->prop['rights'] ) ) {
165            $vals['rights'] = $this->getPermissionManager()->getUserPermissions( $user );
166            ApiResult::setArrayType( $vals['rights'], 'array' ); // even if empty
167            ApiResult::setIndexedTagName( $vals['rights'], 'r' ); // even if empty
168        }
169
170        if ( isset( $this->prop['changeablegroups'] ) ) {
171            $vals['changeablegroups'] = $this->userGroupManager->getGroupsChangeableBy( $this->getAuthority() );
172            ApiResult::setIndexedTagName( $vals['changeablegroups']['add'], 'g' );
173            ApiResult::setIndexedTagName( $vals['changeablegroups']['remove'], 'g' );
174            ApiResult::setIndexedTagName( $vals['changeablegroups']['add-self'], 'g' );
175            ApiResult::setIndexedTagName( $vals['changeablegroups']['remove-self'], 'g' );
176        }
177
178        if ( isset( $this->prop['options'] ) ) {
179            $vals['options'] = $this->userOptionsLookup->getOptions( $user );
180            $vals['options'][ApiResult::META_BC_BOOLS] = array_keys( $vals['options'] );
181        }
182
183        if ( isset( $this->prop['editcount'] ) ) {
184            // use intval to prevent null if a non-logged-in user calls
185            // api.php?format=jsonfm&action=query&meta=userinfo&uiprop=editcount
186            $vals['editcount'] = (int)$user->getEditCount();
187        }
188
189        if ( isset( $this->prop['ratelimits'] ) ) {
190            // true = real rate limits, taking User::isPingLimitable into account
191            $vals['ratelimits'] = $this->getRateLimits( true );
192        }
193        if ( isset( $this->prop['theoreticalratelimits'] ) ) {
194            // false = ignore User::isPingLimitable
195            $vals['theoreticalratelimits'] = $this->getRateLimits( false );
196        }
197
198        if ( isset( $this->prop['realname'] ) &&
199            !in_array( 'realname', $this->getConfig()->get( MainConfigNames::HiddenPrefs ) )
200        ) {
201            $vals['realname'] = $user->getRealName();
202        }
203
204        if ( $this->getAuthority()->isAllowed( 'viewmyprivateinfo' ) && isset( $this->prop['email'] ) ) {
205            $vals['email'] = $user->getEmail();
206            $auth = $user->getEmailAuthenticationTimestamp();
207            if ( $auth !== null ) {
208                $vals['emailauthenticated'] = wfTimestamp( TS::ISO_8601, $auth );
209            }
210        }
211
212        if ( isset( $this->prop['registrationdate'] ) ) {
213            $regDate = $user->getRegistration();
214            if ( $regDate !== false ) {
215                $vals['registrationdate'] = wfTimestampOrNull( TS::ISO_8601, $regDate );
216            }
217        }
218
219        if ( isset( $this->prop['acceptlang'] ) ) {
220            $langs = $this->getRequest()->getAcceptLang();
221            $acceptLang = [];
222            foreach ( $langs as $lang => $val ) {
223                $r = [ 'q' => $val ];
224                ApiResult::setContentValue( $r, 'code', $lang );
225                $acceptLang[] = $r;
226            }
227            ApiResult::setIndexedTagName( $acceptLang, 'lang' );
228            $vals['acceptlang'] = $acceptLang;
229        }
230
231        if ( isset( $this->prop['unreadcount'] ) ) {
232            $unreadNotifications = $this->watchedItemStore->countUnreadNotifications(
233                $user,
234                self::WL_UNREAD_LIMIT
235            );
236
237            if ( $unreadNotifications === true ) {
238                $vals['unreadcount'] = self::WL_UNREAD_LIMIT . '+';
239            } else {
240                $vals['unreadcount'] = $unreadNotifications;
241            }
242        }
243
244        // T409375
245        if ( isset( $this->prop['watchlistlabels'] ) &&
246            $this->getConfig()->get( MainConfigNames::EnableWatchlistLabels )
247        ) {
248            $labels = $this->watchlistLabelStore->loadAllForUser( $user );
249            $wl = array_map( static fn ( $label ) => [
250                'id' => $label->getId(),
251                'name' => $label->getName(),
252            ], $labels );
253            ApiResult::setArrayType( $wl, 'array' );
254            ApiResult::setIndexedTagName( $wl, 'label' );
255            $vals['watchlistlabels'] = $wl;
256        }
257
258        if ( isset( $this->prop['centralids'] ) ) {
259            $vals += self::getCentralUserInfo(
260                $this->getConfig(), $this->getUser(), $this->params['attachedwiki']
261            );
262        }
263
264        if ( isset( $this->prop['latestcontrib'] ) ) {
265            $ts = $this->getLatestContributionTime();
266            if ( $ts !== null ) {
267                $vals['latestcontrib'] = $ts;
268            }
269        }
270
271        if ( isset( $this->prop['cancreateaccount'] ) ) {
272            $status = PermissionStatus::newEmpty();
273            $vals['cancreateaccount'] = $user->definitelyCan( 'createaccount',
274                SpecialPage::getTitleFor( 'CreateAccount' ), $status );
275            if ( !$status->isGood() ) {
276                $vals['cancreateaccounterror'] = $this->getErrorFormatter()->arrayFromStatus( $status );
277            }
278        }
279
280        return $vals;
281    }
282
283    /**
284     * Get the rate limits that apply to the user, or the rate limits
285     * that would apply if the user didn't have `noratelimit`
286     *
287     * @param bool $applyNoRateLimit
288     * @return array
289     */
290    protected function getRateLimits( bool $applyNoRateLimit ) {
291        $retval = [
292            ApiResult::META_TYPE => 'assoc',
293        ];
294
295        $user = $this->getUser();
296        if ( $applyNoRateLimit && !$user->isPingLimitable() ) {
297            return $retval; // No limits
298        }
299
300        // Find out which categories we belong to
301        $categories = [];
302        if ( !$user->isRegistered() ) {
303            $categories[] = 'anon';
304        } else {
305            $categories[] = 'user';
306        }
307        if ( $user->isNewbie() ) {
308            $categories[] = 'ip';
309            $categories[] = 'subnet';
310            if ( $user->isRegistered() ) {
311                $categories[] = 'newbie';
312            }
313        }
314        $categories = array_merge( $categories, $this->userGroupManager->getUserGroups( $user ) );
315
316        // Now get the actual limits
317        foreach ( $this->getConfig()->get( MainConfigNames::RateLimits ) as $action => $limits ) {
318            foreach ( $categories as $cat ) {
319                if ( isset( $limits[$cat] ) ) {
320                    $retval[$action][$cat]['hits'] = (int)$limits[$cat][0];
321                    $retval[$action][$cat]['seconds'] = (int)$limits[$cat][1];
322                }
323            }
324        }
325
326        return $retval;
327    }
328
329    /**
330     * @return string|null ISO 8601 timestamp of current user's last contribution or null if none
331     */
332    protected function getLatestContributionTime() {
333        $timestamp = $this->userEditTracker->getLatestEditTimestamp( $this->getUser() );
334        if ( $timestamp === false ) {
335            return null;
336        }
337        return MWTimestamp::convert( TS::ISO_8601, $timestamp );
338    }
339
340    /** @inheritDoc */
341    public function getAllowedParams() {
342        return [
343            'prop' => [
344                ParamValidator::PARAM_ISMULTI => true,
345                ParamValidator::PARAM_ALL => true,
346                ParamValidator::PARAM_TYPE => [
347                    'blockinfo',
348                    'hasmsg',
349                    'groups',
350                    'groupmemberships',
351                    'implicitgroups',
352                    'rights',
353                    'changeablegroups',
354                    'options',
355                    'editcount',
356                    'ratelimits',
357                    'theoreticalratelimits',
358                    'email',
359                    'realname',
360                    'acceptlang',
361                    'registrationdate',
362                    'unreadcount',
363                    'watchlistlabels',
364                    'centralids',
365                    'latestcontrib',
366                    'cancreateaccount',
367                ],
368                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
369                    'unreadcount' => [
370                        'apihelp-query+userinfo-paramvalue-prop-unreadcount',
371                        self::WL_UNREAD_LIMIT - 1,
372                        self::WL_UNREAD_LIMIT . '+',
373                    ],
374                ],
375            ],
376            'attachedwiki' => null,
377        ];
378    }
379
380    /** @inheritDoc */
381    protected function getExamplesMessages() {
382        return [
383            'action=query&meta=userinfo'
384                => 'apihelp-query+userinfo-example-simple',
385            'action=query&meta=userinfo&uiprop=blockinfo|groups|rights|hasmsg'
386                => 'apihelp-query+userinfo-example-data',
387        ];
388    }
389
390    /** @inheritDoc */
391    public function getHelpUrls() {
392        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Userinfo';
393    }
394}
395
396/** @deprecated class alias since 1.43 */
397class_alias( ApiQueryUserInfo::class, 'ApiQueryUserInfo' );