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