Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
FetchHandler
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 7
1190
0.00% covered (danger)
0.00%
0 / 1
 run
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
210
 getTypeDependencies
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
156
 applyCacheControl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParamSettings
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
2
 dieRESTfullyWithZError
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 dieRESTfully
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
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
11namespace MediaWiki\Extension\WikiLambda\RESTAPI;
12
13use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
14use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
15use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
16use MediaWiki\Extension\WikiLambda\ZErrorException;
17use MediaWiki\Extension\WikiLambda\ZErrorFactory;
18use MediaWiki\Extension\WikiLambda\ZObjectContentHandler;
19use MediaWiki\Extension\WikiLambda\ZObjects\ZError;
20use MediaWiki\Extension\WikiLambda\ZObjectUtils;
21use MediaWiki\Logger\LoggerFactory;
22use MediaWiki\Rest\LocalizedHttpException;
23use MediaWiki\Rest\ResponseInterface;
24use MediaWiki\Rest\SimpleHandler;
25use MediaWiki\Title\Title;
26use Psr\Log\LoggerInterface;
27use Wikimedia\Message\MessageValue;
28use 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 */
34class 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}