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