Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.52% covered (warning)
68.52%
37 / 54
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
PublicApiRun
68.52% covered (warning)
68.52%
37 / 54
50.00% covered (danger)
50.00%
2 / 4
23.99
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
 execute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 executeGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
66.67% covered (warning)
66.67%
32 / 48
0.00% covered (danger)
0.00%
0 / 1
15.48
 getAllowedParams
n/a
0 / 0
n/a
0 / 0
1
 getExamplesMessages
n/a
0 / 0
n/a
0 / 0
1
1<?php
2/**
3 * WikiLambda function call run public 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\PublicAPI;
12
13use ApiPageSet;
14use GuzzleHttp\Exception\ClientException;
15use GuzzleHttp\Exception\ConnectException;
16use GuzzleHttp\Exception\ServerException;
17use MediaWiki\Extension\WikiLambda\ActionAPI\WikiLambdaApiBase;
18use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
19use MediaWiki\Extension\WikiLambda\ZErrorFactory;
20use MediaWiki\Extension\WikiLambda\ZObjects\ZResponseEnvelope;
21use MediaWiki\Extension\WikiLambda\ZObjectUtils;
22use MediaWiki\PoolCounter\PoolCounterWorkViaCallback;
23use MediaWiki\Status\Status;
24use Wikimedia\ParamValidator\ParamValidator;
25
26class PublicApiRun extends WikiLambdaApiBase {
27
28    /**
29     * @inheritDoc
30     */
31    public function __construct( $query, $moduleName ) {
32        parent::__construct( $query, $moduleName );
33
34        $this->setUp();
35    }
36
37    /**
38     * @inheritDoc
39     */
40    public function execute() {
41        // (T362271) Emit appropriate cache headers for a 7 day TTL
42        // NOTE (T362273): MediaWiki out-guesses us and assumes we don't know what we're doing; to fix so it works
43        $this->getMain()->setCacheMode( 'public' );
44        $this->getMain()->setCacheMaxAge( 60 * 60 * 24 * 7 );
45
46        $this->run();
47    }
48
49    /**
50     * @inheritDoc
51     */
52    public function executeGenerator( $resultPageSet ) {
53        $this->run( $resultPageSet );
54    }
55
56    /**
57     * TODO (T338251): Use WikiLambdaApiBase::executeFunctionCall() rather than rolling our own.
58     *
59     * @param ApiPageSet|null $resultPageSet
60     */
61    private function run( $resultPageSet = null ) {
62        // Unlike the Special pages, we don't have a helpful userCanExecute() method
63        $userAuthority = $this->getContext()->getAuthority();
64        if ( !$userAuthority->isAllowed( 'wikifunctions-run' ) ) {
65            $zError = ZErrorFactory::createZErrorInstance( ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN, [] );
66            $this->dieWithZError( $zError, 403 );
67        }
68
69        $params = $this->extractRequestParams();
70        $pageResult = $this->getResult();
71        $stringOfAZ = $params[ 'function_call' ];
72        $zObjectAsStdClass = json_decode( $stringOfAZ );
73        $jsonQuery = [
74            'zobject' => $zObjectAsStdClass,
75            'doValidate' => true
76        ];
77
78        // Don't allow the public API to run "unsaved code" (a custom function with the raw
79        // implementation rather than a ZID string).
80        $isUnsavedCode = false;
81        if (
82            property_exists( $zObjectAsStdClass, 'Z7K1' ) &&
83            is_object( $zObjectAsStdClass->Z7K1 ) &&
84            property_exists( $zObjectAsStdClass->Z7K1, 'Z8K4' ) &&
85            count( $zObjectAsStdClass->Z7K1->Z8K4 ) > 1
86        ) {
87            $implementation = $zObjectAsStdClass->Z7K1->Z8K4[ 1 ];
88            if ( is_object( $implementation ) && property_exists( $implementation, 'Z14K1' ) ) {
89                $isUnsavedCode = true;
90            }
91        }
92        if ( $isUnsavedCode ) {
93            $zError = ZErrorFactory::createZErrorInstance( ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN, [] );
94            $this->dieWithZError( $zError, 403 );
95        }
96
97        $work = new PoolCounterWorkViaCallback( 'WikiLambdaFunctionCall', $this->getUser()->getName(), [
98            'doWork' => function () use ( $jsonQuery ) {
99                return $this->orchestrator->orchestrate( $jsonQuery );
100            },
101            'error' => function ( Status $status ) {
102                $this->dieWithError( [ "apierror-wikilambda_function_call-concurrency-limit" ], null, null, 429 );
103            }
104        ] );
105
106        $result = [];
107        try {
108            $response = $work->execute();
109            $result['data'] = $response;
110        } catch ( ConnectException $exception ) {
111            $this->dieWithError(
112                [ "apierror-wikilambda_function_call-not-connected", $this->orchestratorHost ],
113                null, null, 503
114            );
115        } catch ( ClientException | ServerException $exception ) {
116            $zError = ZErrorFactory::wrapMessageInZError(
117                $exception->getResponse()->getReasonPhrase(),
118                $zObjectAsStdClass
119            );
120            $zResponseMap = ZResponseEnvelope::wrapErrorInResponseMap( $zError );
121            $zResponseObject = new ZResponseEnvelope( null, $zResponseMap );
122            $result['data'] = $zResponseObject->getSerialized();
123        }
124        $pageResult->addValue( [], $this->getModuleName(), $result );
125    }
126
127    /**
128     * @inheritDoc
129     * @codeCoverageIgnore
130     */
131    protected function getAllowedParams(): array {
132        return [
133            'function_call' => [
134                ParamValidator::PARAM_TYPE => 'text',
135                ParamValidator::PARAM_REQUIRED => true,
136            ]
137        ];
138    }
139
140    /**
141     * @see ApiBase::getExamplesMessages()
142     * @return array
143     * @codeCoverageIgnore
144     */
145    protected function getExamplesMessages() {
146        return [
147            'action=wikifunctions_run&function_call='
148                . urlencode( ZObjectUtils::readTestFile( 'Z902_false.json' ) )
149                => 'apihelp-wikilambda_function_call-example-if',
150        ];
151    }
152}