mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Math
synced 2024-11-23 23:25:02 +00:00
Add Restbase interface
* Add a simple interface to Restbase that currently does not do anything. Change-Id: I4c2ef329b3954fd35276a5e270d0dc3fcacabf7b
This commit is contained in:
parent
b292ba512e
commit
6ef2e56439
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,3 +7,5 @@
|
|||
tests/browser/.bundle
|
||||
tests/browser/.gem
|
||||
node_modules/
|
||||
vendor
|
||||
composer.lock
|
||||
|
|
|
@ -21,14 +21,14 @@ class MathHooks {
|
|||
}
|
||||
}
|
||||
$invDefs = array_flip( $defs );
|
||||
if ( is_int( $value ) ){
|
||||
if ( is_int( $value ) ) {
|
||||
if ( array_key_exists( $value, $invDefs ) ) {
|
||||
$value = $invDefs[$value];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
if ( is_string( $value ) ){
|
||||
if ( is_string( $value ) ) {
|
||||
$newValues = array();
|
||||
foreach ( $defs as $k => $v ) {
|
||||
$newValues[$k] = preg_replace_callback( '/_(.)/', function ( $matches ) {
|
||||
|
@ -37,7 +37,7 @@ class MathHooks {
|
|||
}
|
||||
if ( array_key_exists( $value, $defs ) ) {
|
||||
return $newValues[$value];
|
||||
} elseif ( in_array( $value, $newValues ) ){
|
||||
} elseif ( in_array( $value, $newValues ) ) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ class MathHooks {
|
|||
'mathml' => 5,
|
||||
'latexml'=> 7 );
|
||||
|
||||
if ( array_key_exists( $mode, $defs ) ){
|
||||
if ( array_key_exists( $mode, $defs ) ) {
|
||||
return $defs[$mode];
|
||||
} else {
|
||||
return $default;
|
||||
|
@ -106,7 +106,7 @@ class MathHooks {
|
|||
|
||||
// To be independent of the MediaWiki core version,
|
||||
// we check if the core caching logic for math is still available.
|
||||
if ( ! is_callable( 'ParserOptions::getMath' ) && in_array( 'math', $forOptions ) ) {
|
||||
if ( !is_callable( 'ParserOptions::getMath' ) && in_array( 'math', $forOptions ) ) {
|
||||
if ( $user === false ) {
|
||||
$user = $wgUser;
|
||||
}
|
||||
|
@ -258,8 +258,8 @@ class MathHooks {
|
|||
*/
|
||||
public static function getMathNames() {
|
||||
$names = array();
|
||||
foreach ( MathRenderer::getValidModes() as $mode ) {
|
||||
$names[ $mode ] = wfMessage( 'mw_math_' . $mode )->escaped();
|
||||
foreach ( MathRenderer::getValidModes() as $mode ) {
|
||||
$names[$mode] = wfMessage( 'mw_math_' . $mode )->escaped();
|
||||
}
|
||||
|
||||
return $names;
|
||||
|
@ -306,7 +306,7 @@ class MathHooks {
|
|||
if ( in_array( $type, array( 'mysql', 'sqlite', 'postgres' ) ) ) {
|
||||
$sql = __DIR__ . '/db/mathlatexml.' . $type . '.sql';
|
||||
$updater->addExtensionTable( 'mathlatexml', $sql );
|
||||
if ( $type == 'mysql' ){
|
||||
if ( $type == 'mysql' ) {
|
||||
$sql = __DIR__ . '/db/patches/mathlatexml.mathml-length-adjustment.mysql.sql';
|
||||
$updater->modifyExtensionField( 'mathlatexml', 'math_mathml', $sql );
|
||||
}
|
||||
|
|
290
MathRestbaseInterface.php
Normal file
290
MathRestbaseInterface.php
Normal file
|
@ -0,0 +1,290 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki math extension
|
||||
*
|
||||
* (c) 2002-2015 various MediaWiki contributors
|
||||
* GPLv2 license; info in main package.
|
||||
*/
|
||||
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
|
||||
class MathRestbaseInterface {
|
||||
private $hash = false;
|
||||
private $tex;
|
||||
private $type;
|
||||
private $checkedTex;
|
||||
private $success;
|
||||
private $identifiers;
|
||||
private $error;
|
||||
|
||||
/**
|
||||
* MathRestbaseInterface constructor.
|
||||
* @param string $tex
|
||||
* @param bool $displayStyle
|
||||
*/
|
||||
public function __construct( $tex = '', $displayStyle = true ) {
|
||||
$this->tex = $tex;
|
||||
if ( $displayStyle ) {
|
||||
$this->type = 'tex';
|
||||
} else {
|
||||
$this->type = 'inline-tex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string MathML code
|
||||
* @throws MWException
|
||||
*/
|
||||
public function getMathML() {
|
||||
return $this->getContent( 'mml' );
|
||||
}
|
||||
|
||||
private function getContent( $type ) {
|
||||
$this->calculateHash();
|
||||
$request = array(
|
||||
'method' => 'GET',
|
||||
'url' => self::getUrl( "media/math/render/$type/{$this->hash}" )
|
||||
);
|
||||
$serviceClient = $this->getServiceClient();
|
||||
$response = $serviceClient->run( $request );
|
||||
if ( $response['code'] === 200 ) {
|
||||
return $response['body'];
|
||||
}
|
||||
$this->log()->error( 'Restbase math server problem:', array(
|
||||
'request' => $request,
|
||||
'response' => $response,
|
||||
'type' => $type,
|
||||
'tex' => $this->tex
|
||||
) );
|
||||
throw new MWException( "Cannot get $type. Server problem." );
|
||||
}
|
||||
|
||||
private function calculateHash() {
|
||||
if ( !$this->hash ) {
|
||||
if ( !$this->checkTeX() ) {
|
||||
throw new MWException( "TeX input is invalid." );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkTeX() {
|
||||
$postData = array(
|
||||
'type' => $this->type,
|
||||
'q' => $this->tex
|
||||
);
|
||||
$requestResult = $this->makeRestbaseCheckRequest( $postData, $res );
|
||||
$json = json_decode( $res );
|
||||
if ( $requestResult ) {
|
||||
$this->success = $json->success;
|
||||
$this->checkedTex = $json->checked;
|
||||
$this->identifiers = $json->identifiers;
|
||||
return true;
|
||||
} else {
|
||||
$this->success = $json->detail->success;
|
||||
$this->error = $json->detail;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a service request
|
||||
* Generates error messages on failure
|
||||
* @see Http::post()
|
||||
*
|
||||
* @param string $post the encoded post request
|
||||
* @param mixed $res the result
|
||||
* @return bool success
|
||||
*/
|
||||
private function makeRestbaseCheckRequest( $post, &$res ) {
|
||||
$res = null;
|
||||
$request = array(
|
||||
'method' => 'POST',
|
||||
'body' => $post
|
||||
);
|
||||
$serviceClient = $this->getServiceClient();
|
||||
$request['url'] = self::getUrl( "media/math/check/{$this->type}" );
|
||||
$response = $serviceClient->run( $request );
|
||||
if ( $response['code'] === 200 ) {
|
||||
$res = $response['body'];
|
||||
$headers = $response['headers'];
|
||||
$this->hash = $headers['x-resource-location'];
|
||||
return true;
|
||||
} else {
|
||||
$res = $response['body'];
|
||||
$this->log()->debug( 'Tex check failed:', array(
|
||||
'post' => $post,
|
||||
'error' => $response['error'],
|
||||
'url' => $request['url']
|
||||
) );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getServiceClient() {
|
||||
global $wgVirtualRestConfig;
|
||||
$serviceClient = new VirtualRESTServiceClient( new MultiHttpClient( array() ) );
|
||||
if ( isset( $wgVirtualRestConfig['modules']['restbase'] ) ) {
|
||||
$cfg = $wgVirtualRestConfig['modules']['restbase'];
|
||||
$cfg['parsoidCompat'] = false;
|
||||
$vrsObject = new RestbaseVirtualRESTService( $cfg );
|
||||
$serviceClient->mount( '/mathoid/', $vrsObject );
|
||||
}
|
||||
return $serviceClient;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The URL is generated accoding to the following logic:
|
||||
*
|
||||
* Case A: <code>$internal = false</code>, which means one needs an URL that is accessible from
|
||||
* outside:
|
||||
*
|
||||
* --> If <code>$wgMathFullRestbaseURL</code> is configured use it, otherwise fall back try to
|
||||
* <code>$wgVisualEditorFullRestbaseURL</code>. (Note, that this is not be worse than failing
|
||||
* immediately.)
|
||||
*
|
||||
* Case B: <code> $internal= true</code>, which means one needs to access content from Restbase
|
||||
* which does not need to be accessible from outside:
|
||||
*
|
||||
* --> Use the mount point whenever possible. If the mount point is not available, use
|
||||
* <code>$wgMathFullRestbaseURL</code> with fallback to <code>wgVisualEditorFullRestbaseURL</code>
|
||||
*
|
||||
* @param string $path
|
||||
* @param bool|true $internal
|
||||
* @return string
|
||||
* @throws MWException
|
||||
*/
|
||||
private static function getUrl( $path, $internal = true ) {
|
||||
global $wgVirtualRestConfig, $wgMathFullRestbaseURL, $wgVisualEditorFullRestbaseURL;
|
||||
if ( $internal && isset( $wgVirtualRestConfig['modules']['restbase'] ) ) {
|
||||
return "/mathoid/local/v1/$path";
|
||||
}
|
||||
if ( $wgMathFullRestbaseURL ) {
|
||||
return "{$wgMathFullRestbaseURL}v1/$path";
|
||||
}
|
||||
if ( $wgVisualEditorFullRestbaseURL ) {
|
||||
return "{$wgVisualEditorFullRestbaseURL}v1/$path";
|
||||
}
|
||||
throw new MWException( 'Math extension can not find Restbase URL.'.
|
||||
' Please specify $wgMathFullRestbaseURL.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Psr\Log\LoggerInterface
|
||||
*/
|
||||
private function log() {
|
||||
return LoggerFactory::getInstance( 'Math' );
|
||||
}
|
||||
|
||||
public function getSvg() {
|
||||
return $this->getContent( 'svg' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|false $skipConfigCheck
|
||||
* @return bool
|
||||
*/
|
||||
public function checkBackend( $skipConfigCheck = false ) {
|
||||
try {
|
||||
$request = array(
|
||||
'method' => 'GET',
|
||||
'url' => self::getUrl( '?spec' )
|
||||
);
|
||||
} catch ( Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
$serviceClient = $this->getServiceClient();
|
||||
$response = $serviceClient->run( $request );
|
||||
if ( $response['code'] === 200 ) {
|
||||
return $skipConfigCheck || $this->checkConfig();
|
||||
}
|
||||
$this->log()->error( "Restbase backend is not correctly set up.", array(
|
||||
'request' => $request,
|
||||
'response' => $response
|
||||
) );
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique TeX string, renders it and gets it via a public URL.
|
||||
* The method fails, if the public URL does not point to the same server, who did render
|
||||
* the unique TeX input in the first place.
|
||||
* @return bool
|
||||
*/
|
||||
private function checkConfig() {
|
||||
// Generates a TeX string that probably has not been generated before
|
||||
$uniqueTeX = uniqid( 't=', true );
|
||||
$testInterface = new MathRestbaseInterface( $uniqueTeX );
|
||||
if ( ! $testInterface->checkTeX() ){
|
||||
$this->log()->warning( 'Config check failed, since test expression was considered as invalid.',
|
||||
array( 'uniqueTeX' => $uniqueTeX ) );
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$url = $testInterface->getFullSvgUrl();
|
||||
$req = MWHttpRequest::factory( $url );
|
||||
$status = $req->execute();
|
||||
if ( $status->isOK() ){
|
||||
return true;
|
||||
}
|
||||
$this->log()->warning( 'Config check failed, due to an invalid response code.',
|
||||
array( 'responseCode' => $status ) );
|
||||
} catch ( Exception $e ) {
|
||||
$this->log()->warning( 'Config check failed, due to an exception.', array( $e ) );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a publicly accessible link to the generated SVG image.
|
||||
* @return string
|
||||
* @throws MWException
|
||||
*/
|
||||
public function getFullSvgUrl() {
|
||||
$this->calculateHash();
|
||||
return self::getUrl( "media/math/render/svg/{$this->hash}", false );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCheckedTex() {
|
||||
return $this->checkedTex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function getSuccess() {
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getIdentifiers() {
|
||||
return $this->identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass
|
||||
*/
|
||||
public function getError() {
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTex() {
|
||||
return $this->tex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType() {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
"MathDataModule": "MathDataModule.php",
|
||||
"MathHooks": "Math.hooks.php",
|
||||
"MathRenderer": "MathRenderer.php",
|
||||
"MathRestbaseInterface": "MathRestbaseInterface.php",
|
||||
"MathTexvc": "MathTexvc.php",
|
||||
"MathSource": "MathSource.php",
|
||||
"MathMathML": "MathMathML.php",
|
||||
|
@ -91,6 +92,7 @@
|
|||
"MathLaTeXMLUrl": "http://gw125.iu.xsede.org:8888",
|
||||
"MathMathMLTimeout": 20,
|
||||
"MathMathMLUrl": "http://mathoid.testme.wmflabs.org",
|
||||
"MathFullRestbaseURL": false,
|
||||
"MathPath": false,
|
||||
"MathTexvcCheckExecutable": false,
|
||||
"MathValidModes": [
|
||||
|
|
78
tests/MathRestBaseInterfaceTest.php
Normal file
78
tests/MathRestBaseInterfaceTest.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Test the interface to access Restbase paths
|
||||
* /media/math/check/{type}
|
||||
* /media/math/render/{format}/{hash}
|
||||
*
|
||||
* @group Math
|
||||
*/
|
||||
class MathRestbaseInterfaceTest extends MediaWikiTestCase {
|
||||
protected static $hasRestbase;
|
||||
|
||||
public static function setUpBeforeClass() {
|
||||
$rbi = new MathRestbaseInterface();
|
||||
self::$hasRestbase = $rbi->checkBackend( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the fixture, for example, opens a network connection.
|
||||
* This method is called before a test is executed.
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
if ( !self::$hasRestbase ) {
|
||||
$this->markTestSkipped( "Can not connect to Restbase Math interface." );
|
||||
}
|
||||
}
|
||||
|
||||
public function testConfig() {
|
||||
$rbi = new MathRestbaseInterface();
|
||||
$this->assertTrue( $rbi->checkBackend() );
|
||||
}
|
||||
|
||||
public function testSuccess() {
|
||||
$input = '\\sin x^2';
|
||||
$rbi = new MathRestbaseInterface( $input );
|
||||
$this->assertTrue( $rbi->checkTeX(), "Assuming that $input is valid input." );
|
||||
$this->assertTrue( $rbi->getSuccess(), "Assuming that $input is valid input." );
|
||||
$this->assertEquals( '\\sin x^{2}', $rbi->getCheckedTex() );
|
||||
$this->assertContains( '<mi>sin</mi>', $rbi->getMathML() );
|
||||
$url = $rbi->getFullSvgUrl();
|
||||
$req = MWHttpRequest::factory( $url );
|
||||
$status = $req->execute();
|
||||
$this->assertTrue( $status->isOK() );
|
||||
$this->assertContains( '</svg>', $req->getContent() );
|
||||
}
|
||||
|
||||
public function testFail() {
|
||||
$input = '\\sin\\newcommand';
|
||||
$rbi = new MathRestbaseInterface( $input );
|
||||
$this->assertFalse( $rbi->checkTeX(), "Assuming that $input is invalid input." );
|
||||
$this->assertFalse( $rbi->getSuccess(), "Assuming that $input is invalid input." );
|
||||
$this->assertEquals( '', $rbi->getCheckedTex() );
|
||||
$this->assertEquals( 'Illegal TeX function', $rbi->getError()->error->message );
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException MWException
|
||||
* @expectedExceptionMessage TeX input is invalid.
|
||||
*/
|
||||
public function testException() {
|
||||
$input = '\\newcommand';
|
||||
$rbi = new MathRestbaseInterface( $input );
|
||||
$rbi->getMathML();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException MWException
|
||||
* @expectedExceptionMessage TeX input is invalid.
|
||||
*/
|
||||
public function testExceptionSvg() {
|
||||
$input = '\\newcommand';
|
||||
$rbi = new MathRestbaseInterface( $input );
|
||||
$rbi->getFullSvgUrl();
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in a new issue