Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 119 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
FetchHandler | |
0.00% |
0 / 119 |
|
0.00% |
0 / 7 |
1190 | |
0.00% |
0 / 1 |
run | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
210 | |||
getTypeDependencies | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
156 | |||
applyCacheControl | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
needsWriteAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getParamSettings | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
2 | |||
dieRESTfullyWithZError | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
dieRESTfully | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * WikiLambda ZObject simple fetching REST 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\RESTAPI; |
12 | |
13 | use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry; |
14 | use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry; |
15 | use MediaWiki\Extension\WikiLambda\WikiLambdaServices; |
16 | use MediaWiki\Extension\WikiLambda\ZErrorException; |
17 | use MediaWiki\Extension\WikiLambda\ZErrorFactory; |
18 | use MediaWiki\Extension\WikiLambda\ZObjectContentHandler; |
19 | use MediaWiki\Extension\WikiLambda\ZObjects\ZError; |
20 | use MediaWiki\Extension\WikiLambda\ZObjectUtils; |
21 | use MediaWiki\Logger\LoggerFactory; |
22 | use MediaWiki\Rest\LocalizedHttpException; |
23 | use MediaWiki\Rest\ResponseInterface; |
24 | use MediaWiki\Rest\SimpleHandler; |
25 | use MediaWiki\Title\Title; |
26 | use Psr\Log\LoggerInterface; |
27 | use Wikimedia\Message\MessageValue; |
28 | use Wikimedia\ParamValidator\ParamValidator; |
29 | |
30 | /** |
31 | * Simple REST API to fetch the latest versions of one or more ZObjects |
32 | * via GET /wikifunctions/v0/fetch/{zids} |
33 | */ |
34 | class FetchHandler extends SimpleHandler { |
35 | |
36 | public const MAX_REQUESTED_ZIDS = 50; |
37 | private ZTypeRegistry $typeRegistry; |
38 | private LoggerInterface $logger; |
39 | |
40 | /** @inheritDoc */ |
41 | public function run( $ZIDs, $revisions = [] ) { |
42 | $this->typeRegistry = ZTypeRegistry::singleton(); |
43 | $this->logger = LoggerFactory::getInstance( 'WikiLambda' ); |
44 | |
45 | $responseList = []; |
46 | |
47 | $language = $this->getRequest()->getQueryParams()['language']; |
48 | $getDependencies = $this->getRequest()->getQueryParams()['getDependencies']; |
49 | |
50 | if ( count( $revisions ) > 0 && ( count( $revisions ) !== count( $ZIDs ) ) ) { |
51 | $zErrorObject = ZErrorFactory::createZErrorInstance( |
52 | ZErrorTypeRegistry::Z_ERROR_UNKNOWN, |
53 | [ |
54 | 'message' => "You must specify a revision for each ZID, or none at all." |
55 | ] |
56 | ); |
57 | $this->dieRESTfullyWithZError( $zErrorObject, 400 ); |
58 | } |
59 | |
60 | $reqSize = count( $ZIDs ); |
61 | if ( $reqSize > self::MAX_REQUESTED_ZIDS ) { |
62 | $this->dieRESTfully( 'wikilambda-restapi-fetch-too-many', [ $reqSize, self::MAX_REQUESTED_ZIDS ], 403 ); |
63 | } |
64 | |
65 | $extraDependencies = []; |
66 | |
67 | foreach ( $ZIDs as $index => $ZID ) { |
68 | if ( !ZObjectUtils::isValidZObjectReference( mb_strtoupper( $ZID ) ) ) { |
69 | $zErrorObject = ZErrorFactory::createZErrorInstance( |
70 | ZErrorTypeRegistry::Z_ERROR_INVALID_REFERENCE, |
71 | [ 'data' => $ZID ] |
72 | ); |
73 | $this->dieRESTfullyWithZError( $zErrorObject, 404 ); |
74 | } else { |
75 | $title = Title::newFromText( $ZID, NS_MAIN ); |
76 | |
77 | if ( !$title || !( $title instanceof Title ) || !$title->exists() ) { |
78 | $zErrorObject = ZErrorFactory::createZErrorInstance( |
79 | ZErrorTypeRegistry::Z_ERROR_UNKNOWN_REFERENCE, |
80 | [ 'data' => $ZID ] |
81 | ); |
82 | $this->dieRESTfullyWithZError( $zErrorObject, 404 ); |
83 | } else { |
84 | $revision = $revisions[$index] ?? null; |
85 | |
86 | try { |
87 | $fetchedContent = ZObjectContentHandler::getExternalRepresentation( |
88 | $title, $language, $revision |
89 | ); |
90 | } catch ( ZErrorException $error ) { |
91 | // This probably means that the requested revision is not known; return |
92 | // null for this entry rather than throwing or returning a ZError instance. |
93 | $this->dieRESTfully( 'wikilambda-restapi-revision-mismatch', [ $revision, $ZID ], 404 ); |
94 | } |
95 | |
96 | $responseList[ $ZID ] = $fetchedContent; |
97 | |
98 | if ( $getDependencies ) { |
99 | $dependencies = $this->getTypeDependencies( json_decode( $fetchedContent ) ?? [] ); |
100 | foreach ( $dependencies as $_key => $dep ) { |
101 | if ( in_array( $dep, $ZIDs ) ) { |
102 | continue; |
103 | } |
104 | $extraDependencies[] = $dep; |
105 | } |
106 | } |
107 | |
108 | } |
109 | } |
110 | } |
111 | |
112 | // We use array_unique to de-duplicate dependencies if they're used multiple times |
113 | foreach ( array_unique( $extraDependencies ) as $_key => $ZID ) { |
114 | $responseList[$ZID] = ZObjectContentHandler::getExternalRepresentation( |
115 | Title::newFromText( $ZID, NS_MAIN ), |
116 | $language, |
117 | // Get latest, as we have no revision to request. |
118 | null |
119 | ); |
120 | } |
121 | |
122 | $response = $this->getResponseFactory()->createJson( $responseList ); |
123 | |
124 | return $response; |
125 | } |
126 | |
127 | /** |
128 | * Returns the types of type keys and function arguments |
129 | * |
130 | * @param \stdClass $zobject |
131 | * @return array |
132 | */ |
133 | private function getTypeDependencies( $zobject ) { |
134 | $dependencies = []; |
135 | |
136 | // We need to return dependencies of those objects that build arguments of keys: |
137 | // Types: return the types of its keys |
138 | // Functions: return the types of its arguments |
139 | $content = $zobject->{ ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE }; |
140 | if ( |
141 | is_array( $content ) || |
142 | is_string( $content ) || |
143 | !property_exists( $content, ZTypeRegistry::Z_OBJECT_TYPE ) |
144 | ) { |
145 | return $dependencies; |
146 | } |
147 | |
148 | $type = $content->{ ZTypeRegistry::Z_OBJECT_TYPE }; |
149 | if ( $type === ZTypeRegistry::Z_TYPE ) { |
150 | $keys = $content->{ ZTypeRegistry::Z_TYPE_KEYS }; |
151 | foreach ( array_slice( $keys, 1 ) as $key ) { |
152 | $keyType = $key->{ ZTypeRegistry::Z_KEY_TYPE }; |
153 | if ( is_string( $keyType ) && ( !$this->typeRegistry->isZTypeBuiltIn( $keyType ) ) ) { |
154 | array_push( $dependencies, $keyType ); |
155 | } |
156 | } |
157 | } elseif ( $type === ZTypeRegistry::Z_FUNCTION ) { |
158 | $args = $content->{ ZTypeRegistry::Z_FUNCTION_ARGUMENTS }; |
159 | foreach ( array_slice( $args, 1 ) as $arg ) { |
160 | $argType = $arg->{ ZTypeRegistry::Z_ARGUMENTDECLARATION_TYPE }; |
161 | if ( is_string( $argType ) && ( !$this->typeRegistry->isZTypeBuiltIn( $argType ) ) ) { |
162 | array_push( $dependencies, $argType ); |
163 | } |
164 | } |
165 | } |
166 | |
167 | return array_unique( $dependencies ); |
168 | } |
169 | |
170 | public function applyCacheControl( ResponseInterface $response ) { |
171 | if ( $response->getStatusCode() >= 200 && $response->getStatusCode() < 400 ) { |
172 | $response->setHeader( 'Cache-Control', 'public,must-revalidate,s-max-age=' . 60 * 60 * 24 ); |
173 | } |
174 | } |
175 | |
176 | /** @inheritDoc */ |
177 | public function needsWriteAccess() { |
178 | return false; |
179 | } |
180 | |
181 | /** @inheritDoc */ |
182 | public function getParamSettings() { |
183 | $zObjectStore = WikiLambdaServices::getZObjectStore(); |
184 | |
185 | return [ |
186 | 'zids' => [ |
187 | self::PARAM_SOURCE => 'path', |
188 | ParamValidator::PARAM_TYPE => 'string', |
189 | ParamValidator::PARAM_ISMULTI => true, |
190 | ParamValidator::PARAM_REQUIRED => true, |
191 | ], |
192 | 'revisions' => [ |
193 | self::PARAM_SOURCE => 'path', |
194 | ParamValidator::PARAM_TYPE => 'string', |
195 | ParamValidator::PARAM_ISMULTI => true, |
196 | ParamValidator::PARAM_REQUIRED => false, |
197 | ], |
198 | 'language' => [ |
199 | self::PARAM_SOURCE => 'query', |
200 | ParamValidator::PARAM_TYPE => $zObjectStore->fetchAllZLanguageCodes(), |
201 | ParamValidator::PARAM_DEFAULT => null, |
202 | ParamValidator::PARAM_REQUIRED => false, |
203 | ], |
204 | 'getDependencies' => [ |
205 | self::PARAM_SOURCE => 'query', |
206 | ParamValidator::PARAM_TYPE => 'boolean', |
207 | ParamValidator::PARAM_DEFAULT => false, |
208 | ParamValidator::PARAM_REQUIRED => false, |
209 | ], |
210 | ]; |
211 | } |
212 | |
213 | private function dieRESTfullyWithZError( ZError $zerror, int $code = 500, array $errorData = [] ) { |
214 | try { |
215 | $errorData['errorData'] = $zerror->getErrorData(); |
216 | } catch ( ZErrorException $e ) { |
217 | // Generating the human-readable error data itself threw. Oh dear. |
218 | |
219 | $this->logger->warning( |
220 | __METHOD__ . ' called but an error was thrown when trying to report an error', |
221 | [ |
222 | 'zerror' => $zerror->getSerialized(), |
223 | 'error' => $e, |
224 | ] |
225 | ); |
226 | |
227 | $errorData['errorData'] = [ 'zerror' => $zerror->getSerialized() ]; |
228 | } |
229 | |
230 | $this->dieRESTfully( 'wikilambda-zerror', [ $zerror->getZErrorType() ], $code, $errorData ); |
231 | } |
232 | |
233 | /** |
234 | * @return never |
235 | */ |
236 | private function dieRESTfully( string $messageKey, array $spec, int $code, array $errorData = [] ) { |
237 | throw new LocalizedHttpException( |
238 | new MessageValue( $messageKey, $spec ), $code, $errorData |
239 | ); |
240 | } |
241 | } |