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