Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
82.64% |
100 / 121 |
|
37.50% |
3 / 8 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryZObjects | |
82.64% |
100 / 121 |
|
37.50% |
3 / 8 |
49.79 | |
0.00% |
0 / 1 |
__construct | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
execute | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
executeGenerator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dieWithZError | |
38.89% |
7 / 18 |
|
0.00% |
0 / 1 |
2.91 | |||
fetchContent | |
76.67% |
23 / 30 |
|
0.00% |
0 / 1 |
8.81 | |||
getTypeDependencies | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
12 | |||
run | |
100.00% |
47 / 47 |
|
100.00% |
1 / 1 |
12 | |||
getAllowedParams | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getExamplesMessages | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
setLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * WikiLambda ZObjects helper for the query 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 ApiBase; |
14 | use ApiPageSet; |
15 | use ApiQueryGeneratorBase; |
16 | use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry; |
17 | use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry; |
18 | use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry; |
19 | use MediaWiki\Extension\WikiLambda\WikiLambdaServices; |
20 | use MediaWiki\Extension\WikiLambda\ZErrorException; |
21 | use MediaWiki\Extension\WikiLambda\ZErrorFactory; |
22 | use MediaWiki\Extension\WikiLambda\ZObjectContent; |
23 | use MediaWiki\Extension\WikiLambda\ZObjects\ZError; |
24 | use MediaWiki\Extension\WikiLambda\ZObjectUtils; |
25 | use MediaWiki\Languages\LanguageFallback; |
26 | use MediaWiki\Languages\LanguageNameUtils; |
27 | use MediaWiki\Logger\LoggerFactory; |
28 | use MediaWiki\Title\TitleFactory; |
29 | use Psr\Log\LoggerAwareInterface; |
30 | use Psr\Log\LoggerInterface; |
31 | use stdClass; |
32 | use Wikimedia\ParamValidator\ParamValidator; |
33 | |
34 | class ApiQueryZObjects extends ApiQueryGeneratorBase implements LoggerAwareInterface { |
35 | |
36 | protected LanguageFallback $languageFallback; |
37 | protected LanguageNameUtils $languageNameUtils; |
38 | protected TitleFactory $titleFactory; |
39 | protected ZTypeRegistry $typeRegistry; |
40 | protected LoggerInterface $logger; |
41 | |
42 | /** |
43 | * @inheritDoc |
44 | * @codeCoverageIgnore |
45 | */ |
46 | public function __construct( |
47 | $query, |
48 | $moduleName, |
49 | LanguageFallback $languageFallback, |
50 | LanguageNameUtils $languageNameUtils, |
51 | TitleFactory $titleFactory |
52 | ) { |
53 | parent::__construct( $query, $moduleName, 'wikilambdaload_' ); |
54 | |
55 | $this->languageFallback = $languageFallback; |
56 | $this->languageNameUtils = $languageNameUtils; |
57 | $this->titleFactory = $titleFactory; |
58 | $this->typeRegistry = ZTypeRegistry::singleton(); |
59 | $this->setLogger( LoggerFactory::getInstance( 'WikiLambda' ) ); |
60 | } |
61 | |
62 | /** |
63 | * @inheritDoc |
64 | */ |
65 | public function execute() { |
66 | // (T362271) Emit appropriate cache headers for a 24 hour TTL |
67 | // NOTE (T362273): MediaWiki out-guesses us and assumes we don't know what we're doing; to fix so it works |
68 | $this->getMain()->setCacheMode( 'public' ); |
69 | $this->getMain()->setCacheMaxAge( 60 * 60 * 24 ); |
70 | |
71 | $this->run(); |
72 | } |
73 | |
74 | /** |
75 | * @inheritDoc |
76 | */ |
77 | public function executeGenerator( $resultPageSet ) { |
78 | $this->run( $resultPageSet ); |
79 | } |
80 | |
81 | /** |
82 | * This is a copy of WikiLambdaApiBase::dieWithZError() that we can't inherit from as we |
83 | * have to extend ApiQueryGeneratorBase. |
84 | * |
85 | * @param ZError $zerror The ZError object to return to the user |
86 | * @param int $code HTTP error code, defaulting to 400/Bad Request |
87 | */ |
88 | public function dieWithZError( $zerror, $code = 400 ) { |
89 | try { |
90 | $errorData = $zerror->getErrorData(); |
91 | } catch ( ZErrorException $e ) { |
92 | // Generating the human-readable error data itself threw. Oh dear. |
93 | $this->getLogger()->warning( |
94 | __METHOD__ . ' called but an error was thrown when trying to report an error', |
95 | [ |
96 | 'zerror' => $zerror->getSerialized(), |
97 | 'error' => $e, |
98 | ] |
99 | ); |
100 | |
101 | $errorData = [ |
102 | 'zerror' => $zerror->getSerialized() |
103 | ]; |
104 | } |
105 | |
106 | parent::dieWithError( |
107 | [ 'wikilambda-zerror', $zerror->getZErrorType() ], |
108 | null, |
109 | $errorData, |
110 | $code |
111 | ); |
112 | } |
113 | |
114 | /** |
115 | * @param string $zid |
116 | * @param array|null $languages |
117 | * @param bool $getDependencies |
118 | * @param int|null $revision |
119 | * @return array |
120 | * @throws ZErrorException |
121 | */ |
122 | private function fetchContent( $zid, $languages, $getDependencies, $revision = null ) { |
123 | // Check for invalid ZID and throw INVALID_TITLE exception |
124 | if ( !ZObjectUtils::isValidZObjectReference( $zid ) ) { |
125 | throw new ZErrorException( |
126 | ZErrorFactory::createZErrorInstance( |
127 | ZErrorTypeRegistry::Z_ERROR_INVALID_TITLE, |
128 | [ 'title' => $zid ] |
129 | ) |
130 | ); |
131 | } |
132 | |
133 | // Check for unavailable ZObject and throw ZID_NOT_FOUND exception |
134 | $title = $this->titleFactory->newFromText( $zid, NS_MAIN ); |
135 | if ( !$title || !$title->exists() ) { |
136 | throw new ZErrorException( |
137 | ZErrorFactory::createZErrorInstance( |
138 | ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND, |
139 | [ "data" => $zid ] |
140 | ) |
141 | ); |
142 | } |
143 | |
144 | // Fetch ZObject and die if there are unmanageable errors |
145 | $zObjectStore = WikiLambdaServices::getZObjectStore(); |
146 | $page = $zObjectStore->fetchZObjectByTitle( $title, $revision ); |
147 | |
148 | if ( !$page ) { |
149 | $this->dieWithError( [ 'apierror-query+wikilambdaload_zobjects-unloadable', $zid ], null, null, 500 ); |
150 | } |
151 | if ( !( $page instanceof ZObjectContent ) ) { |
152 | $this->dieWithError( [ 'apierror-query+wikilambdaload_zobjects-notzobject', $zid ], null, null, 400 ); |
153 | } |
154 | |
155 | // The object was successfully retrieved |
156 | $zobject = $page->getObject(); |
157 | $dependencies = []; |
158 | |
159 | // 1. Get the dependency types of type keys and function arguments |
160 | if ( $getDependencies ) { |
161 | $dependencies = $this->getTypeDependencies( $zobject ); |
162 | } |
163 | |
164 | // 2. Select only the requested language from all ZMultilingualStrings |
165 | if ( is_array( $languages ) ) { |
166 | $langRegistry = ZLangRegistry::singleton(); |
167 | $languageZids = $langRegistry->getLanguageZids( $languages ); |
168 | $zobject = ZObjectUtils::filterZMultilingualStringsToLanguage( $zobject, $languageZids ); |
169 | } |
170 | |
171 | return [ $zobject, $dependencies ]; |
172 | } |
173 | |
174 | /** |
175 | * Returns the types of type keys and function arguments |
176 | * |
177 | * @param stdClass $zobject |
178 | * @return array |
179 | */ |
180 | private function getTypeDependencies( $zobject ) { |
181 | $dependencies = []; |
182 | |
183 | // We need to return dependencies of those objects that build arguments of keys: |
184 | // Types: return the types of its keys |
185 | // Functions: return the types of its arguments |
186 | $content = $zobject->{ ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE }; |
187 | if ( |
188 | is_array( $content ) || |
189 | is_string( $content ) || |
190 | !property_exists( $content, ZTypeRegistry::Z_OBJECT_TYPE ) |
191 | ) { |
192 | return $dependencies; |
193 | } |
194 | |
195 | $type = $content->{ ZTypeRegistry::Z_OBJECT_TYPE }; |
196 | if ( $type === ZTypeRegistry::Z_TYPE ) { |
197 | $keys = $content->{ ZTypeRegistry::Z_TYPE_KEYS }; |
198 | foreach ( array_slice( $keys, 1 ) as $key ) { |
199 | $keyType = $key->{ ZTypeRegistry::Z_KEY_TYPE }; |
200 | if ( is_string( $keyType ) && ( !$this->typeRegistry->isZTypeBuiltIn( $keyType ) ) ) { |
201 | array_push( $dependencies, $keyType ); |
202 | } |
203 | } |
204 | } elseif ( $type === ZTypeRegistry::Z_FUNCTION ) { |
205 | $args = $content->{ ZTypeRegistry::Z_FUNCTION_ARGUMENTS }; |
206 | foreach ( array_slice( $args, 1 ) as $arg ) { |
207 | $argType = $arg->{ ZTypeRegistry::Z_ARGUMENTDECLARATION_TYPE }; |
208 | if ( is_string( $argType ) && ( !$this->typeRegistry->isZTypeBuiltIn( $argType ) ) ) { |
209 | array_push( $dependencies, $argType ); |
210 | } |
211 | } |
212 | } |
213 | |
214 | return array_unique( $dependencies ); |
215 | } |
216 | |
217 | /** |
218 | * @param ApiPageSet|null $resultPageSet |
219 | */ |
220 | private function run( $resultPageSet = null ) { |
221 | $params = $this->extractRequestParams(); |
222 | |
223 | $languages = null; |
224 | $pageResult = null; |
225 | |
226 | $zids = $params[ 'zids' ]; |
227 | $revisions = $params[ 'revisions' ]; |
228 | $language = $params[ 'language' ]; |
229 | $getDependencies = $params[ 'get_dependencies' ]; |
230 | $revisionMap = []; |
231 | |
232 | // Check that if we request revision, we request one per zid |
233 | if ( $revisions ) { |
234 | if ( count( $revisions ) !== count( $zids ) ) { |
235 | $zErrorObject = ZErrorFactory::createZErrorInstance( |
236 | ZErrorTypeRegistry::Z_ERROR_UNKNOWN, |
237 | [ 'message' => "You must specify a revision for each ZID, or none at all." ] |
238 | ); |
239 | $this->dieWithZError( $zErrorObject, 400 ); |
240 | } |
241 | foreach ( $zids as $index => $zid ) { |
242 | $revisionMap[ $zid ] = (int)$revisions[ $index ]; |
243 | } |
244 | } |
245 | |
246 | // Get language fallback chain if language is set |
247 | if ( $language ) { |
248 | $languages = [ $language ]; |
249 | $languages = array_merge( |
250 | $languages, |
251 | $this->languageFallback->getAll( $language, LanguageFallback::MESSAGES ) |
252 | ); |
253 | } |
254 | |
255 | if ( !$resultPageSet ) { |
256 | $pageResult = $this->getResult(); |
257 | } |
258 | |
259 | $fetchedZids = []; |
260 | while ( count( $zids ) > 0 ) { |
261 | $zid = array_shift( $zids ); |
262 | array_push( $fetchedZids, $zid ); |
263 | |
264 | try { |
265 | // We try to fetch the content and transform it according to params |
266 | [ $fetchedContent, $dependencies ] = $this->fetchContent( |
267 | $zid, |
268 | $languages, |
269 | $getDependencies, |
270 | $revisions ? $revisionMap[ $zid ] : null |
271 | ); |
272 | |
273 | // We queue the type dependencies |
274 | foreach ( $dependencies as $dep ) { |
275 | if ( !in_array( $dep, $fetchedZids ) && !in_array( $dep, $zids ) ) { |
276 | array_push( $zids, $dep ); |
277 | } |
278 | } |
279 | |
280 | // We add the fetchedContent to the pageResult |
281 | // TODO (T338249): How to work out the result when using the generator? |
282 | $pageResult->addValue( [ 'query', $this->getModuleName() ], $zid, [ |
283 | 'success' => true, |
284 | 'data' => $fetchedContent |
285 | ] ); |
286 | } catch ( ZErrorException $e ) { |
287 | // If an error was thrown while fetching, we add the value to the response |
288 | // with success=false and the error object as data |
289 | $pageResult->addValue( [ 'query', $this->getModuleName() ], $zid, [ |
290 | 'success' => false, |
291 | 'data' => $e->getZError() |
292 | ] ); |
293 | } |
294 | } |
295 | } |
296 | |
297 | /** |
298 | * @inheritDoc |
299 | * @codeCoverageIgnore |
300 | */ |
301 | protected function getAllowedParams(): array { |
302 | $zObjectStore = WikiLambdaServices::getZObjectStore(); |
303 | |
304 | return [ |
305 | 'zids' => [ |
306 | ParamValidator::PARAM_TYPE => 'string', |
307 | ParamValidator::PARAM_REQUIRED => true, |
308 | ParamValidator::PARAM_ISMULTI => true, |
309 | ], |
310 | 'revisions' => [ |
311 | ParamValidator::PARAM_TYPE => 'string', |
312 | ParamValidator::PARAM_ISMULTI => true, |
313 | ], |
314 | 'language' => [ |
315 | ParamValidator::PARAM_TYPE => $zObjectStore->fetchAllZLanguageCodes(), |
316 | ParamValidator::PARAM_REQUIRED => false, |
317 | ], |
318 | 'get_dependencies' => [ |
319 | ParamValidator::PARAM_TYPE => 'boolean', |
320 | ParamValidator::PARAM_REQUIRED => false, |
321 | ParamValidator::PARAM_DEFAULT => false, |
322 | ], |
323 | ]; |
324 | } |
325 | |
326 | /** |
327 | * @see ApiBase::getExamplesMessages() |
328 | * @return array |
329 | * @codeCoverageIgnore |
330 | */ |
331 | protected function getExamplesMessages() { |
332 | return [ |
333 | 'action=query&format=json&list=wikilambdaload_zobjects&wikilambdaload_zids=Z12%7CZ4' |
334 | => 'apihelp-query+wikilambdaload_zobjects-example-full', |
335 | 'action=query&format=json&list=wikilambdaload_zobjects&wikilambdaload_zids=Z12%7CZ4' |
336 | . '&wikilambdaload_language=es' |
337 | => 'apihelp-query+wikilambdaload_zobjects-example-language', |
338 | 'action=query&format=json&list=wikilambdaload_zobjects&wikilambdaload_zids=Z0123456789%7CZ1' |
339 | => 'apihelp-query+wikilambdaload_zobjects-example-error', |
340 | ]; |
341 | } |
342 | |
343 | /** @inheritDoc */ |
344 | public function setLogger( LoggerInterface $logger ) { |
345 | $this->logger = $logger; |
346 | } |
347 | |
348 | /** @inheritDoc */ |
349 | public function getLogger(): LoggerInterface { |
350 | return $this->logger; |
351 | } |
352 | } |