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