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