Add Restbase interface

* Add a simple interface to Restbase that currently
  does not do anything.

Change-Id: I4c2ef329b3954fd35276a5e270d0dc3fcacabf7b
This commit is contained in:
physikerwelt 2015-11-26 20:31:09 +01:00 committed by Mobrovac
parent b292ba512e
commit 6ef2e56439
5 changed files with 380 additions and 8 deletions

2
.gitignore vendored
View file

@ -7,3 +7,5 @@
tests/browser/.bundle
tests/browser/.gem
node_modules/
vendor
composer.lock

View file

@ -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
View 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;
}
}

View file

@ -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": [

View 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();
}
}