Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.59% covered (danger)
44.59%
33 / 74
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiFunctionCall
44.59% covered (danger)
44.59%
33 / 74
33.33% covered (danger)
33.33%
1 / 3
73.11
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 run
43.66% covered (danger)
43.66%
31 / 71
0.00% covered (danger)
0.00%
0 / 1
27.88
 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
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
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
11namespace MediaWiki\Extension\WikiLambda\ActionAPI;
12
13use JsonException;
14use MediaWiki\Api\ApiMain;
15use MediaWiki\Api\ApiUsageException;
16use MediaWiki\Extension\WikiLambda\HttpStatus;
17use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
18use MediaWiki\Extension\WikiLambda\ZErrorFactory;
19use MediaWiki\Extension\WikiLambda\ZObjectUtils;
20use MediaWiki\MediaWikiServices;
21use Wikimedia\ParamValidator\ParamValidator;
22use Wikimedia\Telemetry\SpanInterface;
23
24class ApiFunctionCall extends WikiLambdaApiBase {
25
26    public function __construct( ApiMain $mainModule, string $moduleName ) {
27        parent::__construct( $mainModule, $moduleName, 'wikilambda_function_call_' );
28
29        $this->setUp();
30    }
31
32    /**
33     * @inheritDoc
34     */
35    protected function run() {
36        $start = microtime( true );
37
38        // Get input parameters
39        $params = $this->extractRequestParams();
40        $zObjectString = $params[ 'zobject' ];
41
42        // Initialize output
43        $pageResult = $this->getResult();
44
45        // Initialize span
46        $tracer = MediaWikiServices::getInstance()->getTracer();
47        $span = $tracer->createSpan( 'WikiLambda ApiFunctionCall' )
48            ->setSpanKind( SpanInterface::SPAN_KIND_CLIENT )
49            ->start();
50        $span->activate();
51
52        // 1. JSON decode input zobject and die with ZError if invalid syntax
53        try {
54            $zObjectAsStdClass = json_decode( $zObjectString, false, 512, JSON_THROW_ON_ERROR );
55        } catch ( JsonException $e ) {
56            // (T389702) If the JSON is invalid, we return a 400 error rather than have PHP die
57            $this->submitFunctionCallEvent( HttpStatus::BAD_REQUEST, null, $start );
58            $zError = ZErrorFactory::createZErrorInstance( ZErrorTypeRegistry::Z_ERROR_INVALID_SYNTAX, [
59                'message' => $e->getMessage(),
60                'input' => $zObjectString
61            ] );
62            $span
63                ->setSpanStatus( SpanInterface::SPAN_STATUS_ERROR )
64                ->setAttributes( [ 'error.message' => $e->getMessage() ] )
65                ->end();
66            WikiLambdaApiBase::dieWithZError( $zError, HttpStatus::BAD_REQUEST );
67        }
68
69        // Cautionary canonicalization
70        $zObjectAsStdClass = ZObjectUtils::canonicalize( $zObjectAsStdClass );
71
72        // Initialize flags:
73        $flags = [
74            'validate' => true,
75            'isUnsavedCode' => false,
76            // Get bypassCache boolean flag if set to any truthy value, false otherwise
77            'bypassCache' => filter_var( $params[ 'bypass-cache' ], FILTER_VALIDATE_BOOLEAN )
78        ];
79
80        // Get function zid for logging
81        $function = ZObjectUtils::getFunctionZidOrNull( $zObjectAsStdClass );
82        if ( !ZObjectUtils::isValidZObjectReference( (string)$function ) ) {
83            $function = 'No valid ZID';
84            $this->getLogger()->info(
85                __METHOD__ . ' unable to find a ZID for the function called',
86                [
87                    'zobject' => $zObjectString
88                ]
89            );
90        }
91
92        // Arbitrary implementation calls need more than wikilambda-execute;
93        // require wikilambda-execute-unsaved-code, so that it can be independently
94        // activated/deactivated (to run an arbitrary implementation, you have to
95        // pass a custom function with the raw implementation rather than a ZID string.)
96        if ( $this->hasUnsavedCode( $zObjectAsStdClass ) ) {
97            $flags[ 'isUnsavedCode' ] = true;
98        }
99
100        try {
101            $response = $this->executeFunctionCall( $zObjectAsStdClass, $flags );
102
103            $result = [
104                'success' => true,
105                'data' => $response['result']
106            ];
107
108            // Get the Http status code returned by the orchestrator, and:
109            // * For 2xx or 4xx, log it as a successful request
110            // * For 5xx (server errors), log it as a failer request
111            $httpStatusCode = $response['httpStatusCode'];
112            if ( $httpStatusCode < HttpStatus::INTERNAL_SERVER_ERROR ) {
113                $span->setSpanStatus( SpanInterface::SPAN_STATUS_OK );
114            } else {
115                $result['success'] = false;
116                $span
117                    ->setSpanStatus( SpanInterface::SPAN_STATUS_ERROR )
118                    ->setAttributes( [
119                        'response.status_code' => $httpStatusCode,
120                        'exception.message' => $response['result']
121                    ] );
122            }
123
124            $pageResult->addValue( [], $this->getModuleName(), $result );
125            $this->submitFunctionCallEvent( $httpStatusCode, $function, $start );
126
127        } catch ( ApiUsageException $e ) {
128            // Whenever executeFunctionCall dies with error, we intercept it so that:
129            // * we submit a function call metrics event,
130            // * we rethrow the error
131            // * finally we end the span
132            $errorCode = $e->getCode();
133            $httpStatusCode = ( is_int( $errorCode ) && $errorCode >= 100 && $errorCode < 600 ) ?
134                $errorCode : HttpStatus::BAD_REQUEST;
135            $this->submitFunctionCallEvent( $httpStatusCode, $function, $start );
136            throw $e;
137
138        } catch ( \Throwable $e ) {
139            // Whatever we catch here is an unexpected system error:
140            // * we log accordingly as error
141            // * we rethrow the error
142            // * finally we end the span
143            $this->getLogger()->error(
144                __METHOD__ . ' caused an unexpected system failure',
145                [
146                    'zobject' => $zObjectString,
147                    'exception' => $e
148                ]
149            );
150            throw $e;
151
152        } finally {
153            // End the span
154            $span->end();
155        }
156    }
157
158    /**
159     * @inheritDoc
160     * @codeCoverageIgnore
161     */
162    protected function getAllowedParams(): array {
163        return [
164            'zobject' => [
165                ParamValidator::PARAM_TYPE => 'text',
166                ParamValidator::PARAM_REQUIRED => true,
167            ],
168            'bypass-cache' => [
169                ParamValidator::PARAM_TYPE => 'boolean',
170                ParamValidator::PARAM_REQUIRED => false,
171                ParamValidator::PARAM_DEFAULT => false
172            ]
173        ];
174    }
175
176    /**
177     * Reads file contents from test data directory as JSON array.
178     * @param string $fileName
179     * @return array file contents (JSON-decoded)
180     * @codeCoverageIgnore
181     */
182    private function readTestFileAsArray( $fileName ): array {
183        return json_decode( ZObjectUtils::readTestFile( $fileName ), true );
184    }
185
186    /**
187     * Generates URL-encoded example function call exercising user-defined validation function.
188     * This function call produces a validation error. Replace
189     * Z1000000K1: 'a' with Z1000000K1: 'A' in order to see successful validation.
190     * @return string URL-encoded Function Call
191     * @codeCoverageIgnore
192     */
193    private function createUserDefinedValidationExample(): string {
194        $ZMillionOuter = $this->readTestFileAsArray( 'user-defined-validation-type.json' );
195        $ZMillionInner = $this->readTestFileAsArray( 'user-defined-validation-type.json' );
196        $validationZ7 = $this->readTestFileAsArray( 'example-user-defined-validation.json' );
197        $ZMillionOuter["Z4K3"]["Z8K1"][1]["Z17K1"] = $ZMillionInner;
198        $validationZ7["Z801K1"]["Z1K1"] = $ZMillionOuter;
199        return urlencode( json_encode( $validationZ7 ) );
200    }
201
202    /**
203     * Generates URL-encoded example function call exercising curry function.
204     * @return string URL-encoded Function Call
205     * @codeCoverageIgnore
206     */
207    private function createCurryExample(): string {
208        $curryImplementation = $this->readTestFileAsArray( 'curry-implementation-Z409.json' );
209        $curryFunction = $this->readTestFileAsArray( 'curry-Z408.json' );
210        $curryFunction["Z8K4"][1] = $curryImplementation;
211        $curryFunctionCall = $this->readTestFileAsArray( 'curry-call-Z410.json' );
212        $curryFunctionCall["Z8K4"][1]["Z14K2"]["Z7K1"]["Z7K1"] = $curryFunction;
213        $andFunction = $this->readTestFileAsArray( 'and-Z407.json' );
214        $curry = [
215            "Z1K1" => "Z7",
216            "Z7K1" => $curryFunctionCall,
217            "Z410K1" => $andFunction,
218            "Z410K2" => [
219                "Z1K1" => "Z40",
220                "Z40K1" => "Z41"
221            ],
222            "Z410K3" => [
223                "Z1K1" => "Z40",
224                "Z40K1" => "Z41"
225            ]
226        ];
227        return urlencode( json_encode( $curry ) );
228    }
229
230    /**
231     * Generates URL-encoded example function call from JSON file contents.
232     * @param string $fileName
233     * @return string URL-encoded contents
234     * @codeCoverageIgnore
235     */
236    private function createExample( $fileName ): string {
237        return urlencode( ZObjectUtils::readTestFile( $fileName ) );
238    }
239
240    /**
241     * @see ApiBase::getExamplesMessages()
242     * @return array
243     * @codeCoverageIgnore
244     */
245    protected function getExamplesMessages() {
246        return [
247            'action=wikilambda_function_call&wikilambda_function_call_zobject='
248                . $this->createExample( 'Z902_false.json' )
249                => 'apihelp-wikilambda_function_call-example-if',
250            'action=wikilambda_function_call&wikilambda_function_call_zobject='
251                . $this->createExample( 'evaluated-js.json' )
252                => 'apihelp-wikilambda_function_call-example-native-js-code',
253            'action=wikilambda_function_call&wikilambda_function_call_zobject='
254                . $this->createExample( 'evaluated-python.json' )
255                => 'apihelp-wikilambda_function_call-example-native-python-code',
256            'action=wikilambda_function_call&wikilambda_function_call_zobject='
257                . $this->createExample( 'example-composition.json' )
258                => 'apihelp-wikilambda_function_call-example-composition',
259            'action=wikilambda_function_call&wikilambda_function_call_zobject='
260                . $this->createExample( 'example-notempty.json' )
261                => 'apihelp-wikilambda_function_call-example-notempty',
262            'action=wikilambda_function_call&wikilambda_function_call_zobject='
263                . $this->createExample( 'example-map.json' )
264                => 'apihelp-wikilambda_function_call-example-map',
265            'action=wikilambda_function_call&wikilambda_function_call_zobject='
266                . $this->createExample( 'example-apply.json' )
267                => 'apihelp-wikilambda_function_call-example-apply',
268            'action=wikilambda_function_call&wikilambda_function_call_zobject='
269                . $this->createExample( 'example-generic-list.json' )
270                => 'apihelp-wikilambda_function_call-example-generic-list',
271            'action=wikilambda_function_call&wikilambda_function_call_zobject='
272                . $this->createExample( 'example-generic-pair.json' )
273                => 'apihelp-wikilambda_function_call-example-generic-pair',
274            'action=wikilambda_function_call&wikilambda_function_call_zobject='
275                . $this->createExample( 'example-generic-map.json' )
276                => 'apihelp-wikilambda_function_call-example-generic-map',
277            'action=wikilambda_function_call&wikilambda_function_call_zobject='
278                . $this->createExample( 'example-user-defined-python.json' )
279                => 'apihelp-wikilambda_function_call-example-user-defined-python',
280            'action=wikilambda_function_call&wikilambda_function_call_zobject='
281                . $this->createExample( 'example-user-defined-javascript.json' )
282                => 'apihelp-wikilambda_function_call-example-user-defined-javascript',
283            'action=wikilambda_function_call&wikilambda_function_call_zobject='
284                . $this->createUserDefinedValidationExample()
285                => 'apihelp-wikilambda_function_call-example-user-defined-validation',
286            'action=wikilambda_function_call&wikilambda_function_call_zobject='
287                . $this->createExample( 'example-user-defined-generic-type.json' )
288                => 'apihelp-wikilambda_function_call-example-user-defined-generic-type',
289            'action=wikilambda_function_call&wikilambda_function_call_zobject='
290                . $this->createCurryExample()
291                => 'apihelp-wikilambda_function_call-example-curry',
292            'action=wikilambda_function_call&wikilambda_function_call_zobject='
293                . $this->createExample( 'example-socket.json' )
294                => 'apihelp-wikilambda_function_call-example-socket',
295            'action=wikilambda_function_call&wikilambda_function_call_zobject='
296                . $this->createExample( 'example-timeout.json' )
297                => 'apihelp-wikilambda_function_call-example-timeout',
298            'action=wikilambda_function_call&wikilambda_function_call_zobject='
299                . $this->createExample( 'example-orchestrator-timeout.json' )
300                => 'apihelp-wikilambda_function_call-example-orchestrator-timeout',
301        ];
302    }
303
304    /**
305     * Mark as internal. This isn't meant to be user-facing, and can change at any time.
306     * @return bool
307     */
308    public function isInternal() {
309        return true;
310    }
311}