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