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