includes/api/ApiAuthManagerHelper.php
<?php
/**
* Copyright © 2016 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @since 1.27
*/
namespace MediaWiki\Api;
use MediaWiki\Auth\AuthenticationRequest;
use MediaWiki\Auth\AuthenticationResponse;
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\CreateFromLoginAuthenticationRequest;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Message\Message;
use MediaWiki\Parser\Parser;
use UnexpectedValueException;
use Wikimedia\ParamValidator\ParamValidator;
/**
* Helper class for AuthManager-using API modules. Intended for use via
* composition.
*
* @ingroup API
*/
class ApiAuthManagerHelper {
/** @var ApiBase API module, for context and parameters */
private $module;
/** @var string Message output format */
private $messageFormat;
private AuthManager $authManager;
/**
* @param ApiBase $module API module, for context and parameters
* @param AuthManager|null $authManager
*/
public function __construct( ApiBase $module, AuthManager $authManager = null ) {
$this->module = $module;
$params = $module->extractRequestParams();
$this->messageFormat = $params['messageformat'] ?? 'wikitext';
$this->authManager = $authManager ?: MediaWikiServices::getInstance()->getAuthManager();
}
/**
* Static version of the constructor, for chaining
* @param ApiBase $module API module, for context and parameters
* @param AuthManager|null $authManager
* @return ApiAuthManagerHelper
*/
public static function newForModule( ApiBase $module, AuthManager $authManager = null ) {
return new self( $module, $authManager );
}
/**
* Format a message for output
* @param array &$res Result array
* @param string $key Result key
* @param Message $message
*/
private function formatMessage( array &$res, $key, Message $message ) {
switch ( $this->messageFormat ) {
case 'none':
break;
case 'wikitext':
$res[$key] = $message->setContext( $this->module )->text();
break;
case 'html':
$res[$key] = $message->setContext( $this->module )->parseAsBlock();
$res[$key] = Parser::stripOuterParagraph( $res[$key] );
break;
case 'raw':
$params = $message->getParams();
$res[$key] = [
'key' => $message->getKey(),
'params' => $params,
];
ApiResult::setIndexedTagName( $params, 'param' );
break;
}
}
/**
* Call $manager->securitySensitiveOperationStatus()
* @param string $operation Operation being checked.
* @throws ApiUsageException
*/
public function securitySensitiveOperation( $operation ) {
$status = $this->authManager->securitySensitiveOperationStatus( $operation );
switch ( $status ) {
case AuthManager::SEC_OK:
return;
case AuthManager::SEC_REAUTH:
$this->module->dieWithError( 'apierror-reauthenticate' );
// dieWithError prevents continuation
case AuthManager::SEC_FAIL:
$this->module->dieWithError( 'apierror-cannotreauthenticate' );
// dieWithError prevents continuation
default:
throw new UnexpectedValueException( "Unknown status \"$status\"" );
}
}
/**
* Filter out authentication requests by class name
* @param AuthenticationRequest[] $reqs Requests to filter
* @param string[] $remove Class names to remove
* @return AuthenticationRequest[]
*/
public static function blacklistAuthenticationRequests( array $reqs, array $remove ) {
if ( $remove ) {
$remove = array_fill_keys( $remove, true );
$reqs = array_filter( $reqs, static function ( $req ) use ( $remove ) {
return !isset( $remove[get_class( $req )] );
} );
}
return $reqs;
}
/**
* Fetch and load the AuthenticationRequests for an action
* @param string $action One of the AuthManager::ACTION_* constants
* @return AuthenticationRequest[]
*/
public function loadAuthenticationRequests( $action ) {
$params = $this->module->extractRequestParams();
$reqs = $this->authManager->getAuthenticationRequests( $action, $this->module->getUser() );
// Filter requests, if requested to do so
$wantedRequests = null;
if ( isset( $params['requests'] ) ) {
$wantedRequests = array_fill_keys( $params['requests'], true );
} elseif ( isset( $params['request'] ) ) {
$wantedRequests = [ $params['request'] => true ];
}
if ( $wantedRequests !== null ) {
$reqs = array_filter(
$reqs,
static function ( AuthenticationRequest $req ) use ( $wantedRequests ) {
return isset( $wantedRequests[$req->getUniqueId()] );
}
);
}
// Collect the fields for all the requests
$fields = [];
$sensitive = [];
foreach ( $reqs as $req ) {
$info = (array)$req->getFieldInfo();
$fields += $info;
$sensitive += array_filter( $info, static function ( $opts ) {
return !empty( $opts['sensitive'] );
} );
}
// Extract the request data for the fields and mark those request
// parameters as used
$data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
$this->module->getMain()->markParamsUsed( array_keys( $data ) );
if ( $sensitive ) {
$this->module->getMain()->markParamsSensitive( array_keys( $sensitive ) );
$this->module->requirePostedParameters( array_keys( $sensitive ), 'noprefix' );
}
return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
}
/**
* Format an AuthenticationResponse for return
* @param AuthenticationResponse $res
* @return array
*/
public function formatAuthenticationResponse( AuthenticationResponse $res ) {
$ret = [
'status' => $res->status,
];
if ( $res->status === AuthenticationResponse::PASS && $res->username !== null ) {
$ret['username'] = $res->username;
}
if ( $res->status === AuthenticationResponse::REDIRECT ) {
$ret['redirecttarget'] = $res->redirectTarget;
if ( $res->redirectApiData !== null ) {
$ret['redirectdata'] = $res->redirectApiData;
}
}
if ( $res->status === AuthenticationResponse::REDIRECT ||
$res->status === AuthenticationResponse::UI ||
$res->status === AuthenticationResponse::RESTART
) {
$ret += $this->formatRequests( $res->neededRequests );
}
if ( $res->status === AuthenticationResponse::FAIL ||
$res->status === AuthenticationResponse::UI ||
$res->status === AuthenticationResponse::RESTART
) {
$this->formatMessage( $ret, 'message', $res->message );
$ret['messagecode'] = ApiMessage::create( $res->message )->getApiCode();
}
if ( $res->status === AuthenticationResponse::FAIL ||
$res->status === AuthenticationResponse::RESTART
) {
$this->module->getRequest()->getSession()->set(
'ApiAuthManagerHelper::createRequest',
$res->createRequest
);
$ret['canpreservestate'] = $res->createRequest !== null;
} else {
$this->module->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
}
return $ret;
}
/**
* Logs successful or failed authentication.
* @param string $event Event type (e.g. 'accountcreation')
* @param AuthenticationResponse $result Response or error message
*/
public function logAuthenticationResult( $event, AuthenticationResponse $result ) {
if ( !in_array( $result->status, [ AuthenticationResponse::PASS, AuthenticationResponse::FAIL ] ) ) {
return;
}
$module = $this->module->getModuleName();
LoggerFactory::getInstance( 'authevents' )->info( "$module API attempt", [
'event' => $event,
'successful' => $result->status === AuthenticationResponse::PASS,
'status' => $result->message ? $result->message->getKey() : '-',
'module' => $module,
] );
}
/**
* Fetch the preserved CreateFromLoginAuthenticationRequest, if any
* @return CreateFromLoginAuthenticationRequest|null
*/
public function getPreservedRequest() {
$ret = $this->module->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
return $ret instanceof CreateFromLoginAuthenticationRequest ? $ret : null;
}
/**
* Format an array of AuthenticationRequests for return
* @param AuthenticationRequest[] $reqs
* @return array Will have a 'requests' key, and also 'fields' if $module's
* params include 'mergerequestfields'.
*/
public function formatRequests( array $reqs ) {
$params = $this->module->extractRequestParams();
$mergeFields = !empty( $params['mergerequestfields'] );
$ret = [ 'requests' => [] ];
foreach ( $reqs as $req ) {
$describe = $req->describeCredentials();
$reqInfo = [
'id' => $req->getUniqueId(),
'metadata' => $req->getMetadata() + [ ApiResult::META_TYPE => 'assoc' ],
];
switch ( $req->required ) {
case AuthenticationRequest::OPTIONAL:
$reqInfo['required'] = 'optional';
break;
case AuthenticationRequest::REQUIRED:
$reqInfo['required'] = 'required';
break;
case AuthenticationRequest::PRIMARY_REQUIRED:
$reqInfo['required'] = 'primary-required';
break;
}
$this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
$this->formatMessage( $reqInfo, 'account', $describe['account'] );
if ( !$mergeFields ) {
$reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
}
$ret['requests'][] = $reqInfo;
}
if ( $mergeFields ) {
$fields = AuthenticationRequest::mergeFieldInfo( $reqs );
$ret['fields'] = $this->formatFields( $fields );
}
return $ret;
}
/**
* Clean up a field array for output
* @param array $fields
* @phpcs:ignore Generic.Files.LineLength
* @phan-param array{type:string,options:array,value:string,label:Message,help:Message,optional:bool,sensitive:bool,skippable:bool} $fields
* @return array
*/
private function formatFields( array $fields ) {
static $copy = [
'type' => true,
'value' => true,
];
$module = $this->module;
$retFields = [];
foreach ( $fields as $name => $field ) {
$ret = array_intersect_key( $field, $copy );
if ( isset( $field['options'] ) ) {
$ret['options'] = array_map( static function ( $msg ) use ( $module ) {
return $msg->setContext( $module )->plain();
}, $field['options'] );
ApiResult::setArrayType( $ret['options'], 'assoc' );
}
$this->formatMessage( $ret, 'label', $field['label'] );
$this->formatMessage( $ret, 'help', $field['help'] );
$ret['optional'] = !empty( $field['optional'] );
$ret['sensitive'] = !empty( $field['sensitive'] );
$retFields[$name] = $ret;
}
ApiResult::setArrayType( $retFields, 'assoc' );
return $retFields;
}
/**
* Fetch the standard parameters this helper recognizes
* @param string $action AuthManager action
* @param string ...$wantedParams Parameters to use
* @return array
*/
public static function getStandardParams( $action, ...$wantedParams ) {
$params = [
'requests' => [
ParamValidator::PARAM_TYPE => 'string',
ParamValidator::PARAM_ISMULTI => true,
ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-requests', $action ],
],
'request' => [
ParamValidator::PARAM_TYPE => 'string',
ParamValidator::PARAM_REQUIRED => true,
ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-request', $action ],
],
'messageformat' => [
ParamValidator::PARAM_DEFAULT => 'wikitext',
ParamValidator::PARAM_TYPE => [ 'html', 'wikitext', 'raw', 'none' ],
ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-messageformat',
],
'mergerequestfields' => [
ParamValidator::PARAM_DEFAULT => false,
ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-mergerequestfields',
],
'preservestate' => [
ParamValidator::PARAM_DEFAULT => false,
ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-preservestate',
],
'returnurl' => [
ParamValidator::PARAM_TYPE => 'string',
ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-returnurl',
],
'continue' => [
ParamValidator::PARAM_DEFAULT => false,
ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-continue',
],
];
$ret = [];
foreach ( $wantedParams as $name ) {
if ( isset( $params[$name] ) ) {
$ret[$name] = $params[$name];
}
}
return $ret;
}
}
/** @deprecated class alias since 1.43 */
class_alias( ApiAuthManagerHelper::class, 'ApiAuthManagerHelper' );