Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
77.22% |
122 / 158 |
|
71.43% |
20 / 28 |
CRAP | |
0.00% |
0 / 1 |
MathRestbaseInterface | |
77.22% |
122 / 158 |
|
71.43% |
20 / 28 |
97.79 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
batchGetMathML | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
6.04 | |||
setPurge | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMathML | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getContent | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
calculateHash | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
checkTeX | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
executeRestbaseCheckRequest | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
batchEvaluate | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
5.06 | |||
getMultiHttpClient | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getUrl | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getSvg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkConfig | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
getFullSvgUrl | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getCheckedTex | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSuccess | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getIdentifiers | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getError | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTex | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setErrorMessage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getWarnings | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCheckRequest | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
evaluateRestbaseCheckResponse | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
7 | |||
getMathoidStyle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getContentRequest | |
63.64% |
7 / 11 |
|
0.00% |
0 / 1 |
2.19 | |||
evaluateContentResponse | |
25.00% |
3 / 12 |
|
0.00% |
0 / 1 |
6.80 | |||
throwContentError | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | /** |
3 | * MediaWiki math extension |
4 | * |
5 | * @copyright 2002-2015 various MediaWiki contributors |
6 | * @license GPL-2.0-or-later |
7 | */ |
8 | |
9 | namespace MediaWiki\Extension\Math; |
10 | |
11 | use Exception; |
12 | use MediaWiki\Logger\LoggerFactory; |
13 | use MediaWiki\MediaWikiServices; |
14 | use Psr\Log\LoggerInterface; |
15 | use stdClass; |
16 | |
17 | class MathRestbaseInterface { |
18 | /** @var string|false */ |
19 | private $hash = false; |
20 | /** @var string */ |
21 | private $tex; |
22 | /** @var string */ |
23 | private $type; |
24 | private ?string $checkedTex = null; |
25 | /** @var bool|null */ |
26 | private $success; |
27 | /** @var array */ |
28 | private $identifiers; |
29 | /** @var stdClass|null */ |
30 | private $error; |
31 | /** @var string|null */ |
32 | private $mathoidStyle; |
33 | /** @var string|null */ |
34 | private $mml; |
35 | /** @var array */ |
36 | private $warnings = []; |
37 | /** @var bool is there a request to purge the existing mathematical content */ |
38 | private $purge = false; |
39 | /** @var LoggerInterface */ |
40 | private $logger; |
41 | |
42 | /** |
43 | * @param string $tex |
44 | * @param string $type |
45 | */ |
46 | public function __construct( $tex = '', $type = 'tex' ) { |
47 | $this->tex = $tex; |
48 | $this->type = $type; |
49 | $this->logger = LoggerFactory::getInstance( 'Math' ); |
50 | } |
51 | |
52 | /** |
53 | * Bundles several requests for fetching MathML. |
54 | * Does not send requests, if the input TeX is invalid. |
55 | * @param MathRestbaseInterface[] $rbis |
56 | * @param \MultiHttpClient $multiHttpClient |
57 | */ |
58 | private static function batchGetMathML( array $rbis, \MultiHttpClient $multiHttpClient ) { |
59 | $requests = []; |
60 | $skips = []; |
61 | $i = 0; |
62 | foreach ( $rbis as $rbi ) { |
63 | /** @var MathRestbaseInterface $rbi */ |
64 | if ( $rbi->getSuccess() ) { |
65 | $requests[] = $rbi->getContentRequest( 'mml' ); |
66 | } else { |
67 | $skips[] = $i; |
68 | } |
69 | $i++; |
70 | } |
71 | $results = $multiHttpClient->runMulti( $requests ); |
72 | $lenRbis = count( $rbis ); |
73 | $j = 0; |
74 | for ( $i = 0; $i < $lenRbis; $i++ ) { |
75 | if ( !in_array( $i, $skips, true ) ) { |
76 | /** @var MathRestbaseInterface $rbi */ |
77 | $rbi = $rbis[$i]; |
78 | try { |
79 | $response = $results[ $j ][ 'response' ]; |
80 | $mml = $rbi->evaluateContentResponse( 'mml', $response, $requests[$j] ); |
81 | $rbi->mml = $mml; |
82 | } catch ( MathRestbaseException $e ) { |
83 | // FIXME: Why is this silenced? Doesn't this leave invalid data behind? |
84 | } |
85 | $j++; |
86 | } |
87 | } |
88 | } |
89 | |
90 | /** |
91 | * Lets this instance know if this is a purge request. When set to true, |
92 | * it will cause the object to issue the first content request with a |
93 | * 'Cache-Control: no-cache' header to prompt the regeneration of the |
94 | * renders. |
95 | * |
96 | * @param bool $purge whether this is a purge request |
97 | */ |
98 | public function setPurge( $purge = true ) { |
99 | $this->purge = $purge; |
100 | } |
101 | |
102 | /** |
103 | * @return string MathML code |
104 | * @throws MathRestbaseException |
105 | */ |
106 | public function getMathML() { |
107 | if ( !$this->mml ) { |
108 | $this->mml = $this->getContent( 'mml' ); |
109 | } |
110 | return $this->mml; |
111 | } |
112 | |
113 | /** |
114 | * @param string $type |
115 | * @return string |
116 | * @throws MathRestbaseException |
117 | */ |
118 | private function getContent( $type ) { |
119 | $request = $this->getContentRequest( $type ); |
120 | $multiHttpClient = $this->getMultiHttpClient(); |
121 | $response = $multiHttpClient->run( $request ); |
122 | return $this->evaluateContentResponse( $type, $response, $request ); |
123 | } |
124 | |
125 | /** |
126 | * @throws InvalidTeXException |
127 | */ |
128 | private function calculateHash() { |
129 | if ( !$this->hash ) { |
130 | if ( !$this->checkTeX() ) { |
131 | throw new InvalidTeXException( "TeX input is invalid." ); |
132 | } |
133 | } |
134 | } |
135 | |
136 | public function checkTeX() { |
137 | $request = $this->getCheckRequest(); |
138 | $requestResult = $this->executeRestbaseCheckRequest( $request ); |
139 | return $this->evaluateRestbaseCheckResponse( $requestResult ); |
140 | } |
141 | |
142 | /** |
143 | * Performs a service request |
144 | * Generates error messages on failure |
145 | * @see MediaWiki\Http\HttpRequestFactory::post() |
146 | * |
147 | * @param array $request |
148 | * @return array |
149 | */ |
150 | private function executeRestbaseCheckRequest( $request ) { |
151 | $multiHttpClient = $this->getMultiHttpClient(); |
152 | $response = $multiHttpClient->run( $request ); |
153 | if ( $response['code'] !== 200 ) { |
154 | $this->logger->info( 'Tex check failed', [ |
155 | 'post' => $request['body'], |
156 | 'error' => $response['error'], |
157 | 'urlparams' => $request['url'] |
158 | ] ); |
159 | } |
160 | return $response; |
161 | } |
162 | |
163 | /** |
164 | * @param MathRestbaseInterface[] $rbis |
165 | */ |
166 | public static function batchEvaluate( array $rbis ) { |
167 | if ( count( $rbis ) == 0 ) { |
168 | return; |
169 | } |
170 | $requests = []; |
171 | /** @var MathRestbaseInterface $first */ |
172 | $first = $rbis[0]; |
173 | $multiHttpClient = $first->getMultiHttpClient(); |
174 | foreach ( $rbis as $rbi ) { |
175 | /** @var MathRestbaseInterface $rbi */ |
176 | $requests[] = $rbi->getCheckRequest(); |
177 | } |
178 | $results = $multiHttpClient->runMulti( $requests ); |
179 | $i = 0; |
180 | foreach ( $results as $requestResponse ) { |
181 | /** @var MathRestbaseInterface $rbi */ |
182 | $rbi = $rbis[$i++]; |
183 | try { |
184 | $response = $requestResponse[ 'response' ]; |
185 | $rbi->evaluateRestbaseCheckResponse( $response ); |
186 | } catch ( Exception $e ) { |
187 | } |
188 | } |
189 | self::batchGetMathML( $rbis, $multiHttpClient ); |
190 | } |
191 | |
192 | private function getMultiHttpClient() { |
193 | global $wgMathConcurrentReqs; |
194 | $multiHttpClient = MediaWikiServices::getInstance()->getHttpRequestFactory()->createMultiClient( |
195 | [ 'maxConnsPerHost' => $wgMathConcurrentReqs ] ); |
196 | |
197 | return $multiHttpClient; |
198 | } |
199 | |
200 | /** |
201 | * The URL is generated according to the following logic: |
202 | * |
203 | * Case A: <code>$internal = false</code>, which means one needs a URL that is accessible from |
204 | * outside: |
205 | * |
206 | * --> Use <code>$wgMathFullRestbaseURL</code>. It must always be configured. |
207 | * |
208 | * Case B: <code>$internal = true</code>, which means one needs to access content from Restbase |
209 | * which does not need to be accessible from outside: |
210 | * |
211 | * --> Use the mount point when it is available and <code>$wgMathUseInternalRestbasePath = |
212 | * true</code>. If not, use <code>$wgMathFullRestbaseURL</code>. |
213 | * |
214 | * @param string $path |
215 | * @param bool|true $internal |
216 | * @return string |
217 | */ |
218 | public function getUrl( $path, $internal = true ) { |
219 | global $wgMathInternalRestbaseURL, $wgMathFullRestbaseURL; |
220 | if ( $internal ) { |
221 | return "{$wgMathInternalRestbaseURL}v1/$path"; |
222 | } else { |
223 | return "{$wgMathFullRestbaseURL}v1/$path"; |
224 | } |
225 | } |
226 | |
227 | /** |
228 | * @return string |
229 | * @throws MathRestbaseException |
230 | */ |
231 | public function getSvg() { |
232 | return $this->getContent( 'svg' ); |
233 | } |
234 | |
235 | /** |
236 | * Generates a unique TeX string, renders it and gets it via a public URL. |
237 | * The method fails, if the public URL does not point to the same server, who did render |
238 | * the unique TeX input in the first place. |
239 | * @return bool |
240 | */ |
241 | private function checkConfig() { |
242 | // Generates a TeX string that probably has not been generated before |
243 | $uniqueTeX = uniqid( 't=', true ); |
244 | $testInterface = new MathRestbaseInterface( $uniqueTeX ); |
245 | if ( !$testInterface->checkTeX() ) { |
246 | $this->logger->warning( 'Config check failed, since test expression was considered as invalid.', |
247 | [ 'uniqueTeX' => $uniqueTeX ] ); |
248 | return false; |
249 | } |
250 | |
251 | try { |
252 | $url = $testInterface->getFullSvgUrl(); |
253 | $req = MediaWikiServices::getInstance()->getHttpRequestFactory()->create( $url, [], __METHOD__ ); |
254 | $status = $req->execute(); |
255 | if ( $status->isOK() ) { |
256 | return true; |
257 | } |
258 | |
259 | $this->logger->warning( 'Config check failed, due to an invalid response code.', |
260 | [ 'responseCode' => $status ] ); |
261 | } catch ( Exception $e ) { |
262 | $this->logger->warning( 'Config check failed, due to an exception.', [ $e ] ); |
263 | } |
264 | |
265 | return false; |
266 | } |
267 | |
268 | /** |
269 | * Gets a publicly accessible link to the generated SVG image. |
270 | * @return string |
271 | * @throws InvalidTeXException |
272 | */ |
273 | public function getFullSvgUrl() { |
274 | $this->calculateHash(); |
275 | return $this->getUrl( "media/math/render/svg/{$this->hash}", false ); |
276 | } |
277 | |
278 | public function getCheckedTex(): ?string { |
279 | return $this->checkedTex; |
280 | } |
281 | |
282 | public function getSuccess(): bool { |
283 | if ( $this->success === null ) { |
284 | $this->checkTeX(); |
285 | } |
286 | return $this->success; |
287 | } |
288 | |
289 | public function getIdentifiers(): ?array { |
290 | return $this->identifiers; |
291 | } |
292 | |
293 | public function getError(): ?stdClass { |
294 | return $this->error; |
295 | } |
296 | |
297 | public function getTex(): string { |
298 | return $this->tex; |
299 | } |
300 | |
301 | public function getType(): string { |
302 | return $this->type; |
303 | } |
304 | |
305 | private function setErrorMessage( $msg ) { |
306 | $this->error = (object)[ 'error' => (object)[ 'message' => $msg ] ]; |
307 | } |
308 | |
309 | public function getWarnings(): array { |
310 | return $this->warnings; |
311 | } |
312 | |
313 | public function getCheckRequest(): array { |
314 | return [ |
315 | 'method' => 'POST', |
316 | 'body' => [ |
317 | 'type' => $this->type, |
318 | 'q' => $this->tex |
319 | ], |
320 | 'url' => $this->getUrl( "media/math/check/{$this->type}" ) |
321 | ]; |
322 | } |
323 | |
324 | public function evaluateRestbaseCheckResponse( array $response ): bool { |
325 | $json = json_decode( $response['body'] ); |
326 | if ( $response['code'] === 200 && |
327 | isset( $json->success ) && |
328 | isset( $json->checked ) && |
329 | isset( $json->identifiers ) ) { |
330 | $headers = $response['headers']; |
331 | $this->hash = $headers['x-resource-location']; |
332 | $this->success = $json->success; |
333 | $this->checkedTex = $json->checked; |
334 | $this->identifiers = $json->identifiers; |
335 | if ( isset( $json->warnings ) ) { |
336 | $this->warnings = $json->warnings; |
337 | } |
338 | return true; |
339 | } |
340 | if ( isset( $json->detail->success ) ) { |
341 | $this->success = $json->detail->success; |
342 | $this->error = $json->detail; |
343 | return false; |
344 | } |
345 | $this->success = false; |
346 | $this->setErrorMessage( 'Math extension cannot connect to Restbase.' ); |
347 | $this->logger->error( 'Received invalid response from restbase.', [ |
348 | 'body' => $response['body'], |
349 | 'code' => $response['code'] ] ); |
350 | return false; |
351 | } |
352 | |
353 | public function getMathoidStyle(): ?string { |
354 | return $this->mathoidStyle; |
355 | } |
356 | |
357 | /** |
358 | * @param string $type |
359 | * @return array |
360 | * @throws InvalidTeXException |
361 | */ |
362 | private function getContentRequest( $type ) { |
363 | $this->calculateHash(); |
364 | $request = [ |
365 | 'method' => 'GET', |
366 | 'url' => $this->getUrl( "media/math/render/$type/{$this->hash}" ) |
367 | ]; |
368 | if ( $this->purge ) { |
369 | $request['headers'] = [ |
370 | 'Cache-Control' => 'no-cache' |
371 | ]; |
372 | $this->purge = false; |
373 | } |
374 | return $request; |
375 | } |
376 | |
377 | /** |
378 | * @param string $type |
379 | * @param array $response |
380 | * @param array $request |
381 | * @return string |
382 | * @throws MathRestbaseException |
383 | */ |
384 | private function evaluateContentResponse( $type, array $response, array $request ) { |
385 | if ( $response['code'] === 200 ) { |
386 | if ( array_key_exists( 'x-mathoid-style', $response['headers'] ) ) { |
387 | $this->mathoidStyle = $response['headers']['x-mathoid-style']; |
388 | } |
389 | return $response['body']; |
390 | } |
391 | // Remove "convenience" duplicate keys put in place by MultiHttpClient |
392 | unset( $response[0], $response[1], $response[2], $response[3], $response[4] ); |
393 | $this->logger->error( 'Restbase math server problem', [ |
394 | 'urlparams' => $request['url'], |
395 | 'response' => [ 'code' => $response['code'], 'body' => $response['body'] ], |
396 | 'math_type' => $type, |
397 | 'tex' => $this->tex |
398 | ] ); |
399 | self::throwContentError( $type, $response['body'] ); |
400 | } |
401 | |
402 | /** |
403 | * @param string $type |
404 | * @param string $body |
405 | * @throws MathRestbaseException |
406 | * @return never |
407 | */ |
408 | public static function throwContentError( $type, $body ) { |
409 | $detail = 'Server problem.'; |
410 | $json = json_decode( $body ); |
411 | if ( isset( $json->detail ) ) { |
412 | if ( is_array( $json->detail ) ) { |
413 | $detail = $json->detail[0]; |
414 | } elseif ( is_string( $json->detail ) ) { |
415 | $detail = $json->detail; |
416 | } |
417 | } |
418 | throw new MathRestbaseException( "Cannot get $type. $detail" ); |
419 | } |
420 | } |