Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.64% covered (warning)
82.64%
100 / 121
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryZObjects
82.64% covered (warning)
82.64%
100 / 121
37.50% covered (danger)
37.50%
3 / 8
49.79
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 execute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 executeGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 dieWithZError
38.89% covered (danger)
38.89%
7 / 18
0.00% covered (danger)
0.00%
0 / 1
2.91
 fetchContent
76.67% covered (warning)
76.67%
23 / 30
0.00% covered (danger)
0.00%
0 / 1
8.81
 getTypeDependencies
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 run
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
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% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
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
11namespace MediaWiki\Extension\WikiLambda\ActionAPI;
12
13use ApiBase;
14use ApiPageSet;
15use ApiQueryGeneratorBase;
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\ZObjects\ZError;
24use MediaWiki\Extension\WikiLambda\ZObjectUtils;
25use MediaWiki\Languages\LanguageFallback;
26use MediaWiki\Languages\LanguageNameUtils;
27use MediaWiki\Logger\LoggerFactory;
28use MediaWiki\Title\TitleFactory;
29use Psr\Log\LoggerAwareInterface;
30use Psr\Log\LoggerInterface;
31use stdClass;
32use Wikimedia\ParamValidator\ParamValidator;
33
34class 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}