wiki.techinc.nl/includes/Rest/Handler/ActionModuleBasedHandler.php
Bill Pirkle 7295100773 Allow REST API handlers to require csrf-safe session providers
Bug: T305043
Depends-On: Ic7c1b19b86e8a151e2d42aaec00ef0e89db77f08
Change-Id: Ic6bd48b400ecd839ef99b518ef955781470cd05c
2022-05-20 16:52:54 +00:00

269 lines
7.7 KiB
PHP

<?php
namespace MediaWiki\Rest\Handler;
use ApiBase;
use ApiMain;
use ApiMessage;
use ApiUsageException;
use FauxRequest;
use IApiMessage;
use MediaWiki\Rest\Handler;
use MediaWiki\Rest\HttpException;
use MediaWiki\Rest\LocalizedHttpException;
use MediaWiki\Rest\Response;
use RequestContext;
use WebResponse;
use Wikimedia\Message\ListParam;
use Wikimedia\Message\MessageParam;
use Wikimedia\Message\MessageValue;
use Wikimedia\Message\ParamType;
use Wikimedia\Message\ScalarParam;
/**
* Base class for REST handlers that are implemented by mapping to an existing ApiModule.
*
* @stable to extend
*/
abstract class ActionModuleBasedHandler extends Handler {
/**
* @var ApiMain|null
*/
private $apiMain = null;
protected function getUser() {
return $this->getApiMain()->getUser();
}
/**
* Set main action API entry point for testing.
*
* @param ApiMain $apiMain
*/
public function setApiMain( ApiMain $apiMain ) {
$this->apiMain = $apiMain;
}
/**
* @return ApiMain
*/
public function getApiMain() {
if ( $this->apiMain ) {
return $this->apiMain;
}
$context = RequestContext::getMain();
$session = $context->getRequest()->getSession();
// NOTE: This being a FauxRequest instance triggers special case behavior
// in ApiMain, causing ApiMain::isInternalMode() to return true. Among other things,
// this causes ApiMain to throw errors rather than encode them in the result data.
$fauxRequest = new FauxRequest( [], true, $session );
$fauxRequest->setSessionId( $session->getSessionId() );
$fauxContext = new RequestContext();
$fauxContext->setRequest( $fauxRequest );
$fauxContext->setUser( $context->getUser() );
$fauxContext->setLanguage( $context->getLanguage() );
$this->apiMain = new ApiMain( $fauxContext, true );
return $this->apiMain;
}
/**
* Overrides an action API module. Used for testing.
*
* @param string $name
* @param string $group
* @param ApiBase $module
*/
public function overrideActionModule( string $name, string $group, ApiBase $module ) {
$this->getApiMain()->getModuleManager()->addModule(
$name,
$group,
[
'class' => get_class( $module ),
'factory' => static function () use ( $module ) {
return $module;
}
]
);
}
/**
* Main execution method, implemented to delegate execution to ApiMain.
* Which action API module gets called is controlled by the parameter array returned
* by getActionModuleParameters(). The response from the action module is passed to
* mapActionModuleResult(), any ApiUsageException thrown will be converted to a
* HttpException by throwHttpExceptionForActionModuleError().
*
* @return mixed
*/
public function execute() {
$apiMain = $this->getApiMain();
$params = $this->getActionModuleParameters();
$request = $apiMain->getRequest();
foreach ( $params as $key => $value ) {
$request->setVal( $key, $value );
}
try {
// NOTE: ApiMain detects this to be an internal call, so it will throw
// ApiUsageException rather than putting error messages into the result.
$apiMain->execute();
} catch ( ApiUsageException $ex ) {
// use a fake loop to throw the first error
foreach ( $ex->getStatusValue()->getErrorsByType( 'error' ) as $error ) {
$msg = ApiMessage::create( $error );
$this->throwHttpExceptionForActionModuleError( $msg, $ex->getCode() ?: 400 );
}
// This should never happen, since ApiUsageExceptions should always
// have errors in their Status object.
throw new HttpException(
'Unmapped action module error: ' . $ex->getMessage(),
$ex->getCode()
);
}
$actionModuleResult = $apiMain->getResult()->getResultData( null, [ 'Strip' => 'all' ] );
// construct result
$resultData = $this->mapActionModuleResult( $actionModuleResult );
$response = $this->getResponseFactory()->createFromReturnValue( $resultData );
$this->mapActionModuleResponse(
$apiMain->getRequest()->response(),
$actionModuleResult,
$response
);
return $response;
}
/**
* Maps a REST API request to an action API request.
* Implementations typically use information returned by $this->getValidatedBody()
* and $this->getValidatedParams() to construct the return value.
*
* The return value of this method controls which action module is called by execute().
*
* @return array Emulated request parameters to be passed to the ApiModule.
*/
abstract protected function getActionModuleParameters();
/**
* Maps an action API result to a REST API result.
*
* @param array $data Data structure retrieved from the ApiResult returned by the ApiModule
*
* @return mixed Data structure to be converted to JSON and wrapped in a REST Response.
* Will be processed by ResponseFactory::createFromReturnValue().
*/
abstract protected function mapActionModuleResult( array $data );
/**
* Transfers relevant information, such as header values, from the WebResponse constructed
* by the action API call to a REST Response object.
*
* Subclasses may override this to provide special case handling for header fields.
* For mapping the response body, override mapActionModuleResult() instead.
*
* Subclasses overriding this method should call this method in the parent class,
* to preserve baseline behavior.
*
* @stable to override
*
* @param WebResponse $actionModuleResponse
* @param array $actionModuleResult
* @param Response $response
*/
protected function mapActionModuleResponse(
WebResponse $actionModuleResponse,
array $actionModuleResult,
Response $response
) {
// TODO: map status, headers, cookies, etc
}
/**
* Throws a HttpException for a given IApiMessage that represents an error.
* Never returns normally.
*
* Subclasses may override this to provide mappings for specific error codes,
* typically based on $msg->getApiCode(). Subclasses overriding this method must
* always either throw an exception, or call this method in the parent class,
* which then throws an exception.
*
* @stable to override
*
* @param IApiMessage $msg A message object representing an error in an action module,
* typically from calling getStatusValue()->getErrorsByType( 'error' ) on
* an ApiUsageException.
* @param int $statusCode The HTTP status indicated by the original exception
*
* @throws HttpException always.
*/
protected function throwHttpExceptionForActionModuleError( IApiMessage $msg, $statusCode = 400 ) {
// override to supply mappings
throw new LocalizedHttpException(
$this->makeMessageValue( $msg ),
$statusCode,
// Include the original error code in the response.
// This makes it easier to track down the original cause of the error,
// and allows more specific mappings to be added to
// implementations of throwHttpExceptionForActionModuleError() provided by
// subclasses
[ 'actionModuleErrorCode' => $msg->getApiCode() ]
);
}
/**
* Constructs a MessageValue from an IApiMessage.
*
* @param IApiMessage $msg
*
* @return MessageValue
*/
protected function makeMessageValue( IApiMessage $msg ) {
$params = [];
// TODO: find a better home for the parameter mapping logic
foreach ( $msg->getParams() as $p ) {
$params[] = $this->makeMessageParam( $p );
}
return new MessageValue( $msg->getKey(), $params );
}
/**
* @param mixed $param
*
* @return MessageParam
*/
private function makeMessageParam( $param ) {
if ( is_array( $param ) ) {
foreach ( $param as $type => $value ) {
if ( $type === 'list' ) {
$paramList = [];
foreach ( $value as $v ) {
$paramList[] = $this->makeMessageParam( $v );
}
return new ListParam( ParamType::TEXT, $paramList );
} else {
return new ScalarParam( $type, $value );
}
}
} else {
return new ScalarParam( ParamType::TEXT, $param );
}
}
}