1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
|
<?php
namespace MediaWiki\Rest\Handler;
use MediaWiki\Api\ApiBase;
use MediaWiki\Api\ApiMain;
use MediaWiki\Api\ApiMessage;
use MediaWiki\Api\ApiUsageException;
use MediaWiki\Api\IApiMessage;
use MediaWiki\Context\RequestContext;
use MediaWiki\Request\FauxRequest;
use MediaWiki\Request\WebResponse;
use MediaWiki\Rest\Handler;
use MediaWiki\Rest\Handler\Helper\RestStatusTrait;
use MediaWiki\Rest\HttpException;
use MediaWiki\Rest\LocalizedHttpException;
use MediaWiki\Rest\Response;
use Wikimedia\Message\MessageValue;
/**
* Base class for REST handlers that are implemented by mapping to an existing ApiModule.
*
* @stable to extend
*/
abstract class ActionModuleBasedHandler extends Handler {
use RestStatusTrait;
/**
* @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 MediaWiki\Request\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()->getMessages( 'error' ) as $msg ) {
$msg = ApiMessage::create( $msg );
$this->throwHttpExceptionForActionModuleError( $msg, $ex->getCode() ?: 400 );
}
// This should never happen, since ApiUsageExceptions should always
// have errors in their Status object.
throw new LocalizedHttpException( new MessageValue( "rest-unmapped-action-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()->getMessages( '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 ) {
return $this->getMessageValueConverter()->convertMessage( $msg );
}
}
|