aboutsummaryrefslogtreecommitdiffstats
path: root/includes/Rest/Handler/ActionModuleBasedHandler.php
blob: ed5d798944a190ac09e42b673feada948cfcd4db (plain) (blame)
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
<?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(
			MessageValue::newFromSpecifier( $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() ]
		);
	}

}