Merge "Simplify code to avoid interpreting "$" characters in string replacement"
[mediawiki.git] / includes / api / ApiQueryUserInfo.php
blob125281e5d42eed72986e7ba21907b155cd104c47
1 <?php
2 /**
3 * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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.
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.
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
20 * @file
23 namespace MediaWiki\Api;
25 use MediaWiki\Config\Config;
26 use MediaWiki\MainConfigNames;
27 use MediaWiki\MediaWikiServices;
28 use MediaWiki\Permissions\PermissionStatus;
29 use MediaWiki\SpecialPage\SpecialPage;
30 use MediaWiki\User\Options\UserOptionsLookup;
31 use MediaWiki\User\TalkPageNotificationManager;
32 use MediaWiki\User\UserEditTracker;
33 use MediaWiki\User\UserGroupManager;
34 use MediaWiki\User\UserIdentity;
35 use MediaWiki\Utils\MWTimestamp;
36 use MediaWiki\Watchlist\WatchedItemStore;
37 use Wikimedia\ParamValidator\ParamValidator;
39 /**
40 * Query module to get information about the currently logged-in user
42 * @ingroup API
44 class ApiQueryUserInfo extends ApiQueryBase {
46 use ApiBlockInfoTrait;
48 private const WL_UNREAD_LIMIT = 1000;
50 /** @var array */
51 private $params = [];
53 /** @var array */
54 private $prop = [];
56 private TalkPageNotificationManager $talkPageNotificationManager;
57 private WatchedItemStore $watchedItemStore;
58 private UserEditTracker $userEditTracker;
59 private UserOptionsLookup $userOptionsLookup;
60 private UserGroupManager $userGroupManager;
62 /**
63 * @param ApiQuery $query
64 * @param string $moduleName
65 * @param TalkPageNotificationManager $talkPageNotificationManager
66 * @param WatchedItemStore $watchedItemStore
67 * @param UserEditTracker $userEditTracker
68 * @param UserOptionsLookup $userOptionsLookup
69 * @param UserGroupManager $userGroupManager
71 public function __construct(
72 ApiQuery $query,
73 $moduleName,
74 TalkPageNotificationManager $talkPageNotificationManager,
75 WatchedItemStore $watchedItemStore,
76 UserEditTracker $userEditTracker,
77 UserOptionsLookup $userOptionsLookup,
78 UserGroupManager $userGroupManager
79 ) {
80 parent::__construct( $query, $moduleName, 'ui' );
81 $this->talkPageNotificationManager = $talkPageNotificationManager;
82 $this->watchedItemStore = $watchedItemStore;
83 $this->userEditTracker = $userEditTracker;
84 $this->userOptionsLookup = $userOptionsLookup;
85 $this->userGroupManager = $userGroupManager;
88 public function execute() {
89 $this->params = $this->extractRequestParams();
90 $result = $this->getResult();
92 if ( $this->params['prop'] !== null ) {
93 $this->prop = array_fill_keys( $this->params['prop'], true );
96 $r = $this->getCurrentUserInfo();
97 $result->addValue( 'query', $this->getModuleName(), $r );
101 * Get central user info
102 * @param Config $config
103 * @param UserIdentity $user
104 * @param string|false $attachedWiki
105 * @return array Central user info
106 * - centralids: Array mapping non-local Central ID provider names to IDs
107 * - attachedlocal: Array mapping Central ID provider names to booleans
108 * indicating whether the local user is attached.
109 * - attachedwiki: Array mapping Central ID provider names to booleans
110 * indicating whether the user is attached to $attachedWiki.
112 public static function getCentralUserInfo(
113 Config $config,
114 UserIdentity $user,
115 $attachedWiki = UserIdentity::LOCAL
117 $providerIds = array_keys( $config->get( MainConfigNames::CentralIdLookupProviders ) );
119 $ret = [
120 'centralids' => [],
121 'attachedlocal' => [],
123 ApiResult::setArrayType( $ret['centralids'], 'assoc' );
124 ApiResult::setArrayType( $ret['attachedlocal'], 'assoc' );
125 if ( $attachedWiki ) {
126 $ret['attachedwiki'] = [];
127 ApiResult::setArrayType( $ret['attachedwiki'], 'assoc' );
130 $name = $user->getName();
131 $centralIdLookupFactory = MediaWikiServices::getInstance()
132 ->getCentralIdLookupFactory();
133 foreach ( $providerIds as $providerId ) {
134 $provider = $centralIdLookupFactory->getLookup( $providerId );
135 $ret['centralids'][$providerId] = $provider->centralIdFromName( $name );
136 $ret['attachedlocal'][$providerId] = $provider->isAttached( $user );
137 if ( $attachedWiki ) {
138 $ret['attachedwiki'][$providerId] = $provider->isAttached( $user, $attachedWiki );
142 return $ret;
145 protected function getCurrentUserInfo() {
146 $user = $this->getUser();
147 $vals = [];
148 $vals['id'] = $user->getId();
149 $vals['name'] = $user->getName();
151 if ( !$user->isRegistered() ) {
152 $vals['anon'] = true;
155 if ( $user->isTemp() ) {
156 $vals['temp'] = true;
159 if ( isset( $this->prop['blockinfo'] ) ) {
160 $block = $user->getBlock();
161 if ( $block ) {
162 $vals = array_merge( $vals, $this->getBlockDetails( $block ) );
166 if ( isset( $this->prop['hasmsg'] ) ) {
167 $vals['messages'] = $this->talkPageNotificationManager->userHasNewMessages( $user );
170 if ( isset( $this->prop['groups'] ) ) {
171 $vals['groups'] = $this->userGroupManager->getUserEffectiveGroups( $user );
172 ApiResult::setArrayType( $vals['groups'], 'array' ); // even if empty
173 ApiResult::setIndexedTagName( $vals['groups'], 'g' ); // even if empty
176 if ( isset( $this->prop['groupmemberships'] ) ) {
177 $ugms = $this->userGroupManager->getUserGroupMemberships( $user );
178 $vals['groupmemberships'] = [];
179 foreach ( $ugms as $group => $ugm ) {
180 $vals['groupmemberships'][] = [
181 'group' => $group,
182 'expiry' => ApiResult::formatExpiry( $ugm->getExpiry() ),
185 ApiResult::setArrayType( $vals['groupmemberships'], 'array' ); // even if empty
186 ApiResult::setIndexedTagName( $vals['groupmemberships'], 'groupmembership' ); // even if empty
189 if ( isset( $this->prop['implicitgroups'] ) ) {
190 $vals['implicitgroups'] = $this->userGroupManager->getUserImplicitGroups( $user );
191 ApiResult::setArrayType( $vals['implicitgroups'], 'array' ); // even if empty
192 ApiResult::setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty
195 if ( isset( $this->prop['rights'] ) ) {
196 $vals['rights'] = $this->getPermissionManager()->getUserPermissions( $user );
197 ApiResult::setArrayType( $vals['rights'], 'array' ); // even if empty
198 ApiResult::setIndexedTagName( $vals['rights'], 'r' ); // even if empty
201 if ( isset( $this->prop['changeablegroups'] ) ) {
202 $vals['changeablegroups'] = $this->userGroupManager->getGroupsChangeableBy( $this->getAuthority() );
203 ApiResult::setIndexedTagName( $vals['changeablegroups']['add'], 'g' );
204 ApiResult::setIndexedTagName( $vals['changeablegroups']['remove'], 'g' );
205 ApiResult::setIndexedTagName( $vals['changeablegroups']['add-self'], 'g' );
206 ApiResult::setIndexedTagName( $vals['changeablegroups']['remove-self'], 'g' );
209 if ( isset( $this->prop['options'] ) ) {
210 $vals['options'] = $this->userOptionsLookup->getOptions( $user );
211 $vals['options'][ApiResult::META_BC_BOOLS] = array_keys( $vals['options'] );
214 if ( isset( $this->prop['editcount'] ) ) {
215 // use intval to prevent null if a non-logged-in user calls
216 // api.php?format=jsonfm&action=query&meta=userinfo&uiprop=editcount
217 $vals['editcount'] = (int)$user->getEditCount();
220 if ( isset( $this->prop['ratelimits'] ) ) {
221 // true = real rate limits, taking User::isPingLimitable into account
222 $vals['ratelimits'] = $this->getRateLimits( true );
224 if ( isset( $this->prop['theoreticalratelimits'] ) ) {
225 // false = ignore User::isPingLimitable
226 $vals['theoreticalratelimits'] = $this->getRateLimits( false );
229 if ( isset( $this->prop['realname'] ) &&
230 !in_array( 'realname', $this->getConfig()->get( MainConfigNames::HiddenPrefs ) )
232 $vals['realname'] = $user->getRealName();
235 if ( $this->getAuthority()->isAllowed( 'viewmyprivateinfo' ) && isset( $this->prop['email'] ) ) {
236 $vals['email'] = $user->getEmail();
237 $auth = $user->getEmailAuthenticationTimestamp();
238 if ( $auth !== null ) {
239 $vals['emailauthenticated'] = wfTimestamp( TS_ISO_8601, $auth );
243 if ( isset( $this->prop['registrationdate'] ) ) {
244 $regDate = $user->getRegistration();
245 if ( $regDate !== false ) {
246 $vals['registrationdate'] = wfTimestampOrNull( TS_ISO_8601, $regDate );
250 if ( isset( $this->prop['acceptlang'] ) ) {
251 $langs = $this->getRequest()->getAcceptLang();
252 $acceptLang = [];
253 foreach ( $langs as $lang => $val ) {
254 $r = [ 'q' => $val ];
255 ApiResult::setContentValue( $r, 'code', $lang );
256 $acceptLang[] = $r;
258 ApiResult::setIndexedTagName( $acceptLang, 'lang' );
259 $vals['acceptlang'] = $acceptLang;
262 if ( isset( $this->prop['unreadcount'] ) ) {
263 $unreadNotifications = $this->watchedItemStore->countUnreadNotifications(
264 $user,
265 self::WL_UNREAD_LIMIT
268 if ( $unreadNotifications === true ) {
269 $vals['unreadcount'] = self::WL_UNREAD_LIMIT . '+';
270 } else {
271 $vals['unreadcount'] = $unreadNotifications;
275 if ( isset( $this->prop['centralids'] ) ) {
276 $vals += self::getCentralUserInfo(
277 $this->getConfig(), $this->getUser(), $this->params['attachedwiki']
281 if ( isset( $this->prop['latestcontrib'] ) ) {
282 $ts = $this->getLatestContributionTime();
283 if ( $ts !== null ) {
284 $vals['latestcontrib'] = $ts;
288 if ( isset( $this->prop['cancreateaccount'] ) ) {
289 $status = PermissionStatus::newEmpty();
290 $vals['cancreateaccount'] = $user->definitelyCan( 'createaccount',
291 SpecialPage::getTitleFor( 'CreateAccount' ), $status );
292 if ( !$status->isGood() ) {
293 $vals['cancreateaccounterror'] = $this->getErrorFormatter()->arrayFromStatus( $status );
297 return $vals;
301 * Get the rate limits that apply to the user, or the rate limits
302 * that would apply if the user didn't have `noratelimit`
304 * @param bool $applyNoRateLimit
305 * @return array
307 protected function getRateLimits( bool $applyNoRateLimit ) {
308 $retval = [
309 ApiResult::META_TYPE => 'assoc',
312 $user = $this->getUser();
313 if ( $applyNoRateLimit && !$user->isPingLimitable() ) {
314 return $retval; // No limits
317 // Find out which categories we belong to
318 $categories = [];
319 if ( !$user->isRegistered() ) {
320 $categories[] = 'anon';
321 } else {
322 $categories[] = 'user';
324 if ( $user->isNewbie() ) {
325 $categories[] = 'ip';
326 $categories[] = 'subnet';
327 if ( $user->isRegistered() ) {
328 $categories[] = 'newbie';
331 $categories = array_merge( $categories, $this->userGroupManager->getUserGroups( $user ) );
333 // Now get the actual limits
334 foreach ( $this->getConfig()->get( MainConfigNames::RateLimits ) as $action => $limits ) {
335 foreach ( $categories as $cat ) {
336 if ( isset( $limits[$cat] ) ) {
337 $retval[$action][$cat]['hits'] = (int)$limits[$cat][0];
338 $retval[$action][$cat]['seconds'] = (int)$limits[$cat][1];
343 return $retval;
347 * @return string|null ISO 8601 timestamp of current user's last contribution or null if none
349 protected function getLatestContributionTime() {
350 $timestamp = $this->userEditTracker->getLatestEditTimestamp( $this->getUser() );
351 if ( $timestamp === false ) {
352 return null;
354 return MWTimestamp::convert( TS_ISO_8601, $timestamp );
357 public function getAllowedParams() {
358 return [
359 'prop' => [
360 ParamValidator::PARAM_ISMULTI => true,
361 ParamValidator::PARAM_ALL => true,
362 ParamValidator::PARAM_TYPE => [
363 'blockinfo',
364 'hasmsg',
365 'groups',
366 'groupmemberships',
367 'implicitgroups',
368 'rights',
369 'changeablegroups',
370 'options',
371 'editcount',
372 'ratelimits',
373 'theoreticalratelimits',
374 'email',
375 'realname',
376 'acceptlang',
377 'registrationdate',
378 'unreadcount',
379 'centralids',
380 'latestcontrib',
381 'cancreateaccount',
383 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
384 'unreadcount' => [
385 'apihelp-query+userinfo-paramvalue-prop-unreadcount',
386 self::WL_UNREAD_LIMIT - 1,
387 self::WL_UNREAD_LIMIT . '+',
391 'attachedwiki' => null,
395 protected function getExamplesMessages() {
396 return [
397 'action=query&meta=userinfo'
398 => 'apihelp-query+userinfo-example-simple',
399 'action=query&meta=userinfo&uiprop=blockinfo|groups|rights|hasmsg'
400 => 'apihelp-query+userinfo-example-data',
404 public function getHelpUrls() {
405 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Userinfo';
409 /** @deprecated class alias since 1.43 */
410 class_alias( ApiQueryUserInfo::class, 'ApiQueryUserInfo' );