Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.29% covered (danger)
47.29%
61 / 129
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
FetchHandler
47.29% covered (danger)
47.29%
61 / 129
0.00% covered (danger)
0.00%
0 / 5
192.51
0.00% covered (danger)
0.00%
0 / 1
 run
41.33% covered (danger)
41.33%
31 / 75
0.00% covered (danger)
0.00%
0 / 1
60.43
 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
96.77% covered (success)
96.77%
30 / 31
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\HttpStatus;
14use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
15use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
16use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
17use MediaWiki\Extension\WikiLambda\ZErrorException;
18use MediaWiki\Extension\WikiLambda\ZErrorFactory;
19use MediaWiki\Extension\WikiLambda\ZObjectContentHandler;
20use MediaWiki\Extension\WikiLambda\ZObjectUtils;
21use MediaWiki\Logger\LoggerFactory;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Rest\ResponseInterface;
24use MediaWiki\Title\Title;
25use Wikimedia\ParamValidator\ParamValidator;
26use Wikimedia\Telemetry\SpanInterface;
27
28/**
29 * Simple REST API to fetch the latest versions of one or more ZObjects
30 * via GET /wikifunctions/v0/fetch/{zids}
31 */
32class FetchHandler extends WikiLambdaRESTHandler {
33
34    public const MAX_REQUESTED_ZIDS = 50;
35    private ZTypeRegistry $typeRegistry;
36
37    /** @inheritDoc */
38    public function run( $ZIDs, $revisions = [] ) {
39        // This API is only availble on the repo installation, not client wikis
40        if ( !MediaWikiServices::getInstance()->getMainConfig()->get( 'WikiLambdaEnableRepoMode' ) ) {
41            $this->dieRESTfully( 'wikilambda-restapi-disabled-repo-mode-only', [], HttpStatus::BAD_REQUEST );
42        }
43
44        $this->typeRegistry = ZTypeRegistry::singleton();
45        $this->logger = LoggerFactory::getInstance( 'WikiLambda' );
46
47        $tracer = MediaWikiServices::getInstance()->getTracer();
48        $span = $tracer->createSpan( 'WikiLambda FetchHandler' )
49            ->setSpanKind( SpanInterface::SPAN_KIND_CLIENT )
50            ->start();
51        $span->activate();
52
53        $responseList = [];
54
55        // (T365728) Make this method call separately, so the null operator below isn't confused
56        $params = $this->getRequest()->getQueryParams();
57
58        // (T391046) Parse as null if not specified.
59        $language = $params['language'] ?? null;
60        // (T391046) Fallback to not recurse through dependencies if not specified.
61        $getDependencies = $params['getDependencies'] ?? false;
62
63        if ( count( $revisions ) > 0 && ( count( $revisions ) !== count( $ZIDs ) ) ) {
64            $errorMessage = "You must specify a revision for each ZID, or none at all.";
65            $zErrorObject = ZErrorFactory::createZErrorInstance(
66                ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
67                [
68                    'message' => $errorMessage
69                ]
70            );
71            $this->dieRESTfullyWithZError( $zErrorObject, HttpStatus::BAD_REQUEST );
72        }
73
74        $reqSize = count( $ZIDs );
75        if ( $reqSize > self::MAX_REQUESTED_ZIDS ) {
76            $this->dieRESTfully(
77                'wikilambda-restapi-fetch-too-many',
78                [ $reqSize, self::MAX_REQUESTED_ZIDS ],
79                HttpStatus::FORBIDDEN
80            );
81        }
82
83        $extraDependencies = [];
84
85        foreach ( $ZIDs as $index => $ZID ) {
86            if ( !ZObjectUtils::isValidZObjectReference( mb_strtoupper( $ZID ) ) ) {
87                $zErrorObject = ZErrorFactory::createZErrorInstance(
88                    ZErrorTypeRegistry::Z_ERROR_INVALID_REFERENCE,
89                    [ 'data' => $ZID ]
90                );
91                $this->dieRESTfullyWithZError( $zErrorObject, HttpStatus::NOT_FOUND );
92            } else {
93                $title = Title::newFromText( $ZID, NS_MAIN );
94
95                if ( !$title || !( $title instanceof Title ) || !$title->exists() ) {
96                    $zErrorObject = ZErrorFactory::createZErrorInstance(
97                        ZErrorTypeRegistry::Z_ERROR_UNKNOWN_REFERENCE,
98                        [ 'data' => $ZID ]
99                    );
100                    $this->dieRESTfullyWithZError( $zErrorObject, HttpStatus::NOT_FOUND );
101                } else {
102                    $revision = $revisions[$index] ?? null;
103
104                    try {
105                        $fetchedContent = ZObjectContentHandler::getExternalRepresentation(
106                            $title, $language, $revision
107                        );
108                        $span->setSpanStatus( SpanInterface::SPAN_STATUS_OK );
109                    } catch ( ZErrorException $error ) {
110                        $span->setSpanStatus( SpanInterface::SPAN_STATUS_ERROR )
111                            ->setAttributes( [
112                                'response.status_code' => 404,
113                                'exception.message' => $error->getMessage()
114                            ] );
115                        // This probably means that the requested revision is not known; return
116                        // null for this entry rather than throwing or returning a ZError instance.
117                        $this->dieRESTfully(
118                            'wikilambda-restapi-revision-mismatch',
119                            [ $revision, $ZID ],
120                            HttpStatus::NOT_FOUND );
121                    } finally {
122                        $span->end();
123                    }
124
125                    $responseList[ $ZID ] = $fetchedContent ?? '';
126
127                    if ( $getDependencies ) {
128                        $dependencies = $this->getTypeDependencies( json_decode( $fetchedContent ?? '' ) ?? [] );
129                        foreach ( $dependencies as $_key => $dep ) {
130                            if ( in_array( $dep, $ZIDs ) ) {
131                                continue;
132                            }
133                            $extraDependencies[] = $dep;
134                        }
135                    }
136                }
137            }
138        }
139
140        // We use array_unique to de-duplicate dependencies if they're used multiple times
141        foreach ( array_unique( $extraDependencies ) as $_key => $ZID ) {
142            $responseList[$ZID] = ZObjectContentHandler::getExternalRepresentation(
143                Title::newFromText( $ZID, NS_MAIN ),
144                $language,
145                // Get latest, as we have no revision to request.
146                null
147            );
148        }
149
150        $response = $this->getResponseFactory()->createJson( $responseList );
151        return $response;
152    }
153
154    /**
155     * Returns the types of type keys and function arguments
156     *
157     * @param \stdClass $zobject
158     * @return array
159     */
160    private function getTypeDependencies( $zobject ) {
161        $dependencies = [];
162
163        // We need to return dependencies of those objects that build arguments of keys:
164        // Types: return the types of its keys
165        // Functions: return the types of its arguments
166        $content = $zobject->{ ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE };
167        if (
168            is_array( $content ) ||
169            is_string( $content ) ||
170            !property_exists( $content, ZTypeRegistry::Z_OBJECT_TYPE )
171        ) {
172            return $dependencies;
173        }
174
175        $type = $content->{ ZTypeRegistry::Z_OBJECT_TYPE };
176        if ( $type === ZTypeRegistry::Z_TYPE ) {
177            $keys = $content->{ ZTypeRegistry::Z_TYPE_KEYS };
178            foreach ( array_slice( $keys, 1 ) as $key ) {
179                $keyType = $key->{ ZTypeRegistry::Z_KEY_TYPE };
180                if ( is_string( $keyType ) && ( !$this->typeRegistry->isZTypeBuiltIn( $keyType ) ) ) {
181                    array_push( $dependencies, $keyType );
182                }
183            }
184        } elseif ( $type === ZTypeRegistry::Z_FUNCTION ) {
185            $args = $content->{ ZTypeRegistry::Z_FUNCTION_ARGUMENTS };
186            foreach ( array_slice( $args, 1 ) as $arg ) {
187                $argType = $arg->{ ZTypeRegistry::Z_ARGUMENTDECLARATION_TYPE };
188                if ( is_string( $argType ) && ( !$this->typeRegistry->isZTypeBuiltIn( $argType ) ) ) {
189                    array_push( $dependencies, $argType );
190                }
191            }
192        }
193
194        return array_unique( $dependencies );
195    }
196
197    public function applyCacheControl( ResponseInterface $response ) {
198        if ( $response->getStatusCode() >= 200 && $response->getStatusCode() < 400 ) {
199            $response->setHeader( 'Cache-Control', 'public,must-revalidate,s-maxage=' . 60 * 60 * 24 );
200        }
201    }
202
203    /** @inheritDoc */
204    public function needsWriteAccess() {
205        return false;
206    }
207
208    /** @inheritDoc */
209    public function getParamSettings() {
210        $zObjectStore = WikiLambdaServices::getZObjectStore();
211
212        // Don't try to read the supported languages from the DB on client wikis, we can't.
213        $supportedLanguageCodes =
214            ( MediaWikiServices::getInstance()->getMainConfig()->get( 'WikiLambdaEnableRepoMode' ) ) ?
215                $zObjectStore->fetchAllZLanguageCodes() :
216                [];
217
218        return [
219            'zids' => [
220                self::PARAM_SOURCE => 'path',
221                ParamValidator::PARAM_TYPE => 'string',
222                ParamValidator::PARAM_ISMULTI => true,
223                ParamValidator::PARAM_REQUIRED => true,
224            ],
225            'revisions' => [
226                self::PARAM_SOURCE => 'path',
227                ParamValidator::PARAM_TYPE => 'string',
228                ParamValidator::PARAM_ISMULTI => true,
229                ParamValidator::PARAM_REQUIRED => false,
230            ],
231            'language' => [
232                self::PARAM_SOURCE => 'query',
233                ParamValidator::PARAM_TYPE => $supportedLanguageCodes,
234                ParamValidator::PARAM_DEFAULT => null,
235                ParamValidator::PARAM_REQUIRED => false,
236            ],
237            'getDependencies' => [
238                self::PARAM_SOURCE => 'query',
239                ParamValidator::PARAM_TYPE => 'boolean',
240                ParamValidator::PARAM_DEFAULT => false,
241                ParamValidator::PARAM_REQUIRED => false,
242            ],
243        ];
244    }
245}