mediawiki-extensions-Math/src/MathMathMLCli.php

198 lines
4.9 KiB
PHP
Raw Normal View History

<?php
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
/**
* @author Moritz Schubotz
*/
class MathMathMLCli extends MathMathML {
/**
Don't expect objects by reference in hook handlers The motivation for this patch is to make the code less complex, better readable, and less brittle. Example: public function onExampleHook( Parser &$parser, array &$result ) { /* This is the hook handler */ } In this example the $result array is meant to be manipulated by the hook handler. Changes should become visible to the caller. Since PHP passes arrays by value, the & is needed to make this possible. But the & is misplaced in pretty much all cases where the parameter is an object. The only reason we still see these & in many hook handlers is historical: PHP 4 passed objects by value, which potentially caused expensive cloning. This was prevented with the &. Since PHP 5 objects are passed by reference. However, this did not made the & entirely meaningless. Keeping the & means callees are allowed to replace passed objects with new ones. The & makes it look like a function might intentionally replace a passed object, which is unintended and actually scary in cases like the Parser. Luckily all Hooks::run I have seen so far ignore unintended out-values. So even if a hook handler tries to do something bad like replacing the Parser with a different one, this would not have an effect. Removing the & does not remove the possibility to manipulate the object. Changes done to public properties are still visible to the caller. Unfortunately these & cannot be removed from the callers as long as there is a single callee expecting a reference. This patch reduces the number of such problematic callees. Change-Id: I21d53c989ea487607dc69e6b3365c023ef6729f5
2018-05-15 15:50:08 +00:00
* @param array[] $tags math tags
* @return bool
* @throws MWException
*/
Don't expect objects by reference in hook handlers The motivation for this patch is to make the code less complex, better readable, and less brittle. Example: public function onExampleHook( Parser &$parser, array &$result ) { /* This is the hook handler */ } In this example the $result array is meant to be manipulated by the hook handler. Changes should become visible to the caller. Since PHP passes arrays by value, the & is needed to make this possible. But the & is misplaced in pretty much all cases where the parameter is an object. The only reason we still see these & in many hook handlers is historical: PHP 4 passed objects by value, which potentially caused expensive cloning. This was prevented with the &. Since PHP 5 objects are passed by reference. However, this did not made the & entirely meaningless. Keeping the & means callees are allowed to replace passed objects with new ones. The & makes it look like a function might intentionally replace a passed object, which is unintended and actually scary in cases like the Parser. Luckily all Hooks::run I have seen so far ignore unintended out-values. So even if a hook handler tries to do something bad like replacing the Parser with a different one, this would not have an effect. Removing the & does not remove the possibility to manipulate the object. Changes done to public properties are still visible to the caller. Unfortunately these & cannot be removed from the callers as long as there is a single callee expecting a reference. This patch reduces the number of such problematic callees. Change-Id: I21d53c989ea487607dc69e6b3365c023ef6729f5
2018-05-15 15:50:08 +00:00
public static function batchEvaluate( array $tags ) {
$req = [];
foreach ( $tags as $key => $tag ) {
/** @var MathMathMLCli $renderer */
$renderer = $tag[0];
// checking if the rendering is in the database is no security issue since only the md5
// hash of the user input string will be sent to the database
if ( !$renderer->isInDatabase() ) {
$req[] = $renderer->getMathoidCliQuery();
}
}
if ( count( $req ) === 0 ) {
return true;
}
$exitCode = 1;
$res = self::evaluateWithCli( $req, $exitCode );
foreach ( $tags as $key => $tag ) {
/** @var MathMathMLCli $renderer */
$renderer = $tag[0];
if ( !$renderer->isInDatabase() ) {
$renderer->initializeFromCliResponse( $res );
}
}
return true;
}
/**
* @param object $res
* @return bool
*/
private function initializeFromCliResponse( $res ) {
global $wgMathoidCli;
if ( !property_exists( $res, $this->getMd5() ) ) {
$this->lastError =
$this->getError( 'math_mathoid_error', 'cli',
var_export( get_object_vars( $res ), true ) );
return false;
}
if ( $this->isEmpty() ) {
return false;
}
$response = $res->{$this->getMd5()};
if ( !$response->success ) {
$this->lastError = $this->renderError( $response );
return false;
}
$this->texSecure = true;
$this->tex = $response->sanetex;
// The host name is only relevant for the debugging. So using file:// to indicate that the
// cli interface seems to be OK.
$this->processJsonResult( $response, 'file://' . $wgMathoidCli[0] );
$this->mathStyle = $response->mathoidStyle;
if ( array_key_exists( 'png', $response ) ) {
$this->png = implode( array_map( "chr", $response->png->data ) );
} else {
LoggerFactory::getInstance( 'Math' )->error( 'Mathoid did not return a PNG image.' .
' Check your librsvg installation https://github.com/wikimedia/mathoid/.' );
}
$this->changed = true;
}
public function renderError( $response ) {
$msg = $response->error;
try {
switch ( $response->detail->status ) {
case "F":
$msg .= "\n Found {$response->detail->details}" .
$this->appendLocationInfo( $response );
break;
case 'S':
case "C":
$msg .= $this->appendLocationInfo( $response );
break;
case '-':
// we do not know any cases that triggers this error
}
}
catch ( Exception $e ) {
// use default error message
}
return $this->getError( 'math_mathoid_error', 'cli', $msg );
}
/**
* @return array
*/
public function getMathoidCliQuery() {
return [
'query' => [
'q' => $this->getTex(),
'type' => $this->getInputType(),
'hash' => $this->getMd5(),
],
];
}
/**
* @param mixed $req request
* @param int|null &$exitCode exit code
* @return mixed
* @throws MWException
*/
public static function evaluateWithCli( $req, &$exitCode = null ) {
global $wgMathoidCli;
$json_req = json_encode( $req );
$cmd = MediaWikiServices::getInstance()->getShellCommandFactory()->create();
$cmd->params( $wgMathoidCli );
$cmd->input( $json_req );
$result = $cmd->execute();
if ( $result->getExitCode() != 0 ) {
$errorMsg = $result->getStderr();
LoggerFactory::getInstance( 'Math' )->error( 'Can not process {req} with config
{conf} returns {res}', [
'req' => $req,
'conf' => var_export( $wgMathoidCli, true ),
'res' => var_export( $result, true ),
] );
throw new MWException( "Failed to execute Mathoid cli '$wgMathoidCli[0]', reason: $errorMsg" );
}
$res = json_decode( $result->getStdout() );
if ( !$res ) {
throw new MWException( "Mathoid cli response '$res' is no valid JSON file." );
}
return $res;
}
public function render( $forceReRendering = false ) {
if ( $this->getLastError() ) {
return false;
}
return true;
}
protected function doCheck() {
// avoid that restbase is called if check is set to always
return $this->texSecure;
}
protected function initializeFromDatabaseRow( $rpage ) {
if ( !empty( $rpage->math_svg ) ) {
$this->png = $rpage->math_png;
}
parent::initializeFromDatabaseRow( $rpage ); // TODO: Change the autogenerated stub
}
protected function dbOutArray() {
$out = parent::dbOutArray();
$out['math_png'] = $this->png;
return $out;
}
protected function dbInArray() {
$out = parent::dbInArray();
$out[] = 'math_png';
return $out;
}
public function getPng() {
if ( !$this->png ) {
$this->initializeFromCliResponse( self::evaluateWithCli( [
$this->getMathoidCliQuery(),
] ) );
}
return parent::getPng();
}
/**
* @param stdClass $response object from cli
* @return string containing the location information
*/
private function appendLocationInfo( $response ) {
return "in {$response->detail->line}:{$response->detail->column}";
}
}