Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
68.52% |
37 / 54 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
PublicApiRun | |
68.52% |
37 / 54 |
|
50.00% |
2 / 4 |
23.99 | |
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 | |
66.67% |
32 / 48 |
|
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 | |
11 | namespace MediaWiki\Extension\WikiLambda\PublicAPI; |
12 | |
13 | use ApiPageSet; |
14 | use GuzzleHttp\Exception\ClientException; |
15 | use GuzzleHttp\Exception\ConnectException; |
16 | use GuzzleHttp\Exception\ServerException; |
17 | use MediaWiki\Extension\WikiLambda\ActionAPI\WikiLambdaApiBase; |
18 | use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry; |
19 | use MediaWiki\Extension\WikiLambda\ZErrorFactory; |
20 | use MediaWiki\Extension\WikiLambda\ZObjects\ZResponseEnvelope; |
21 | use MediaWiki\Extension\WikiLambda\ZObjectUtils; |
22 | use MediaWiki\PoolCounter\PoolCounterWorkViaCallback; |
23 | use MediaWiki\Status\Status; |
24 | use Wikimedia\ParamValidator\ParamValidator; |
25 | |
26 | class 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 | } |