Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
37.50% |
60 / 160 |
|
50.00% |
3 / 6 |
CRAP | |
0.00% |
0 / 1 |
WikiLambdaApiBase | |
37.50% |
60 / 160 |
|
50.00% |
3 / 6 |
107.13 | |
0.00% |
0 / 1 |
setUp | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
dieWithZError | |
38.89% |
7 / 18 |
|
0.00% |
0 / 1 |
2.91 | |||
returnWithZError | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
executeFunctionCall | |
35.16% |
45 / 128 |
|
0.00% |
0 / 1 |
59.08 | |||
setLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\WikiLambda\ActionAPI; |
4 | |
5 | use ApiBase; |
6 | use ApiUsageException; |
7 | use FormatJson; |
8 | use GuzzleHttp\Client; |
9 | use GuzzleHttp\Exception\ClientException; |
10 | use GuzzleHttp\Exception\ConnectException; |
11 | use GuzzleHttp\Exception\ServerException; |
12 | use MediaWiki\Extension\WikiLambda\OrchestratorRequest; |
13 | use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry; |
14 | use MediaWiki\Extension\WikiLambda\ZErrorException; |
15 | use MediaWiki\Extension\WikiLambda\ZErrorFactory; |
16 | use MediaWiki\Extension\WikiLambda\ZObjectFactory; |
17 | use MediaWiki\Extension\WikiLambda\ZObjects\ZError; |
18 | use MediaWiki\Extension\WikiLambda\ZObjects\ZFunctionCall; |
19 | use MediaWiki\Extension\WikiLambda\ZObjects\ZResponseEnvelope; |
20 | use MediaWiki\Logger\LoggerFactory; |
21 | use MediaWiki\MediaWikiServices; |
22 | use MediaWiki\PoolCounter\PoolCounterWorkViaCallback; |
23 | use MediaWiki\Status\Status; |
24 | use Psr\Log\LoggerAwareInterface; |
25 | use Psr\Log\LoggerInterface; |
26 | use Wikimedia\RequestTimeout\RequestTimeoutException; |
27 | |
28 | /** |
29 | * WikiLambda Base API util |
30 | * |
31 | * This abstract class extends the Wikimedia's ApiBase class |
32 | * and provides specific additional methods. |
33 | * |
34 | * @stable to extend |
35 | * |
36 | * @ingroup API |
37 | * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt |
38 | * @license MIT |
39 | */ |
40 | abstract class WikiLambdaApiBase extends ApiBase implements LoggerAwareInterface { |
41 | |
42 | protected OrchestratorRequest $orchestrator; |
43 | protected string $orchestratorHost; |
44 | protected LoggerInterface $logger; |
45 | |
46 | protected function setUp() { |
47 | $this->setLogger( LoggerFactory::getInstance( 'WikiLambda' ) ); |
48 | |
49 | // TODO (T330033): Consider injecting this service rather than just fetching from main |
50 | $services = MediaWikiServices::getInstance(); |
51 | |
52 | $config = $services->getConfigFactory()->makeConfig( 'WikiLambda' ); |
53 | $this->orchestratorHost = $config->get( 'WikiLambdaOrchestratorLocation' ); |
54 | $client = new Client( [ "base_uri" => $this->orchestratorHost ] ); |
55 | $this->orchestrator = new OrchestratorRequest( $client ); |
56 | } |
57 | |
58 | /** |
59 | * @param ZError $zerror The ZError object to return to the user |
60 | * @param int $code HTTP error code, defaulting to 400/Bad Request |
61 | */ |
62 | public function dieWithZError( $zerror, $code = 400 ) { |
63 | try { |
64 | $errorData = $zerror->getErrorData(); |
65 | } catch ( ZErrorException $e ) { |
66 | // Generating the human-readable error data itself threw. Oh dear. |
67 | $this->getLogger()->warning( |
68 | __METHOD__ . ' called but an error was thrown when trying to report an error', |
69 | [ |
70 | 'zerror' => $zerror->getSerialized(), |
71 | 'error' => $e, |
72 | ] |
73 | ); |
74 | |
75 | $errorData = [ |
76 | 'zerror' => $zerror->getSerialized() |
77 | ]; |
78 | } |
79 | |
80 | parent::dieWithError( |
81 | [ 'wikilambda-zerror', $zerror->getZErrorType() ], |
82 | null, |
83 | $errorData, |
84 | $code |
85 | ); |
86 | } |
87 | |
88 | /** |
89 | * @param string $errorMessage |
90 | * @param string $zFunctionCallString |
91 | * @return ZResponseEnvelope |
92 | */ |
93 | private function returnWithZError( $errorMessage, $zFunctionCallString ): ZResponseEnvelope { |
94 | $zErrorObject = ZErrorFactory::wrapMessageInZError( |
95 | $errorMessage, |
96 | $zFunctionCallString |
97 | ); |
98 | $zResponseMap = ZResponseEnvelope::wrapErrorInResponseMap( $zErrorObject ); |
99 | return new ZResponseEnvelope( null, $zResponseMap ); |
100 | } |
101 | |
102 | /** |
103 | * @param ZFunctionCall|\stdClass $zObject |
104 | * @param bool $validate |
105 | * @return ZResponseEnvelope |
106 | */ |
107 | protected function executeFunctionCall( $zObject, $validate ) { |
108 | $zObjectAsStdClass = ( $zObject instanceof ZFunctionCall ) ? $zObject->getSerialized() : $zObject; |
109 | $zObjectAsString = json_encode( $zObjectAsStdClass ); |
110 | |
111 | if ( $zObjectAsStdClass->Z1K1 !== 'Z7' && $zObjectAsStdClass->Z1K1->Z9K1 !== 'Z7' ) { |
112 | $this->dieWithError( [ "apierror-wikilambda_function_call-not-a-function" ], null, null, 400 ); |
113 | } |
114 | |
115 | $this->getLogger()->debug( |
116 | __METHOD__ . ' called', |
117 | [ |
118 | 'zObject' => $zObjectAsString, |
119 | 'validate' => $validate, |
120 | ] |
121 | ); |
122 | |
123 | // Unlike the Special pages, we don't have a helpful userCanExecute() method |
124 | if ( !$this->getContext()->getAuthority()->isAllowed( 'wikilambda-execute' ) ) { |
125 | $zError = ZErrorFactory::createZErrorInstance( ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN, [] ); |
126 | $this->dieWithZError( $zError, 403 ); |
127 | } |
128 | |
129 | $queryArguments = [ |
130 | 'zobject' => $zObjectAsStdClass, |
131 | 'doValidate' => $validate |
132 | ]; |
133 | try { |
134 | $work = new PoolCounterWorkViaCallback( |
135 | 'WikiLambdaFunctionCall', |
136 | $this->getUser()->getName(), |
137 | [ |
138 | 'doWork' => function () use ( $queryArguments ) { |
139 | return $this->orchestrator->orchestrate( $queryArguments ); |
140 | }, |
141 | 'error' => function ( Status $status ) { |
142 | $this->dieWithError( |
143 | [ "apierror-wikilambda_function_call-concurrency-limit" ], |
144 | null, null, 429 |
145 | ); |
146 | } |
147 | ] |
148 | ); |
149 | $response = $work->execute(); |
150 | |
151 | $this->getLogger()->debug( |
152 | __METHOD__ . ' executed successfully', |
153 | [ |
154 | 'zObject' => $zObjectAsString, |
155 | 'validate' => $validate, |
156 | 'response' => $response, |
157 | ] |
158 | ); |
159 | |
160 | $responseContents = FormatJson::decode( $response ); |
161 | |
162 | try { |
163 | $responseObject = ZObjectFactory::create( $responseContents ); |
164 | } catch ( ZErrorException $e ) { |
165 | $this->dieWithError( |
166 | [ |
167 | 'apierror-wikilambda_function_call-response-malformed', |
168 | $e->getZErrorMessage() |
169 | ], |
170 | null, null, 500 |
171 | ); |
172 | } |
173 | '@phan-var \MediaWiki\Extension\WikiLambda\ZObjects\ZResponseEnvelope $responseObject'; |
174 | return $responseObject; |
175 | } catch ( ConnectException $exception ) { |
176 | $this->dieWithError( |
177 | [ "apierror-wikilambda_function_call-not-connected", $this->orchestratorHost ], |
178 | null, null, 503 |
179 | ); |
180 | } catch ( ClientException | ServerException $exception ) { |
181 | if ( $exception->getResponse()->getStatusCode() === 404 ) { |
182 | $this->dieWithError( |
183 | [ "apierror-wikilambda_function_call-not-connected", $this->orchestratorHost ], |
184 | null, null, 503 |
185 | ); |
186 | } |
187 | |
188 | $this->getLogger()->warning( |
189 | __METHOD__ . ' failed to execute with a ClientException/ServerException: {exception}', |
190 | [ |
191 | 'zObject' => $zObjectAsString, |
192 | 'validate' => $validate, |
193 | 'exception' => $exception, |
194 | ] |
195 | ); |
196 | |
197 | return $this->returnWithZError( |
198 | $exception->getResponse()->getReasonPhrase(), |
199 | $zObjectAsString |
200 | ); |
201 | } catch ( RequestTimeoutException $exception ) { |
202 | $this->getLogger()->warning( |
203 | __METHOD__ . ' failed to execute with a RequestTimeoutException: {exception}', |
204 | [ |
205 | 'zObject' => $zObjectAsString, |
206 | 'validate' => $validate, |
207 | 'exception' => $exception, |
208 | ] |
209 | ); |
210 | |
211 | return $this->returnWithZError( |
212 | $exception->getMessage(), |
213 | $zObjectAsString |
214 | ); |
215 | } catch ( ApiUsageException $exception ) { |
216 | // This is almost certainly a user-limit-error, and not worth worrying in the middleware |
217 | // about, so only log as debug() not warning() |
218 | $this->getLogger()->debug( |
219 | __METHOD__ . ' failed to execute with a ApiUsageException: {exception}', |
220 | [ |
221 | 'zObject' => $zObjectAsString, |
222 | 'validate' => $validate, |
223 | 'exception' => $exception, |
224 | ] |
225 | ); |
226 | |
227 | return $this->returnWithZError( |
228 | $exception->getMessage(), |
229 | $zObjectAsString |
230 | ); |
231 | } catch ( ZErrorException $exception ) { |
232 | // This is almost certainly a user-error, and not worth worrying in the middleware |
233 | // about, so only log as debug() not warning() |
234 | $this->getLogger()->debug( |
235 | __METHOD__ . ' failed to execute with a ZErrorException: {exception}', |
236 | [ |
237 | 'zObject' => $zObjectAsString, |
238 | 'validate' => $validate, |
239 | 'exception' => $exception, |
240 | ] |
241 | ); |
242 | |
243 | return $this->returnWithZError( |
244 | $exception->getZErrorMessage(), |
245 | $zObjectAsString |
246 | ); |
247 | } catch ( \Exception $exception ) { |
248 | |
249 | $this->getLogger()->warning( |
250 | __METHOD__ . ' failed to execute with a general Exception: {exception}', |
251 | [ |
252 | 'zObject' => $zObjectAsString, |
253 | 'validate' => $validate, |
254 | 'exception' => $exception, |
255 | ] |
256 | ); |
257 | |
258 | return $this->returnWithZError( |
259 | $exception->getMessage(), |
260 | $zObjectAsString |
261 | ); |
262 | } |
263 | } |
264 | |
265 | /** @inheritDoc */ |
266 | public function setLogger( LoggerInterface $logger ) { |
267 | $this->logger = $logger; |
268 | } |
269 | |
270 | /** @inheritDoc */ |
271 | public function getLogger(): LoggerInterface { |
272 | return $this->logger; |
273 | } |
274 | } |