Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
35.29% |
36 / 102 |
|
33.33% |
2 / 6 |
CRAP | |
0.00% |
0 / 1 |
ApiFunctionCall | |
35.29% |
36 / 102 |
|
33.33% |
2 / 6 |
256.84 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
executeGenerator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run | |
63.27% |
31 / 49 |
|
0.00% |
0 / 1 |
19.14 | |||
getAllowedParams | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
readTestFileAsArray | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
createUserDefinedValidationExample | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
createCurryExample | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
createExample | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getExamplesMessages | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
makeRequest | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
56 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * WikiLambda function call API |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt |
8 | * @license MIT |
9 | */ |
10 | |
11 | namespace MediaWiki\Extension\WikiLambda\ActionAPI; |
12 | |
13 | use ApiMain; |
14 | use ApiPageSet; |
15 | use ApiUsageException; |
16 | use GuzzleHttp\Exception\ClientException; |
17 | use GuzzleHttp\Exception\ConnectException; |
18 | use GuzzleHttp\Exception\ServerException; |
19 | use MediaWiki\Context\DerivativeContext; |
20 | use MediaWiki\Context\RequestContext; |
21 | use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry; |
22 | use MediaWiki\Extension\WikiLambda\ZErrorException; |
23 | use MediaWiki\Extension\WikiLambda\ZErrorFactory; |
24 | use MediaWiki\Extension\WikiLambda\ZObjectFactory; |
25 | use MediaWiki\Extension\WikiLambda\ZObjects\ZError; |
26 | use MediaWiki\Extension\WikiLambda\ZObjects\ZQuote; |
27 | use MediaWiki\Extension\WikiLambda\ZObjects\ZResponseEnvelope; |
28 | use MediaWiki\Extension\WikiLambda\ZObjectUtils; |
29 | use MediaWiki\PoolCounter\PoolCounterWorkViaCallback; |
30 | use MediaWiki\Request\FauxRequest; |
31 | use MediaWiki\Status\Status; |
32 | use Wikimedia\ParamValidator\ParamValidator; |
33 | |
34 | class ApiFunctionCall extends WikiLambdaApiBase { |
35 | |
36 | /** |
37 | * @inheritDoc |
38 | */ |
39 | public function __construct( $query, $moduleName ) { |
40 | parent::__construct( $query, $moduleName, 'wikilambda_function_call_' ); |
41 | |
42 | $this->setUp(); |
43 | } |
44 | |
45 | /** |
46 | * @inheritDoc |
47 | */ |
48 | public function execute() { |
49 | // (T362271) Emit appropriate cache headers for a 24 hour TTL |
50 | // NOTE (T362273): MediaWiki out-guesses us and assumes we don't know what we're doing; to fix so it works |
51 | $this->getMain()->setCacheMode( 'public' ); |
52 | $this->getMain()->setCacheMaxAge( 60 * 60 * 24 ); |
53 | |
54 | $this->run(); |
55 | } |
56 | |
57 | /** |
58 | * @inheritDoc |
59 | */ |
60 | public function executeGenerator( $resultPageSet ) { |
61 | $this->run( $resultPageSet ); |
62 | } |
63 | |
64 | /** |
65 | * TODO (T338251): Use WikiLambdaApiBase::executeFunctionCall() rather than rolling our own. |
66 | * |
67 | * @param ApiPageSet|null $resultPageSet |
68 | */ |
69 | private function run( $resultPageSet = null ) { |
70 | // Unlike the Special pages, we don't have a helpful userCanExecute() method |
71 | $userAuthority = $this->getContext()->getAuthority(); |
72 | if ( !$userAuthority->isAllowed( 'wikilambda-execute' ) ) { |
73 | $zError = ZErrorFactory::createZErrorInstance( ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN, [] ); |
74 | $this->dieWithZError( $zError, 403 ); |
75 | } |
76 | |
77 | $params = $this->extractRequestParams(); |
78 | $pageResult = $this->getResult(); |
79 | $stringOfAZ = $params[ 'zobject' ]; |
80 | $zObjectAsStdClass = json_decode( $stringOfAZ ); |
81 | $jsonQuery = [ |
82 | 'zobject' => $zObjectAsStdClass, |
83 | 'doValidate' => true |
84 | ]; |
85 | |
86 | // Arbitrary implementation calls need more than wikilambda-execute; |
87 | // require wikilambda-execute-unsaved-code, so that it can be independently |
88 | // activated/deactivated (to run an arbitrary implementation, you have to |
89 | // pass a custom function with the raw implementation rather than a ZID string.) |
90 | $isUnsavedCode = false; |
91 | if ( |
92 | property_exists( $zObjectAsStdClass, 'Z7K1' ) && |
93 | is_object( $zObjectAsStdClass->Z7K1 ) && |
94 | property_exists( $zObjectAsStdClass->Z7K1, 'Z8K4' ) && |
95 | count( $zObjectAsStdClass->Z7K1->Z8K4 ) > 1 |
96 | ) { |
97 | $implementation = $zObjectAsStdClass->Z7K1->Z8K4[ 1 ]; |
98 | if ( is_object( $implementation ) && property_exists( $implementation, 'Z14K1' ) ) { |
99 | $isUnsavedCode = true; |
100 | } |
101 | } |
102 | if ( $isUnsavedCode && !$userAuthority->isAllowed( 'wikilambda-execute-unsaved-code' ) ) { |
103 | $zError = ZErrorFactory::createZErrorInstance( ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN, [] ); |
104 | $this->dieWithZError( $zError, 403 ); |
105 | } |
106 | |
107 | $work = new PoolCounterWorkViaCallback( 'WikiLambdaFunctionCall', $this->getUser()->getName(), [ |
108 | 'doWork' => function () use ( $jsonQuery ) { |
109 | return $this->orchestrator->orchestrate( $jsonQuery ); |
110 | }, |
111 | 'error' => function ( Status $status ) { |
112 | $this->dieWithError( [ "apierror-wikilambda_function_call-concurrency-limit" ], null, null, 429 ); |
113 | } |
114 | ] ); |
115 | |
116 | $result = [ 'success' => false ]; |
117 | try { |
118 | $response = $work->execute(); |
119 | $result['data'] = $response; |
120 | $result['success'] = true; |
121 | } catch ( ConnectException $exception ) { |
122 | $this->dieWithError( |
123 | [ "apierror-wikilambda_function_call-not-connected", $this->orchestratorHost ], |
124 | null, null, 503 |
125 | ); |
126 | } catch ( ClientException | ServerException $exception ) { |
127 | $zError = ZErrorFactory::wrapMessageInZError( |
128 | $exception->getResponse()->getReasonPhrase(), |
129 | $zObjectAsStdClass |
130 | ); |
131 | $zResponseMap = ZResponseEnvelope::wrapErrorInResponseMap( $zError ); |
132 | $zResponseObject = new ZResponseEnvelope( null, $zResponseMap ); |
133 | $result['data'] = $zResponseObject->getSerialized(); |
134 | } |
135 | $pageResult->addValue( [ 'query' ], $this->getModuleName(), $result ); |
136 | } |
137 | |
138 | /** |
139 | * @inheritDoc |
140 | * @codeCoverageIgnore |
141 | */ |
142 | protected function getAllowedParams(): array { |
143 | return [ |
144 | 'zobject' => [ |
145 | ParamValidator::PARAM_TYPE => 'text', |
146 | ParamValidator::PARAM_REQUIRED => true, |
147 | ] |
148 | ]; |
149 | } |
150 | |
151 | /** |
152 | * Reads file contents from test data directory as JSON array. |
153 | * @param string $fileName |
154 | * @return array file contents (JSON-decoded) |
155 | * @codeCoverageIgnore |
156 | */ |
157 | private function readTestFileAsArray( $fileName ): array { |
158 | return json_decode( ZObjectUtils::readTestFile( $fileName ), true ); |
159 | } |
160 | |
161 | /** |
162 | * Generates URL-encoded example function call exercising user-defined validation function. |
163 | * This function call produces a validation error. Replace |
164 | * Z1000000K1: 'a' with Z1000000K1: 'A' in order to see successful validation. |
165 | * @return string URL-encoded Function Call |
166 | * @codeCoverageIgnore |
167 | */ |
168 | private function createUserDefinedValidationExample(): string { |
169 | $ZMillionOuter = $this->readTestFileAsArray( 'user-defined-validation-type.json' ); |
170 | $ZMillionInner = $this->readTestFileAsArray( 'user-defined-validation-type.json' ); |
171 | $validationZ7 = $this->readTestFileAsArray( 'example-user-defined-validation.json' ); |
172 | $ZMillionOuter["Z4K3"]["Z8K1"][1]["Z17K1"] = $ZMillionInner; |
173 | $validationZ7["Z801K1"]["Z1K1"] = $ZMillionOuter; |
174 | return urlencode( json_encode( $validationZ7 ) ); |
175 | } |
176 | |
177 | /** |
178 | * Generates URL-encoded example function call exercising curry function. |
179 | * @return string URL-encoded Function Call |
180 | * @codeCoverageIgnore |
181 | */ |
182 | private function createCurryExample(): string { |
183 | $curryImplementation = $this->readTestFileAsArray( 'curry-implementation-Z409.json' ); |
184 | $curryFunction = $this->readTestFileAsArray( 'curry-Z408.json' ); |
185 | $curryFunction["Z8K4"][1] = $curryImplementation; |
186 | $curryFunctionCall = $this->readTestFileAsArray( 'curry-call-Z410.json' ); |
187 | $curryFunctionCall["Z8K4"][1]["Z14K2"]["Z7K1"]["Z7K1"] = $curryFunction; |
188 | $andFunction = $this->readTestFileAsArray( 'and-Z407.json' ); |
189 | $curry = [ |
190 | "Z1K1" => "Z7", |
191 | "Z7K1" => $curryFunctionCall, |
192 | "Z410K1" => $andFunction, |
193 | "Z410K2" => [ |
194 | "Z1K1" => "Z40", |
195 | "Z40K1" => "Z41" |
196 | ], |
197 | "Z410K3" => [ |
198 | "Z1K1" => "Z40", |
199 | "Z40K1" => "Z41" |
200 | ] |
201 | ]; |
202 | return urlencode( json_encode( $curry ) ); |
203 | } |
204 | |
205 | /** |
206 | * Generates URL-encoded example function call from JSON file contents. |
207 | * @param string $fileName |
208 | * @return string URL-encoded contents |
209 | * @codeCoverageIgnore |
210 | */ |
211 | private function createExample( $fileName ): string { |
212 | return urlencode( ZObjectUtils::readTestFile( $fileName ) ); |
213 | } |
214 | |
215 | /** |
216 | * @see ApiBase::getExamplesMessages() |
217 | * @return array |
218 | * @codeCoverageIgnore |
219 | */ |
220 | protected function getExamplesMessages() { |
221 | return [ |
222 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
223 | . $this->createExample( 'Z902_false.json' ) |
224 | => 'apihelp-wikilambda_function_call-example-if', |
225 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
226 | . $this->createExample( 'evaluated-js.json' ) |
227 | => 'apihelp-wikilambda_function_call-example-native-js-code', |
228 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
229 | . $this->createExample( 'evaluated-python.json' ) |
230 | => 'apihelp-wikilambda_function_call-example-native-python-code', |
231 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
232 | . $this->createExample( 'example-composition.json' ) |
233 | => 'apihelp-wikilambda_function_call-example-composition', |
234 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
235 | . $this->createExample( 'example-notempty.json' ) |
236 | => 'apihelp-wikilambda_function_call-example-notempty', |
237 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
238 | . $this->createExample( 'example-map.json' ) |
239 | => 'apihelp-wikilambda_function_call-example-map', |
240 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
241 | . $this->createExample( 'example-apply.json' ) |
242 | => 'apihelp-wikilambda_function_call-example-apply', |
243 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
244 | . $this->createExample( 'example-generic-list.json' ) |
245 | => 'apihelp-wikilambda_function_call-example-generic-list', |
246 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
247 | . $this->createExample( 'example-generic-pair.json' ) |
248 | => 'apihelp-wikilambda_function_call-example-generic-pair', |
249 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
250 | . $this->createExample( 'example-generic-map.json' ) |
251 | => 'apihelp-wikilambda_function_call-example-generic-map', |
252 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
253 | . $this->createExample( 'example-user-defined-python.json' ) |
254 | => 'apihelp-wikilambda_function_call-example-user-defined-python', |
255 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
256 | . $this->createExample( 'example-user-defined-javascript.json' ) |
257 | => 'apihelp-wikilambda_function_call-example-user-defined-javascript', |
258 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
259 | . $this->createUserDefinedValidationExample() |
260 | => 'apihelp-wikilambda_function_call-example-user-defined-validation', |
261 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
262 | . $this->createExample( 'example-user-defined-generic-type.json' ) |
263 | => 'apihelp-wikilambda_function_call-example-user-defined-generic-type', |
264 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
265 | . $this->createCurryExample() |
266 | => 'apihelp-wikilambda_function_call-example-curry', |
267 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
268 | . $this->createExample( 'example-socket.json' ) |
269 | => 'apihelp-wikilambda_function_call-example-socket', |
270 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
271 | . $this->createExample( 'example-timeout.json' ) |
272 | => 'apihelp-wikilambda_function_call-example-timeout', |
273 | 'action=wikilambda_function_call&wikilambda_function_call_zobject=' |
274 | . $this->createExample( 'example-orchestrator-timeout.json' ) |
275 | => 'apihelp-wikilambda_function_call-example-orchestrator-timeout', |
276 | ]; |
277 | } |
278 | |
279 | /** |
280 | * A convenience function for making a ZFunctionCall and returning its result to embed within a page. |
281 | * |
282 | * @param string $call The ZFunctionCall to make, as a JSON object turned into a string |
283 | * @return string Currently the only permissable response objects are strings |
284 | * @throws ZErrorException When the request is responded to oddly by the orchestrator |
285 | */ |
286 | public static function makeRequest( $call ): string { |
287 | $api = new ApiMain( new FauxRequest() ); |
288 | $request = new FauxRequest( |
289 | [ |
290 | 'format' => 'json', |
291 | 'action' => 'wikilambda_function_call', |
292 | 'wikilambda_function_call_zobject' => $call, |
293 | ], |
294 | /* wasPosted */ true |
295 | ); |
296 | |
297 | $context = new DerivativeContext( RequestContext::getMain() ); |
298 | $context->setRequest( $request ); |
299 | $api->setContext( $context ); |
300 | $api->execute(); |
301 | $outerResponse = $api->getResult()->getResultData( [], [ 'Strip' => 'all' ] ); |
302 | |
303 | if ( isset( $outerResponse[ 'error' ] ) ) { |
304 | try { |
305 | $zerror = ZObjectFactory::create( $outerResponse['error'] ); |
306 | } catch ( ZErrorException $e ) { |
307 | // Can't use $this->dieWithError() as we're static, so use the call indirectly |
308 | throw ApiUsageException::newWithMessage( |
309 | null, |
310 | [ |
311 | 'apierror-wikilambda_function_call-response-malformed', |
312 | // TODO (T362236): Pass the rendering language in, don't default to English |
313 | $e->getZError()->getMessage() |
314 | ], |
315 | null, |
316 | null, |
317 | 400 |
318 | ); |
319 | } |
320 | if ( !( $zerror instanceof ZError ) ) { |
321 | $zerror = ZErrorFactory::wrapMessageInZError( new ZQuote( $zerror ), $call ); |
322 | } |
323 | throw new ZErrorException( $zerror ); |
324 | } |
325 | |
326 | // Now we know that the request has not failed before it even got to the orchestrator, get the response |
327 | // JSON string as a ZResponseEnvelope (falling back to an empty string in case it's unset). |
328 | $response = ZObjectFactory::create( |
329 | $outerResponse['query']['wikilambda_function_call']['data'] ?? '' |
330 | ); |
331 | |
332 | if ( !( $response instanceof ZResponseEnvelope ) ) { |
333 | // The server's not given us a result! |
334 | $responseType = $response->getZType(); |
335 | $zerror = ZErrorFactory::wrapMessageInZError( |
336 | "Server returned a non-result of type '$responseType'!", |
337 | $call |
338 | ); |
339 | throw new ZErrorException( $zerror ); |
340 | } |
341 | |
342 | if ( $response->hasErrors() ) { |
343 | // If the server has responsed with a Z5/Error, show that properly. |
344 | $zerror = $response->getErrors(); |
345 | if ( !( $zerror instanceof ZError ) ) { |
346 | $zerror = ZErrorFactory::wrapMessageInZError( new ZQuote( $zerror ), $call ); |
347 | } |
348 | throw new ZErrorException( $zerror ); |
349 | } |
350 | |
351 | return trim( $response->getZValue() ); |
352 | } |
353 | |
354 | /** |
355 | * Mark as internal. This isn't meant to be user-facing, and can change at any time. |
356 | * @return bool |
357 | */ |
358 | public function isInternal() { |
359 | return true; |
360 | } |
361 | } |