Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
45.13% |
88 / 195 |
|
44.44% |
4 / 9 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryUserInfo | |
45.13% |
88 / 195 |
|
44.44% |
4 / 9 |
517.09 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getCentralUserInfo | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
getCurrentUserInfo | |
38.95% |
37 / 95 |
|
0.00% |
0 / 1 |
265.03 | |||
getRateLimits | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
getLatestContributionTime | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getAllowedParams | |
100.00% |
35 / 35 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
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 | |
23 | use MediaWiki\Config\Config; |
24 | use MediaWiki\MainConfigNames; |
25 | use MediaWiki\MediaWikiServices; |
26 | use MediaWiki\Permissions\PermissionStatus; |
27 | use MediaWiki\SpecialPage\SpecialPage; |
28 | use MediaWiki\User\Options\UserOptionsLookup; |
29 | use MediaWiki\User\TalkPageNotificationManager; |
30 | use MediaWiki\User\UserEditTracker; |
31 | use MediaWiki\User\UserGroupManager; |
32 | use MediaWiki\User\UserIdentity; |
33 | use MediaWiki\Utils\MWTimestamp; |
34 | use Wikimedia\ParamValidator\ParamValidator; |
35 | |
36 | /** |
37 | * Query module to get information about the currently logged-in user |
38 | * |
39 | * @ingroup API |
40 | */ |
41 | class 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 | } |