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