Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / api / ApiLogin.php
blobe2e1bdac55705af23658ec962e91b60b1fe07701
1 <?php
2 /**
3 * Copyright © 2006-2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com",
4 * Daniel Cannon (cannon dot danielc at gmail dot com)
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
21 * @file
24 namespace MediaWiki\Api;
26 use MediaWiki\Auth\AuthenticationRequest;
27 use MediaWiki\Auth\AuthenticationResponse;
28 use MediaWiki\Auth\AuthManager;
29 use MediaWiki\Logger\LoggerFactory;
30 use MediaWiki\MainConfigNames;
31 use MediaWiki\Message\Message;
32 use MediaWiki\Session\SessionManager;
33 use MediaWiki\User\BotPassword;
34 use MediaWiki\User\UserIdentityUtils;
35 use Wikimedia\ParamValidator\ParamValidator;
37 /**
38 * Unit to authenticate log-in attempts to the current wiki.
40 * @ingroup API
42 class ApiLogin extends ApiBase {
44 private AuthManager $authManager;
46 private UserIdentityUtils $identityUtils;
48 /**
49 * @param ApiMain $main
50 * @param string $action
51 * @param AuthManager $authManager
52 * @param UserIdentityUtils $identityUtils IdentityUtils to retrieve account type
54 public function __construct(
55 ApiMain $main,
56 string $action,
57 AuthManager $authManager,
58 UserIdentityUtils $identityUtils
59 ) {
60 parent::__construct( $main, $action, 'lg' );
61 $this->authManager = $authManager;
62 $this->identityUtils = $identityUtils;
65 protected function getExtendedDescription() {
66 if ( $this->getConfig()->get( MainConfigNames::EnableBotPasswords ) ) {
67 return 'apihelp-login-extended-description';
68 } else {
69 return 'apihelp-login-extended-description-nobotpasswords';
73 /**
74 * Format a message for the response
75 * @param Message|string|array $message
76 * @return string|array
78 private function formatMessage( $message ) {
79 $message = Message::newFromSpecifier( $message );
80 $errorFormatter = $this->getErrorFormatter();
81 if ( $errorFormatter instanceof ApiErrorFormatter_BackCompat ) {
82 return ApiErrorFormatter::stripMarkup(
83 $message->useDatabase( false )->inLanguage( 'en' )->text()
85 } else {
86 return $errorFormatter->formatMessage( $message );
90 /**
91 * Executes the log-in attempt using the parameters passed. If
92 * the log-in succeeds, it attaches a cookie to the session
93 * and outputs the user id, username, and session token. If a
94 * log-in fails, as the result of a bad password, a nonexistent
95 * user, or any other reason, the host is cached with an expiry
96 * and no log-in attempts will be accepted until that expiry
97 * is reached. The expiry is $this->mLoginThrottle.
99 public function execute() {
100 // If we're in a mode that breaks the same-origin policy, no tokens can
101 // be obtained
102 if ( $this->lacksSameOriginSecurity() ) {
103 $this->getResult()->addValue( null, 'login', [
104 'result' => 'Aborted',
105 'reason' => $this->formatMessage( 'api-login-fail-sameorigin' ),
106 ] );
108 return;
111 $this->requirePostedParameters( [ 'password', 'token' ] );
113 $params = $this->extractRequestParams();
115 $result = [];
117 // Make sure session is persisted
118 $session = SessionManager::getGlobalSession();
119 $session->persist();
121 // Make sure it's possible to log in
122 if ( !$session->canSetUser() ) {
123 $this->getResult()->addValue( null, 'login', [
124 'result' => 'Aborted',
125 'reason' => $this->formatMessage( [
126 'api-login-fail-badsessionprovider',
127 $session->getProvider()->describe( $this->getErrorFormatter()->getLanguage() ),
129 ] );
131 return;
134 $authRes = false;
135 $loginType = 'N/A';
136 $performer = $this->getUser();
138 // Check login token
139 $token = $session->getToken( '', 'login' );
140 if ( !$params['token'] ) {
141 $authRes = 'NeedToken';
142 } elseif ( $token->wasNew() ) {
143 $authRes = 'Failed';
144 $message = ApiMessage::create( 'authpage-cannot-login-continue', 'sessionlost' );
145 } elseif ( !$token->match( $params['token'] ) ) {
146 $authRes = 'WrongToken';
149 // Try bot passwords
150 if ( $authRes === false && $this->getConfig()->get( MainConfigNames::EnableBotPasswords ) ) {
151 $botLoginData = BotPassword::canonicalizeLoginData( $params['name'], $params['password'] );
152 if ( $botLoginData ) {
153 $status = BotPassword::login(
154 $botLoginData[0], $botLoginData[1], $this->getRequest()
156 if ( $status->isOK() ) {
157 $session = $status->getValue();
158 $authRes = 'Success';
159 $loginType = 'BotPassword';
160 } elseif (
161 $status->hasMessage( 'login-throttled' ) ||
162 $status->hasMessage( 'botpasswords-needs-reset' ) ||
163 $status->hasMessage( 'botpasswords-locked' )
165 $authRes = 'Failed';
166 $message = $status->getMessage();
167 LoggerFactory::getInstance( 'authentication' )->info(
168 'BotPassword login failed: ' . $status->getWikiText( false, false, 'en' )
172 // For other errors, let's see if it's a valid non-bot login
175 if ( $authRes === false ) {
176 // Simplified AuthManager login, for backwards compatibility
177 $reqs = AuthenticationRequest::loadRequestsFromSubmission(
178 $this->authManager->getAuthenticationRequests(
179 AuthManager::ACTION_LOGIN,
180 $this->getUser()
183 'username' => $params['name'],
184 'password' => $params['password'],
185 'domain' => $params['domain'],
186 'rememberMe' => true,
189 $res = $this->authManager->beginAuthentication( $reqs, 'null:' );
190 switch ( $res->status ) {
191 case AuthenticationResponse::PASS:
192 if ( $this->getConfig()->get( MainConfigNames::EnableBotPasswords ) ) {
193 $this->addDeprecation( 'apiwarn-deprecation-login-botpw', 'main-account-login' );
194 } else {
195 $this->addDeprecation( 'apiwarn-deprecation-login-nobotpw', 'main-account-login' );
197 $authRes = 'Success';
198 $loginType = 'AuthManager';
199 break;
201 case AuthenticationResponse::FAIL:
202 // Hope it's not a PreAuthenticationProvider that failed...
203 $authRes = 'Failed';
204 $message = $res->message;
205 LoggerFactory::getInstance( 'authentication' )
206 ->info( __METHOD__ . ': Authentication failed: '
207 . $message->inLanguage( 'en' )->plain() );
208 break;
210 default:
211 LoggerFactory::getInstance( 'authentication' )
212 ->info( __METHOD__ . ': Authentication failed due to unsupported response type: '
213 . $res->status, $this->getAuthenticationResponseLogData( $res ) );
214 $authRes = 'Aborted';
215 break;
219 $result['result'] = $authRes;
220 switch ( $authRes ) {
221 case 'Success':
222 $user = $session->getUser();
224 // Deprecated hook
225 $injected_html = '';
226 $this->getHookRunner()->onUserLoginComplete( $user, $injected_html, true );
228 $result['lguserid'] = $user->getId();
229 $result['lgusername'] = $user->getName();
230 break;
232 case 'NeedToken':
233 $result['token'] = $token->toString();
234 $this->addDeprecation( 'apiwarn-deprecation-login-token', 'action=login&!lgtoken' );
235 break;
237 case 'WrongToken':
238 break;
240 case 'Failed':
241 // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable
242 // message set on error
243 $result['reason'] = $this->formatMessage( $message );
244 break;
246 case 'Aborted':
247 $result['reason'] = $this->formatMessage(
248 $this->getConfig()->get( MainConfigNames::EnableBotPasswords )
249 ? 'api-login-fail-aborted'
250 : 'api-login-fail-aborted-nobotpw'
252 break;
254 // @codeCoverageIgnoreStart
255 // Unreachable
256 default:
257 ApiBase::dieDebug( __METHOD__, "Unhandled case value: {$authRes}" );
258 // @codeCoverageIgnoreEnd
261 $this->getResult()->addValue( null, 'login', $result );
263 LoggerFactory::getInstance( 'authevents' )->info( 'Login attempt', [
264 'event' => 'login',
265 'successful' => $authRes === 'Success',
266 'accountType' => $this->identityUtils->getShortUserTypeInternal( $performer ),
267 'loginType' => $loginType,
268 'status' => $authRes,
269 ] );
272 public function isDeprecated() {
273 return !$this->getConfig()->get( MainConfigNames::EnableBotPasswords );
276 public function mustBePosted() {
277 return true;
280 public function isReadMode() {
281 return false;
284 public function isWriteMode() {
285 // (T283394) Logging in triggers some database writes, so should be marked appropriately.
286 return true;
289 public function getAllowedParams() {
290 return [
291 'name' => null,
292 'password' => [
293 ParamValidator::PARAM_TYPE => 'password',
295 'domain' => null,
296 'token' => [
297 ParamValidator::PARAM_TYPE => 'string',
298 ParamValidator::PARAM_REQUIRED => false, // for BC
299 ParamValidator::PARAM_SENSITIVE => true,
300 ApiBase::PARAM_HELP_MSG => [ 'api-help-param-token', 'login' ],
305 protected function getExamplesMessages() {
306 return [
307 'action=login&lgname=user&lgpassword=password&lgtoken=123ABC'
308 => 'apihelp-login-example-login',
312 public function getHelpUrls() {
313 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Login';
317 * Turns an AuthenticationResponse into a hash suitable for passing to Logger
318 * @param AuthenticationResponse $response
319 * @return array
321 protected function getAuthenticationResponseLogData( AuthenticationResponse $response ) {
322 $ret = [
323 'status' => $response->status,
325 if ( $response->message ) {
326 $ret['responseMessage'] = $response->message->inLanguage( 'en' )->plain();
328 $reqs = [
329 'neededRequests' => $response->neededRequests,
330 'createRequest' => $response->createRequest,
331 'linkRequest' => $response->linkRequest,
333 foreach ( $reqs as $k => $v ) {
334 if ( $v ) {
335 $v = is_array( $v ) ? $v : [ $v ];
336 $reqClasses = array_unique( array_map( 'get_class', $v ) );
337 sort( $reqClasses );
338 $ret[$k] = implode( ', ', $reqClasses );
341 return $ret;
345 /** @deprecated class alias since 1.43 */
346 class_alias( ApiLogin::class, 'ApiLogin' );