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