Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ApiAbstractWikiRunFragment
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
3 / 3
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
4
 getLatestFragmentAndRevalidate
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
3
 isInternal
n/a
0 / 0
n/a
0 / 0
1
 getAllowedParams
n/a
0 / 0
n/a
0 / 0
1
 buildExampleCallFor
n/a
0 / 0
n/a
0 / 0
1
 getExamplesMessages
n/a
0 / 0
n/a
0 / 0
1
1<?php
2/**
3 * WikiLambda Abstract Wiki run fragment 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\ApiMain;
15use MediaWiki\Extension\WikiLambda\AbstractContent\AbstractWikiRequest;
16use MediaWiki\Extension\WikiLambda\AWStorage\AWFragmentStore;
17use MediaWiki\Extension\WikiLambda\HttpStatus;
18use MediaWiki\Extension\WikiLambda\Language\WikifunctionsLanguage;
19use MediaWiki\Extension\WikiLambda\Language\WikifunctionsLanguageFactory;
20use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
21use MediaWiki\Extension\WikiLambda\ZObjectUtils;
22use MediaWiki\Logger\LoggerFactory;
23use Psr\Log\LoggerInterface;
24use Wikimedia\ParamValidator\ParamValidator;
25
26class ApiAbstractWikiRunFragment extends ApiBase {
27
28    private LoggerInterface $logger;
29
30    public function __construct(
31        ApiMain $mainModule,
32        string $moduleName,
33        private readonly WikifunctionsLanguageFactory $wfLanguageFactory,
34        private readonly AWFragmentStore $fragmentStore,
35        private readonly AbstractWikiRequest $abstractWikiRequest
36    ) {
37        parent::__construct( $mainModule, $moduleName, 'abstractwiki_run_fragment_' );
38        // Non-injected items
39        $this->logger = LoggerFactory::getInstance( 'WikiLambdaAbstract' );
40    }
41
42    /**
43     * Gets the latest available HTML fragment rendered from the given
44     * Abstract Content fragment and its arguments.
45     *
46     * Implements Stale-While-Revalidate caching strategy:
47     * * Returns cached HTML fragment for today,
48     * * If fresh fragment is not available in the cache, it queues
49     *   a job to regenerate the fragment with today's date and returns
50     *   whatever is available.
51     *
52     * Implements synchronous or asynchronous behavior depending on the
53     * async flag:
54     * * If called with async=true, the request is expected to respond
55     *   immediately with whatever it has available in the cache. In the
56     *   case that there is no stale content, it returns a pending response
57     *   while the job to regenerate the value is queued.
58     * * If called with async=false, the request is expected to respond
59     *   with the rendered value. In the case in which there is no fresh
60     *   nor stale values cached, it will perform a synchronous call to
61     *   wikifunctions_function_call and wait for its response.
62     *
63     * A successful fragment will contain a 'success' flag set to true,
64     * and the sanitized rendered fragment as 'value':
65     * [
66     *   'success' => true,
67     *   'value' => '<em>sanitized fragment</em>'
68     * ]
69     *
70     * A pending fragment will contain a 'success' flag set to true,
71     * and a 'pending' flag, also set to true:
72     * [
73     *   'success' => true,
74     *   'pending' => true
75     * ]
76     *
77     * A failed fragment will contain a 'success' flag set to false,
78     * the error information under the 'value' key:
79     * [
80     *   'success' => false,
81     *   'value' => [
82     *     'msg' => 'apierror-abstractwiki_run_fragment-returned-zerror',
83     *     'params' => [ 'Z500' ]
84     *     'httpStatusCode' => 400,
85     *     'zerror' => [ ... ],
86     *   ]
87     * ]
88     *
89     * Note that a successfully retrieved fragment that contains a failure
90     * will be returned as a successful HTTP request (200). A failed request
91     * due to API-related reasons (not enabled, bad request, etc.) will exit
92     * with an ApiUsageException.
93     *
94     * @inheritDoc
95     */
96    public function execute() {
97        // Abstract Wiki not enabled: exit with HTTP 501
98        if ( !WikiLambdaServices::getMode()->isAbstract() ) {
99            $this->dieWithError(
100                [ 'apierror-abstractwiki_run_fragment-not-enabled' ],
101                null, null, HttpStatus::NOT_IMPLEMENTED
102            );
103        }
104
105        $params = $this->extractRequestParams();
106
107        $qid = $params[ 'qid' ];
108        $date = $params[ 'date' ];
109        $languageZid = $params[ 'language' ];
110        $fragmentStr = $params[ 'fragment' ];
111        $async = filter_var( $params[ 'async' ], FILTER_VALIDATE_BOOLEAN );
112
113        // Check fragment validity
114        $fragment = json_decode( $fragmentStr, true );
115        if ( $fragment === null || !is_array( $fragment ) ) {
116            $this->dieWithError(
117                [ 'apierror-abstractwiki_run_fragment-bad-fragment' ],
118                null, null, HttpStatus::BAD_REQUEST
119            );
120        }
121
122        $language = $this->wfLanguageFactory->getLanguageFromZid( $languageZid );
123
124        $result = $this->getLatestFragmentAndRevalidate( $fragment, $qid, $language, $date, $async );
125
126        // Set response (fragment might be pending, successful or failed):
127        $pageResult = $this->getResult();
128        $pageResult->addValue( [], $this->getModuleName(), $result );
129    }
130
131    /**
132     * Fetch the AWFragment from AWFragmentStore, and return it if available.
133     * If missing, return pending fragment if called with async=true, or
134     * make a synchronous call to render and sanitize the fragment and
135     * return whatever results of that call.
136     *
137     * @param array $fragment
138     * @param string $qid
139     * @param WikifunctionsLanguage $language
140     * @param string $date
141     * @param bool $async
142     * @return array
143     */
144    private function getLatestFragmentAndRevalidate(
145        array $fragment,
146        string $qid,
147        WikifunctionsLanguage $language,
148        string $date,
149        bool $async
150    ): array {
151        // Get stored fragment (if any)
152        $awFragment = $this->fragmentStore->getRenderedAWFragment(
153            $fragment,
154            $qid,
155            $language,
156            $date,
157        );
158
159        // Stale or fresh, return the payload and be done
160        if ( !$awFragment->isMissing() ) {
161            return $awFragment->getValue();
162        }
163
164        // Missing fragment:
165        // if async=true, return pending value
166        if ( $async ) {
167            return [
168                'success' => true,
169                'pending' => true
170            ];
171        }
172
173        // else, synchronously run and return value
174        return $this->abstractWikiRequest->fetchRenderedAWFragment(
175            $fragment,
176            $qid,
177            $language->getZid(),
178            $date,
179            $awFragment->getKey()
180        );
181    }
182
183    /**
184     * @inheritDoc
185     * @codeCoverageIgnore
186     */
187    public function isInternal() {
188        return true;
189    }
190
191    /**
192     * @inheritDoc
193     * @codeCoverageIgnore
194     */
195    protected function getAllowedParams(): array {
196        return [
197            'qid' => [
198                ParamValidator::PARAM_TYPE => 'string',
199                ParamValidator::PARAM_REQUIRED => true,
200            ],
201            'language' => [
202                ParamValidator::PARAM_TYPE => 'string',
203                ParamValidator::PARAM_REQUIRED => true,
204            ],
205            'date' => [
206                ParamValidator::PARAM_TYPE => 'string',
207                ParamValidator::PARAM_REQUIRED => true,
208            ],
209            'fragment' => [
210                ParamValidator::PARAM_TYPE => 'text',
211                ParamValidator::PARAM_REQUIRED => true,
212            ],
213            'async' => [
214                ParamValidator::PARAM_TYPE => 'boolean',
215                ParamValidator::PARAM_REQUIRED => false,
216                ParamValidator::PARAM_DEFAULT => false
217            ]
218        ];
219    }
220
221    /**
222     * Generates URL-encoded example call to run an abstract fragment
223     *
224     * @param string $qid
225     * @param string $language
226     * @param string $date
227     * @param string $fragmentFile
228     * @return string URL-encoded contents
229     * @codeCoverageIgnore
230     */
231    private function buildExampleCallFor( $qid, $language, $date, $fragmentFile ): string {
232        $fragment = ZObjectUtils::readTestFile( 'abstract/' . $fragmentFile );
233        return 'action=abstractwiki_run_fragment&'
234            . 'abstractwiki_run_fragment_qid=' . $qid . '&'
235            . 'abstractwiki_run_fragment_language=' . $language . '&'
236            . 'abstractwiki_run_fragment_date=' . $date . '&'
237            . 'abstractwiki_run_fragment_fragment=' . urlencode( $fragment );
238    }
239
240    /**
241     * @inheritDoc
242     * @codeCoverageIgnore
243     */
244    protected function getExamplesMessages(): array {
245        return [
246            // Run an Abstract Wiki content fragment that contains a simple literal HTML
247            $this->buildExampleCallFor( 'Q319', 'Z1002', '26-7-2023', 'fragment-literal-html.json' )
248                => 'apihelp-abstractwiki_run_fragment-example-literal-html',
249
250            // Run an Abstract Wiki content fragment that contains a composition with arguments
251            $this->buildExampleCallFor( 'Q319', 'Z1002', '26-7-2023', 'fragment-with-args.json' )
252                => 'apihelp-abstractwiki_run_fragment-example-composition'
253        ];
254    }
255}