Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ActionModuleBasedHandler
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 8
182
0.00% covered (danger)
0.00%
0 / 1
 getUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setApiMain
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getApiMain
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 overrideActionModule
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 getActionModuleParameters
n/a
0 / 0
n/a
0 / 0
0
 mapActionModuleResult
n/a
0 / 0
n/a
0 / 0
0
 mapActionModuleResponse
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 throwHttpExceptionForActionModuleError
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 makeMessageValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use ApiBase;
6use ApiMain;
7use ApiMessage;
8use ApiUsageException;
9use IApiMessage;
10use MediaWiki\Context\RequestContext;
11use MediaWiki\Request\FauxRequest;
12use MediaWiki\Request\WebResponse;
13use MediaWiki\Rest\Handler;
14use MediaWiki\Rest\Handler\Helper\RestStatusTrait;
15use MediaWiki\Rest\HttpException;
16use MediaWiki\Rest\LocalizedHttpException;
17use MediaWiki\Rest\Response;
18use Wikimedia\Message\MessageValue;
19
20/**
21 * Base class for REST handlers that are implemented by mapping to an existing ApiModule.
22 *
23 * @stable to extend
24 */
25abstract class ActionModuleBasedHandler extends Handler {
26    use RestStatusTrait;
27
28    /**
29     * @var ApiMain|null
30     */
31    private $apiMain = null;
32
33    protected function getUser() {
34        return $this->getApiMain()->getUser();
35    }
36
37    /**
38     * Set main action API entry point for testing.
39     *
40     * @param ApiMain $apiMain
41     */
42    public function setApiMain( ApiMain $apiMain ) {
43        $this->apiMain = $apiMain;
44    }
45
46    /**
47     * @return ApiMain
48     */
49    public function getApiMain() {
50        if ( $this->apiMain ) {
51            return $this->apiMain;
52        }
53
54        $context = RequestContext::getMain();
55        $session = $context->getRequest()->getSession();
56
57        // NOTE: This being a MediaWiki\Request\FauxRequest instance triggers special case behavior
58        // in ApiMain, causing ApiMain::isInternalMode() to return true. Among other things,
59        // this causes ApiMain to throw errors rather than encode them in the result data.
60        $fauxRequest = new FauxRequest( [], true, $session );
61        $fauxRequest->setSessionId( $session->getSessionId() );
62
63        $fauxContext = new RequestContext();
64        $fauxContext->setRequest( $fauxRequest );
65        $fauxContext->setUser( $context->getUser() );
66        $fauxContext->setLanguage( $context->getLanguage() );
67
68        $this->apiMain = new ApiMain( $fauxContext, true );
69        return $this->apiMain;
70    }
71
72    /**
73     * Overrides an action API module. Used for testing.
74     *
75     * @param string $name
76     * @param string $group
77     * @param ApiBase $module
78     */
79    public function overrideActionModule( string $name, string $group, ApiBase $module ) {
80        $this->getApiMain()->getModuleManager()->addModule(
81            $name,
82            $group,
83            [
84                'class' => get_class( $module ),
85                'factory' => static function () use ( $module ) {
86                    return $module;
87                }
88            ]
89        );
90    }
91
92    /**
93     * Main execution method, implemented to delegate execution to ApiMain.
94     * Which action API module gets called is controlled by the parameter array returned
95     * by getActionModuleParameters(). The response from the action module is passed to
96     * mapActionModuleResult(), any ApiUsageException thrown will be converted to a
97     * HttpException by throwHttpExceptionForActionModuleError().
98     *
99     * @return mixed
100     */
101    public function execute() {
102        $apiMain = $this->getApiMain();
103
104        $params = $this->getActionModuleParameters();
105        $request = $apiMain->getRequest();
106
107        foreach ( $params as $key => $value ) {
108            $request->setVal( $key, $value );
109        }
110
111        try {
112            // NOTE: ApiMain detects this to be an internal call, so it will throw
113            // ApiUsageException rather than putting error messages into the result.
114            $apiMain->execute();
115        } catch ( ApiUsageException $ex ) {
116            // use a fake loop to throw the first error
117            foreach ( $ex->getStatusValue()->getErrorsByType( 'error' ) as $error ) {
118                $msg = ApiMessage::create( $error );
119                $this->throwHttpExceptionForActionModuleError( $msg, $ex->getCode() ?: 400 );
120            }
121
122            // This should never happen, since ApiUsageExceptions should always
123            // have errors in their Status object.
124            throw new HttpException(
125                'Unmapped action module error: ' . $ex->getMessage(),
126                $ex->getCode()
127            );
128        }
129
130        $actionModuleResult = $apiMain->getResult()->getResultData( null, [ 'Strip' => 'all' ] );
131
132        // construct result
133        $resultData = $this->mapActionModuleResult( $actionModuleResult );
134
135        $response = $this->getResponseFactory()->createFromReturnValue( $resultData );
136
137        $this->mapActionModuleResponse(
138            $apiMain->getRequest()->response(),
139            $actionModuleResult,
140            $response
141        );
142
143        return $response;
144    }
145
146    /**
147     * Maps a REST API request to an action API request.
148     * Implementations typically use information returned by $this->getValidatedBody()
149     * and $this->getValidatedParams() to construct the return value.
150     *
151     * The return value of this method controls which action module is called by execute().
152     *
153     * @return array Emulated request parameters to be passed to the ApiModule.
154     */
155    abstract protected function getActionModuleParameters();
156
157    /**
158     * Maps an action API result to a REST API result.
159     *
160     * @param array $data Data structure retrieved from the ApiResult returned by the ApiModule
161     *
162     * @return mixed Data structure to be converted to JSON and wrapped in a REST Response.
163     *         Will be processed by ResponseFactory::createFromReturnValue().
164     */
165    abstract protected function mapActionModuleResult( array $data );
166
167    /**
168     * Transfers relevant information, such as header values, from the WebResponse constructed
169     * by the action API call to a REST Response object.
170     *
171     * Subclasses may override this to provide special case handling for header fields.
172     * For mapping the response body, override mapActionModuleResult() instead.
173     *
174     * Subclasses overriding this method should call this method in the parent class,
175     * to preserve baseline behavior.
176     *
177     * @stable to override
178     *
179     * @param WebResponse $actionModuleResponse
180     * @param array $actionModuleResult
181     * @param Response $response
182     */
183    protected function mapActionModuleResponse(
184        WebResponse $actionModuleResponse,
185        array $actionModuleResult,
186        Response $response
187    ) {
188        // TODO: map status, headers, cookies, etc
189    }
190
191    /**
192     * Throws a HttpException for a given IApiMessage that represents an error.
193     * Never returns normally.
194     *
195     * Subclasses may override this to provide mappings for specific error codes,
196     * typically based on $msg->getApiCode(). Subclasses overriding this method must
197     * always either throw an exception, or call this method in the parent class,
198     * which then throws an exception.
199     *
200     * @stable to override
201     *
202     * @param IApiMessage $msg A message object representing an error in an action module,
203     *        typically from calling getStatusValue()->getErrorsByType( 'error' ) on
204     *        an ApiUsageException.
205     * @param int $statusCode The HTTP status indicated by the original exception
206     *
207     * @throws HttpException always.
208     */
209    protected function throwHttpExceptionForActionModuleError( IApiMessage $msg, $statusCode = 400 ) {
210        // override to supply mappings
211
212        throw new LocalizedHttpException(
213            $this->makeMessageValue( $msg ),
214            $statusCode,
215            // Include the original error code in the response.
216            // This makes it easier to track down the original cause of the error,
217            // and allows more specific mappings to be added to
218            // implementations of throwHttpExceptionForActionModuleError() provided by
219            // subclasses
220            [ 'actionModuleErrorCode' => $msg->getApiCode() ]
221        );
222    }
223
224    /**
225     * Constructs a MessageValue from an IApiMessage.
226     *
227     * @param IApiMessage $msg
228     *
229     * @return MessageValue
230     */
231    protected function makeMessageValue( IApiMessage $msg ) {
232        return $this->getMessageValueConverter()->convertMessage( $msg );
233    }
234
235}