Provide command to adjust phpunit.xml for code coverage
[lhc/web/wiklou.git] / includes / api / ApiQueryUserInfo.php
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\Block\AbstractBlock;
24 use MediaWiki\MediaWikiServices;
25
26 /**
27 * Query module to get information about the currently logged-in user
28 *
29 * @ingroup API
30 */
31 class ApiQueryUserInfo extends ApiQueryBase {
32
33 use ApiBlockInfoTrait;
34
35 const WL_UNREAD_LIMIT = 1000;
36
37 /** @var array */
38 private $params = [];
39 /** @var array */
40 private $prop = [];
41
42 public function __construct( ApiQuery $query, $moduleName ) {
43 parent::__construct( $query, $moduleName, 'ui' );
44 }
45
46 public function execute() {
47 $this->params = $this->extractRequestParams();
48 $result = $this->getResult();
49
50 if ( !is_null( $this->params['prop'] ) ) {
51 $this->prop = array_flip( $this->params['prop'] );
52 }
53
54 $r = $this->getCurrentUserInfo();
55 $result->addValue( 'query', $this->getModuleName(), $r );
56 }
57
58 /**
59 * Get basic info about a given block
60 *
61 * @deprecated since 1.34 Use ApiBlockInfoTrait::getBlockDetails() instead.
62 * @param AbstractBlock $block
63 * @return array See ApiBlockInfoTrait::getBlockDetails
64 */
65 public static function getBlockInfo( AbstractBlock $block ) {
66 wfDeprecated( __METHOD__, '1.34' );
67
68 // Hack to access a private method from a trait:
69 $dummy = new class {
70 use ApiBlockInfoTrait {
71 getBlockDetails as public;
72 }
73 };
74
75 return $dummy->getBlockDetails( $block );
76 }
77
78 /**
79 * Get central user info
80 * @param Config $config
81 * @param User $user
82 * @param string|null $attachedWiki
83 * @return array Central user info
84 * - centralids: Array mapping non-local Central ID provider names to IDs
85 * - attachedlocal: Array mapping Central ID provider names to booleans
86 * indicating whether the local user is attached.
87 * - attachedwiki: Array mapping Central ID provider names to booleans
88 * indicating whether the user is attached to $attachedWiki.
89 */
90 public static function getCentralUserInfo( Config $config, User $user, $attachedWiki = null ) {
91 $providerIds = array_keys( $config->get( 'CentralIdLookupProviders' ) );
92
93 $ret = [
94 'centralids' => [],
95 'attachedlocal' => [],
96 ];
97 ApiResult::setArrayType( $ret['centralids'], 'assoc' );
98 ApiResult::setArrayType( $ret['attachedlocal'], 'assoc' );
99 if ( $attachedWiki ) {
100 $ret['attachedwiki'] = [];
101 ApiResult::setArrayType( $ret['attachedwiki'], 'assoc' );
102 }
103
104 $name = $user->getName();
105 foreach ( $providerIds as $providerId ) {
106 $provider = CentralIdLookup::factory( $providerId );
107 $ret['centralids'][$providerId] = $provider->centralIdFromName( $name );
108 $ret['attachedlocal'][$providerId] = $provider->isAttached( $user );
109 if ( $attachedWiki ) {
110 $ret['attachedwiki'][$providerId] = $provider->isAttached( $user, $attachedWiki );
111 }
112 }
113
114 return $ret;
115 }
116
117 protected function getCurrentUserInfo() {
118 $user = $this->getUser();
119 $vals = [];
120 $vals['id'] = (int)$user->getId();
121 $vals['name'] = $user->getName();
122
123 if ( $user->isAnon() ) {
124 $vals['anon'] = true;
125 }
126
127 if ( isset( $this->prop['blockinfo'] ) ) {
128 $block = $user->getBlock();
129 if ( $block ) {
130 $vals = array_merge( $vals, $this->getBlockDetails( $block ) );
131 }
132 }
133
134 if ( isset( $this->prop['hasmsg'] ) ) {
135 $vals['messages'] = $user->getNewtalk();
136 }
137
138 if ( isset( $this->prop['groups'] ) ) {
139 $vals['groups'] = $user->getEffectiveGroups();
140 ApiResult::setArrayType( $vals['groups'], 'array' ); // even if empty
141 ApiResult::setIndexedTagName( $vals['groups'], 'g' ); // even if empty
142 }
143
144 if ( isset( $this->prop['groupmemberships'] ) ) {
145 $ugms = $user->getGroupMemberships();
146 $vals['groupmemberships'] = [];
147 foreach ( $ugms as $group => $ugm ) {
148 $vals['groupmemberships'][] = [
149 'group' => $group,
150 'expiry' => ApiResult::formatExpiry( $ugm->getExpiry() ),
151 ];
152 }
153 ApiResult::setArrayType( $vals['groupmemberships'], 'array' ); // even if empty
154 ApiResult::setIndexedTagName( $vals['groupmemberships'], 'groupmembership' ); // even if empty
155 }
156
157 if ( isset( $this->prop['implicitgroups'] ) ) {
158 $vals['implicitgroups'] = $user->getAutomaticGroups();
159 ApiResult::setArrayType( $vals['implicitgroups'], 'array' ); // even if empty
160 ApiResult::setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty
161 }
162
163 if ( isset( $this->prop['rights'] ) ) {
164 $vals['rights'] = $this->getPermissionManager()->getUserPermissions( $user );
165 ApiResult::setArrayType( $vals['rights'], 'array' ); // even if empty
166 ApiResult::setIndexedTagName( $vals['rights'], 'r' ); // even if empty
167 }
168
169 if ( isset( $this->prop['changeablegroups'] ) ) {
170 $vals['changeablegroups'] = $user->changeableGroups();
171 ApiResult::setIndexedTagName( $vals['changeablegroups']['add'], 'g' );
172 ApiResult::setIndexedTagName( $vals['changeablegroups']['remove'], 'g' );
173 ApiResult::setIndexedTagName( $vals['changeablegroups']['add-self'], 'g' );
174 ApiResult::setIndexedTagName( $vals['changeablegroups']['remove-self'], 'g' );
175 }
176
177 if ( isset( $this->prop['options'] ) ) {
178 $vals['options'] = $user->getOptions();
179 $vals['options'][ApiResult::META_BC_BOOLS] = array_keys( $vals['options'] );
180 }
181
182 if ( isset( $this->prop['preferencestoken'] ) &&
183 !$this->lacksSameOriginSecurity() &&
184 $this->getPermissionManager()->userHasRight( $user, 'editmyoptions' )
185 ) {
186 $vals['preferencestoken'] = $user->getEditToken( '', $this->getMain()->getRequest() );
187 }
188
189 if ( isset( $this->prop['editcount'] ) ) {
190 // use intval to prevent null if a non-logged-in user calls
191 // api.php?format=jsonfm&action=query&meta=userinfo&uiprop=editcount
192 $vals['editcount'] = (int)$user->getEditCount();
193 }
194
195 if ( isset( $this->prop['ratelimits'] ) ) {
196 $vals['ratelimits'] = $this->getRateLimits();
197 }
198
199 if ( isset( $this->prop['realname'] ) &&
200 !in_array( 'realname', $this->getConfig()->get( 'HiddenPrefs' ) )
201 ) {
202 $vals['realname'] = $user->getRealName();
203 }
204
205 if ( $this->getPermissionManager()->userHasRight( $user, 'viewmyprivateinfo' ) &&
206 isset( $this->prop['email'] ) ) {
207 $vals['email'] = $user->getEmail();
208 $auth = $user->getEmailAuthenticationTimestamp();
209 if ( $auth !== null ) {
210 $vals['emailauthenticated'] = wfTimestamp( TS_ISO_8601, $auth );
211 }
212 }
213
214 if ( isset( $this->prop['registrationdate'] ) ) {
215 $regDate = $user->getRegistration();
216 if ( $regDate !== false ) {
217 $vals['registrationdate'] = wfTimestamp( TS_ISO_8601, $regDate );
218 }
219 }
220
221 if ( isset( $this->prop['acceptlang'] ) ) {
222 $langs = $this->getRequest()->getAcceptLang();
223 $acceptLang = [];
224 foreach ( $langs as $lang => $val ) {
225 $r = [ 'q' => $val ];
226 ApiResult::setContentValue( $r, 'code', $lang );
227 $acceptLang[] = $r;
228 }
229 ApiResult::setIndexedTagName( $acceptLang, 'lang' );
230 $vals['acceptlang'] = $acceptLang;
231 }
232
233 if ( isset( $this->prop['unreadcount'] ) ) {
234 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
235 $unreadNotifications = $store->countUnreadNotifications(
236 $user,
237 self::WL_UNREAD_LIMIT
238 );
239
240 if ( $unreadNotifications === true ) {
241 $vals['unreadcount'] = self::WL_UNREAD_LIMIT . '+';
242 } else {
243 $vals['unreadcount'] = $unreadNotifications;
244 }
245 }
246
247 if ( isset( $this->prop['centralids'] ) ) {
248 $vals += self::getCentralUserInfo(
249 $this->getConfig(), $this->getUser(), $this->params['attachedwiki']
250 );
251 }
252
253 if ( isset( $this->prop['latestcontrib'] ) ) {
254 $ts = $this->getLatestContributionTime();
255 if ( $ts !== null ) {
256 $vals['latestcontrib'] = $ts;
257 }
258 }
259
260 return $vals;
261 }
262
263 protected function getRateLimits() {
264 $retval = [
265 ApiResult::META_TYPE => 'assoc',
266 ];
267
268 $user = $this->getUser();
269 if ( !$user->isPingLimitable() ) {
270 return $retval; // No limits
271 }
272
273 // Find out which categories we belong to
274 $categories = [];
275 if ( $user->isAnon() ) {
276 $categories[] = 'anon';
277 } else {
278 $categories[] = 'user';
279 }
280 if ( $user->isNewbie() ) {
281 $categories[] = 'ip';
282 $categories[] = 'subnet';
283 if ( !$user->isAnon() ) {
284 $categories[] = 'newbie';
285 }
286 }
287 $categories = array_merge( $categories, $user->getGroups() );
288
289 // Now get the actual limits
290 foreach ( $this->getConfig()->get( 'RateLimits' ) as $action => $limits ) {
291 foreach ( $categories as $cat ) {
292 if ( isset( $limits[$cat] ) && !is_null( $limits[$cat] ) ) {
293 $retval[$action][$cat]['hits'] = (int)$limits[$cat][0];
294 $retval[$action][$cat]['seconds'] = (int)$limits[$cat][1];
295 }
296 }
297 }
298
299 return $retval;
300 }
301
302 /**
303 * @return string|null ISO 8601 timestamp of current user's last contribution or null if none
304 */
305 protected function getLatestContributionTime() {
306 global $wgActorTableSchemaMigrationStage;
307
308 $user = $this->getUser();
309 $dbr = $this->getDB();
310
311 if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
312 if ( $user->getActorId() === null ) {
313 return null;
314 }
315 $res = $dbr->selectField( 'revision_actor_temp',
316 'MAX(revactor_timestamp)',
317 [ 'revactor_actor' => $user->getActorId() ],
318 __METHOD__
319 );
320 } else {
321 if ( $user->isLoggedIn() ) {
322 $conds = [ 'rev_user' => $user->getId() ];
323 } else {
324 $conds = [ 'rev_user_text' => $user->getName() ];
325 }
326 $res = $dbr->selectField( 'revision',
327 'MAX(rev_timestamp)',
328 $conds,
329 __METHOD__
330 );
331 }
332
333 return $res ? wfTimestamp( TS_ISO_8601, $res ) : null;
334 }
335
336 public function getAllowedParams() {
337 return [
338 'prop' => [
339 ApiBase::PARAM_ISMULTI => true,
340 ApiBase::PARAM_TYPE => [
341 'blockinfo',
342 'hasmsg',
343 'groups',
344 'groupmemberships',
345 'implicitgroups',
346 'rights',
347 'changeablegroups',
348 'options',
349 'editcount',
350 'ratelimits',
351 'email',
352 'realname',
353 'acceptlang',
354 'registrationdate',
355 'unreadcount',
356 'centralids',
357 'preferencestoken',
358 'latestcontrib',
359 ],
360 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
361 'unreadcount' => [
362 'apihelp-query+userinfo-paramvalue-prop-unreadcount',
363 self::WL_UNREAD_LIMIT - 1,
364 self::WL_UNREAD_LIMIT . '+',
365 ],
366 ],
367 ApiBase::PARAM_DEPRECATED_VALUES => [
368 'preferencestoken' => [
369 'apiwarn-deprecation-withreplacement',
370 $this->getModulePrefix() . "prop=preferencestoken",
371 'action=query&meta=tokens',
372 ]
373 ],
374 ],
375 'attachedwiki' => null,
376 ];
377 }
378
379 protected function getExamplesMessages() {
380 return [
381 'action=query&meta=userinfo'
382 => 'apihelp-query+userinfo-example-simple',
383 'action=query&meta=userinfo&uiprop=blockinfo|groups|rights|hasmsg'
384 => 'apihelp-query+userinfo-example-data',
385 ];
386 }
387
388 public function getHelpUrls() {
389 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Userinfo';
390 }
391 }