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