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