Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / api / ApiAuthManagerHelper.php
blob37c36a665e8c3c7554d7d0c16feb91e43bcabb05
1 <?php
2 /**
3 * Copyright © 2016 Wikimedia Foundation and contributors
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
21 * @since 1.27
24 namespace MediaWiki\Api;
26 use MediaWiki\Auth\AuthenticationRequest;
27 use MediaWiki\Auth\AuthenticationResponse;
28 use MediaWiki\Auth\AuthManager;
29 use MediaWiki\Auth\CreateFromLoginAuthenticationRequest;
30 use MediaWiki\Logger\LoggerFactory;
31 use MediaWiki\MediaWikiServices;
32 use MediaWiki\Message\Message;
33 use MediaWiki\Parser\Parser;
34 use MediaWiki\User\UserIdentity;
35 use MediaWiki\User\UserIdentityUtils;
36 use UnexpectedValueException;
37 use Wikimedia\ParamValidator\ParamValidator;
39 /**
40 * Helper class for AuthManager-using API modules. Intended for use via
41 * composition.
43 * @ingroup API
45 class ApiAuthManagerHelper {
47 /** @var ApiBase API module, for context and parameters */
48 private $module;
50 /** @var string Message output format */
51 private $messageFormat;
53 private AuthManager $authManager;
55 private UserIdentityUtils $identityUtils;
57 /**
58 * @param ApiBase $module API module, for context and parameters
59 * @param AuthManager|null $authManager
60 * @param UserIdentityUtils|null $identityUtils
62 public function __construct(
63 ApiBase $module,
64 ?AuthManager $authManager = null,
65 ?UserIdentityUtils $identityUtils = null
66 ) {
67 $this->module = $module;
69 $params = $module->extractRequestParams();
70 $this->messageFormat = $params['messageformat'] ?? 'wikitext';
71 $this->authManager = $authManager ?: MediaWikiServices::getInstance()->getAuthManager();
72 // TODO: inject this as currently it's always taken from container
73 $this->identityUtils = $identityUtils ?: MediaWikiServices::getInstance()->getUserIdentityUtils();
76 /**
77 * Static version of the constructor, for chaining
78 * @param ApiBase $module API module, for context and parameters
79 * @param AuthManager|null $authManager
80 * @return ApiAuthManagerHelper
82 public static function newForModule( ApiBase $module, ?AuthManager $authManager = null ) {
83 return new self( $module, $authManager );
86 /**
87 * Format a message for output
88 * @param array &$res Result array
89 * @param string $key Result key
90 * @param Message $message
92 private function formatMessage( array &$res, $key, Message $message ) {
93 switch ( $this->messageFormat ) {
94 case 'none':
95 break;
97 case 'wikitext':
98 $res[$key] = $message->setContext( $this->module )->text();
99 break;
101 case 'html':
102 $res[$key] = $message->setContext( $this->module )->parseAsBlock();
103 $res[$key] = Parser::stripOuterParagraph( $res[$key] );
104 break;
106 case 'raw':
107 $params = $message->getParams();
108 $res[$key] = [
109 'key' => $message->getKey(),
110 'params' => $params,
112 ApiResult::setIndexedTagName( $params, 'param' );
113 break;
118 * Call $manager->securitySensitiveOperationStatus()
119 * @param string $operation Operation being checked.
120 * @throws ApiUsageException
122 public function securitySensitiveOperation( $operation ) {
123 $status = $this->authManager->securitySensitiveOperationStatus( $operation );
124 switch ( $status ) {
125 case AuthManager::SEC_OK:
126 return;
128 case AuthManager::SEC_REAUTH:
129 $this->module->dieWithError( 'apierror-reauthenticate' );
130 // dieWithError prevents continuation
132 case AuthManager::SEC_FAIL:
133 $this->module->dieWithError( 'apierror-cannotreauthenticate' );
134 // dieWithError prevents continuation
136 default:
137 throw new UnexpectedValueException( "Unknown status \"$status\"" );
142 * Filter out authentication requests by class name
143 * @param AuthenticationRequest[] $reqs Requests to filter
144 * @param string[] $remove Class names to remove
145 * @return AuthenticationRequest[]
147 public static function blacklistAuthenticationRequests( array $reqs, array $remove ) {
148 if ( $remove ) {
149 $remove = array_fill_keys( $remove, true );
150 $reqs = array_filter( $reqs, static function ( $req ) use ( $remove ) {
151 return !isset( $remove[get_class( $req )] );
152 } );
154 return $reqs;
158 * Fetch and load the AuthenticationRequests for an action
159 * @param string $action One of the AuthManager::ACTION_* constants
160 * @return AuthenticationRequest[]
162 public function loadAuthenticationRequests( $action ) {
163 $params = $this->module->extractRequestParams();
165 $reqs = $this->authManager->getAuthenticationRequests( $action, $this->module->getUser() );
167 // Filter requests, if requested to do so
168 $wantedRequests = null;
169 if ( isset( $params['requests'] ) ) {
170 $wantedRequests = array_fill_keys( $params['requests'], true );
171 } elseif ( isset( $params['request'] ) ) {
172 $wantedRequests = [ $params['request'] => true ];
174 if ( $wantedRequests !== null ) {
175 $reqs = array_filter(
176 $reqs,
177 static function ( AuthenticationRequest $req ) use ( $wantedRequests ) {
178 return isset( $wantedRequests[$req->getUniqueId()] );
183 // Collect the fields for all the requests
184 $fields = [];
185 $sensitive = [];
186 foreach ( $reqs as $req ) {
187 $info = (array)$req->getFieldInfo();
188 $fields += $info;
189 $sensitive += array_filter( $info, static function ( $opts ) {
190 return !empty( $opts['sensitive'] );
191 } );
194 // Extract the request data for the fields and mark those request
195 // parameters as used
196 $data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
197 $this->module->getMain()->markParamsUsed( array_keys( $data ) );
199 if ( $sensitive ) {
200 $this->module->getMain()->markParamsSensitive( array_keys( $sensitive ) );
201 $this->module->requirePostedParameters( array_keys( $sensitive ), 'noprefix' );
204 return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
208 * Format an AuthenticationResponse for return
209 * @param AuthenticationResponse $res
210 * @return array
212 public function formatAuthenticationResponse( AuthenticationResponse $res ) {
213 $ret = [
214 'status' => $res->status,
217 if ( $res->status === AuthenticationResponse::PASS && $res->username !== null ) {
218 $ret['username'] = $res->username;
221 if ( $res->status === AuthenticationResponse::REDIRECT ) {
222 $ret['redirecttarget'] = $res->redirectTarget;
223 if ( $res->redirectApiData !== null ) {
224 $ret['redirectdata'] = $res->redirectApiData;
228 if ( $res->status === AuthenticationResponse::REDIRECT ||
229 $res->status === AuthenticationResponse::UI ||
230 $res->status === AuthenticationResponse::RESTART
232 $ret += $this->formatRequests( $res->neededRequests );
235 if ( $res->status === AuthenticationResponse::FAIL ||
236 $res->status === AuthenticationResponse::UI ||
237 $res->status === AuthenticationResponse::RESTART
239 $this->formatMessage( $ret, 'message', $res->message );
240 $ret['messagecode'] = ApiMessage::create( $res->message )->getApiCode();
243 if ( $res->status === AuthenticationResponse::FAIL ||
244 $res->status === AuthenticationResponse::RESTART
246 $this->module->getRequest()->getSession()->set(
247 'ApiAuthManagerHelper::createRequest',
248 $res->createRequest
250 $ret['canpreservestate'] = $res->createRequest !== null;
251 } else {
252 $this->module->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
255 return $ret;
259 * Logs successful or failed authentication.
260 * @param string $event Event type (e.g. 'accountcreation')
261 * @param UserIdentity $performer
262 * @param AuthenticationResponse $result Response or error message
264 public function logAuthenticationResult( $event, UserIdentity $performer, AuthenticationResponse $result ) {
265 if ( !in_array( $result->status, [ AuthenticationResponse::PASS, AuthenticationResponse::FAIL ] ) ) {
266 return;
268 $accountType = $this->identityUtils->getShortUserTypeInternal( $performer );
270 $module = $this->module->getModuleName();
271 LoggerFactory::getInstance( 'authevents' )->info( "$module API attempt", [
272 'event' => $event,
273 'successful' => $result->status === AuthenticationResponse::PASS,
274 'status' => $result->message ? $result->message->getKey() : '-',
275 'accountType' => $accountType,
276 'module' => $module,
277 ] );
281 * Fetch the preserved CreateFromLoginAuthenticationRequest, if any
282 * @return CreateFromLoginAuthenticationRequest|null
284 public function getPreservedRequest() {
285 $ret = $this->module->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
286 return $ret instanceof CreateFromLoginAuthenticationRequest ? $ret : null;
290 * Format an array of AuthenticationRequests for return
291 * @param AuthenticationRequest[] $reqs
292 * @return array Will have a 'requests' key, and also 'fields' if $module's
293 * params include 'mergerequestfields'.
295 public function formatRequests( array $reqs ) {
296 $params = $this->module->extractRequestParams();
297 $mergeFields = !empty( $params['mergerequestfields'] );
299 $ret = [ 'requests' => [] ];
300 foreach ( $reqs as $req ) {
301 $describe = $req->describeCredentials();
302 $reqInfo = [
303 'id' => $req->getUniqueId(),
304 'metadata' => $req->getMetadata() + [ ApiResult::META_TYPE => 'assoc' ],
306 switch ( $req->required ) {
307 case AuthenticationRequest::OPTIONAL:
308 $reqInfo['required'] = 'optional';
309 break;
310 case AuthenticationRequest::REQUIRED:
311 $reqInfo['required'] = 'required';
312 break;
313 case AuthenticationRequest::PRIMARY_REQUIRED:
314 $reqInfo['required'] = 'primary-required';
315 break;
317 $this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
318 $this->formatMessage( $reqInfo, 'account', $describe['account'] );
319 if ( !$mergeFields ) {
320 $reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
322 $ret['requests'][] = $reqInfo;
325 if ( $mergeFields ) {
326 $fields = AuthenticationRequest::mergeFieldInfo( $reqs );
327 $ret['fields'] = $this->formatFields( $fields );
330 return $ret;
334 * Clean up a field array for output
335 * @param array $fields
336 * @phpcs:ignore Generic.Files.LineLength
337 * @phan-param array{type:string,options:array,value:string,label:Message,help:Message,optional:bool,sensitive:bool,skippable:bool} $fields
338 * @return array
340 private function formatFields( array $fields ) {
341 static $copy = [
342 'type' => true,
343 'value' => true,
346 $module = $this->module;
347 $retFields = [];
349 foreach ( $fields as $name => $field ) {
350 $ret = array_intersect_key( $field, $copy );
352 if ( isset( $field['options'] ) ) {
353 $ret['options'] = array_map( static function ( $msg ) use ( $module ) {
354 return $msg->setContext( $module )->plain();
355 }, $field['options'] );
356 ApiResult::setArrayType( $ret['options'], 'assoc' );
358 $this->formatMessage( $ret, 'label', $field['label'] );
359 $this->formatMessage( $ret, 'help', $field['help'] );
360 $ret['optional'] = !empty( $field['optional'] );
361 $ret['sensitive'] = !empty( $field['sensitive'] );
363 $retFields[$name] = $ret;
366 ApiResult::setArrayType( $retFields, 'assoc' );
368 return $retFields;
372 * Fetch the standard parameters this helper recognizes
373 * @param string $action AuthManager action
374 * @param string ...$wantedParams Parameters to use
375 * @return array
377 public static function getStandardParams( $action, ...$wantedParams ) {
378 $params = [
379 'requests' => [
380 ParamValidator::PARAM_TYPE => 'string',
381 ParamValidator::PARAM_ISMULTI => true,
382 ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-requests', $action ],
384 'request' => [
385 ParamValidator::PARAM_TYPE => 'string',
386 ParamValidator::PARAM_REQUIRED => true,
387 ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-request', $action ],
389 'messageformat' => [
390 ParamValidator::PARAM_DEFAULT => 'wikitext',
391 ParamValidator::PARAM_TYPE => [ 'html', 'wikitext', 'raw', 'none' ],
392 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-messageformat',
394 'mergerequestfields' => [
395 ParamValidator::PARAM_DEFAULT => false,
396 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-mergerequestfields',
398 'preservestate' => [
399 ParamValidator::PARAM_DEFAULT => false,
400 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-preservestate',
402 'returnurl' => [
403 ParamValidator::PARAM_TYPE => 'string',
404 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-returnurl',
406 'continue' => [
407 ParamValidator::PARAM_DEFAULT => false,
408 ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-continue',
412 $ret = [];
413 foreach ( $wantedParams as $name ) {
414 if ( isset( $params[$name] ) ) {
415 $ret[$name] = $params[$name];
418 return $ret;
422 /** @deprecated class alias since 1.43 */
423 class_alias( ApiAuthManagerHelper::class, 'ApiAuthManagerHelper' );