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