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 ) { |
| 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 ) { |
| 189 | } |
| 190 | } |
| 191 | self::batchGetMathML( $rbis, $multiHttpClient ); |
| 192 | } |
| 193 | |
| 194 | private function getMultiHttpClient(): MultiHttpClient { |
| 195 | global $wgMathHTTPProxy, $wgMathConcurrentReqs; |
| 196 | $multiHttpClient = MediaWikiServices::getInstance()->getHttpRequestFactory()->createMultiClient( |
| 197 | [ 'maxConnsPerHost' => $wgMathConcurrentReqs, 'proxy' => $wgMathHTTPProxy ] ); |
| 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 | } |