first commit
This commit is contained in:
+149
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\Math\Reader;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use DOMXPath;
|
||||
use PhpOffice\Math\Element;
|
||||
use PhpOffice\Math\Exception\InvalidInputException;
|
||||
use PhpOffice\Math\Exception\NotImplementedException;
|
||||
use PhpOffice\Math\Math;
|
||||
use PhpOffice\Math\Reader\Security\XmlScanner;
|
||||
|
||||
class MathML implements ReaderInterface
|
||||
{
|
||||
/** @var Math */
|
||||
private $math;
|
||||
|
||||
/** @var DOMDocument */
|
||||
private $dom;
|
||||
|
||||
/** @var DOMXPath */
|
||||
private $xpath;
|
||||
|
||||
/** @var XmlScanner */
|
||||
private $xmlScanner;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->xmlScanner = XmlScanner::getInstance();
|
||||
}
|
||||
|
||||
public function read(string $content): ?Math
|
||||
{
|
||||
$content = $this->xmlScanner->scan($content);
|
||||
$content = str_replace(
|
||||
[
|
||||
'⁢',
|
||||
],
|
||||
[
|
||||
'<mchar name="InvisibleTimes"/>',
|
||||
],
|
||||
$content
|
||||
);
|
||||
|
||||
$this->dom = new DOMDocument();
|
||||
$this->dom->loadXML($content);
|
||||
|
||||
$this->math = new Math();
|
||||
$this->parseNode(null, $this->math);
|
||||
|
||||
return $this->math;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Math|Element\AbstractGroupElement $parent
|
||||
*/
|
||||
protected function parseNode(?DOMNode $nodeRowElement, $parent): void
|
||||
{
|
||||
$this->xpath = new DOMXPath($this->dom);
|
||||
foreach ($this->xpath->query('*', $nodeRowElement) ?: [] as $nodeElement) {
|
||||
if ($parent instanceof Element\Semantics
|
||||
&& $nodeElement instanceof DOMElement
|
||||
&& $nodeElement->nodeName == 'annotation') {
|
||||
$parent->addAnnotation(
|
||||
$nodeElement->getAttribute('encoding'),
|
||||
trim($nodeElement->nodeValue)
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$parent->add($this->getElement($nodeElement));
|
||||
}
|
||||
}
|
||||
|
||||
protected function getElement(DOMNode $nodeElement): Element\AbstractElement
|
||||
{
|
||||
$nodeValue = trim($nodeElement->nodeValue);
|
||||
switch ($nodeElement->nodeName) {
|
||||
case 'mfrac':
|
||||
$nodeList = $this->xpath->query('*', $nodeElement);
|
||||
if ($nodeList && $nodeList->length == 2) {
|
||||
return new Element\Fraction(
|
||||
$this->getElement($nodeList->item(0)),
|
||||
$this->getElement($nodeList->item(1))
|
||||
);
|
||||
}
|
||||
|
||||
throw new InvalidInputException(sprintf(
|
||||
'%s : The tag `%s` has not two subelements',
|
||||
__METHOD__,
|
||||
$nodeElement->nodeName
|
||||
));
|
||||
case 'mi':
|
||||
return new Element\Identifier($nodeValue);
|
||||
case 'mn':
|
||||
return new Element\Numeric(floatval($nodeValue));
|
||||
case 'mo':
|
||||
if (empty($nodeValue)) {
|
||||
$nodeList = $this->xpath->query('*', $nodeElement);
|
||||
if (
|
||||
$nodeList
|
||||
&& $nodeList->length == 1
|
||||
&& $nodeList->item(0)->nodeName == 'mchar'
|
||||
&& $nodeList->item(0) instanceof DOMElement
|
||||
&& $nodeList->item(0)->hasAttribute('name')
|
||||
) {
|
||||
$nodeValue = $nodeList->item(0)->getAttribute('name');
|
||||
}
|
||||
}
|
||||
|
||||
return new Element\Operator($nodeValue);
|
||||
case 'mrow':
|
||||
$mrow = new Element\Row();
|
||||
|
||||
$this->parseNode($nodeElement, $mrow);
|
||||
|
||||
return $mrow;
|
||||
case 'msup':
|
||||
$nodeList = $this->xpath->query('*', $nodeElement);
|
||||
if ($nodeList && $nodeList->length == 2) {
|
||||
return new Element\Superscript(
|
||||
$this->getElement($nodeList->item(0)),
|
||||
$this->getElement($nodeList->item(1))
|
||||
);
|
||||
}
|
||||
|
||||
throw new InvalidInputException(sprintf(
|
||||
'%s : The tag `%s` has not two subelements',
|
||||
__METHOD__,
|
||||
$nodeElement->nodeName
|
||||
));
|
||||
case 'semantics':
|
||||
$semantics = new Element\Semantics();
|
||||
|
||||
$this->parseNode($nodeElement, $semantics);
|
||||
|
||||
return $semantics;
|
||||
default:
|
||||
throw new NotImplementedException(sprintf(
|
||||
'%s : The tag `%s` is not implemented',
|
||||
__METHOD__,
|
||||
$nodeElement->nodeName
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\Math\Reader;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
use DOMXPath;
|
||||
use PhpOffice\Math\Element;
|
||||
use PhpOffice\Math\Exception\InvalidInputException;
|
||||
use PhpOffice\Math\Exception\NotImplementedException;
|
||||
use PhpOffice\Math\Math;
|
||||
|
||||
class OfficeMathML implements ReaderInterface
|
||||
{
|
||||
/** @var DOMDocument */
|
||||
protected $dom;
|
||||
|
||||
/** @var Math */
|
||||
protected $math;
|
||||
|
||||
/** @var DOMXPath */
|
||||
protected $xpath;
|
||||
|
||||
/** @var string[] */
|
||||
protected $operators = ['+', '-', '/', '∗'];
|
||||
|
||||
public function read(string $content): ?Math
|
||||
{
|
||||
$nsMath = 'xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"';
|
||||
$nsWord = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';
|
||||
|
||||
$content = str_replace(
|
||||
$nsMath,
|
||||
$nsMath . ' ' . $nsWord,
|
||||
$content
|
||||
);
|
||||
|
||||
$this->dom = new DOMDocument();
|
||||
$this->dom->loadXML($content);
|
||||
|
||||
$this->math = new Math();
|
||||
$this->parseNode(null, $this->math);
|
||||
|
||||
return $this->math;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://devblogs.microsoft.com/math-in-office/officemath/
|
||||
* @see https://learn.microsoft.com/fr-fr/archive/blogs/murrays/mathml-and-ecma-math-omml
|
||||
*
|
||||
* @param Math|Element\AbstractGroupElement $parent
|
||||
*/
|
||||
protected function parseNode(?DOMNode $nodeRowElement, $parent): void
|
||||
{
|
||||
$this->xpath = new DOMXPath($this->dom);
|
||||
foreach ($this->xpath->query('*', $nodeRowElement) ?: [] as $nodeElement) {
|
||||
$element = $this->getElement($nodeElement);
|
||||
$parent->add($element);
|
||||
|
||||
if ($element instanceof Element\AbstractGroupElement) {
|
||||
$this->parseNode($nodeElement, $element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getElement(DOMNode $nodeElement): Element\AbstractElement
|
||||
{
|
||||
switch ($nodeElement->nodeName) {
|
||||
case 'm:f':
|
||||
// Numerator
|
||||
$nodeNumerator = $this->xpath->query('m:num/m:r/m:t', $nodeElement);
|
||||
if ($nodeNumerator && $nodeNumerator->length == 1) {
|
||||
$value = $nodeNumerator->item(0)->nodeValue;
|
||||
if (is_numeric($value)) {
|
||||
$numerator = new Element\Numeric(floatval($value));
|
||||
} else {
|
||||
$numerator = new Element\Identifier($value);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidInputException(sprintf(
|
||||
'%s : The tag `%s` has no numerator defined',
|
||||
__METHOD__,
|
||||
$nodeElement->nodeName
|
||||
));
|
||||
}
|
||||
// Denominator
|
||||
$nodeDenominator = $this->xpath->query('m:den/m:r/m:t', $nodeElement);
|
||||
if ($nodeDenominator && $nodeDenominator->length == 1) {
|
||||
$value = $nodeDenominator->item(0)->nodeValue;
|
||||
if (is_numeric($value)) {
|
||||
$denominator = new Element\Numeric(floatval($value));
|
||||
} else {
|
||||
$denominator = new Element\Identifier($value);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidInputException(sprintf(
|
||||
'%s : The tag `%s` has no denominator defined',
|
||||
__METHOD__,
|
||||
$nodeElement->nodeName
|
||||
));
|
||||
}
|
||||
|
||||
return new Element\Fraction($numerator, $denominator);
|
||||
case 'm:r':
|
||||
$nodeText = $this->xpath->query('m:t', $nodeElement);
|
||||
if ($nodeText && $nodeText->length == 1) {
|
||||
$value = trim($nodeText->item(0)->nodeValue);
|
||||
if (in_array($value, $this->operators)) {
|
||||
return new Element\Operator($value);
|
||||
}
|
||||
if (is_numeric($value)) {
|
||||
return new Element\Numeric(floatval($value));
|
||||
}
|
||||
|
||||
return new Element\Identifier($value);
|
||||
}
|
||||
|
||||
throw new InvalidInputException(sprintf(
|
||||
'%s : The tag `%s` has no tag `m:t` defined',
|
||||
__METHOD__,
|
||||
$nodeElement->nodeName
|
||||
));
|
||||
case 'm:oMath':
|
||||
return new Element\Row();
|
||||
default:
|
||||
throw new NotImplementedException(sprintf(
|
||||
'%s : The tag `%s` is not implemented',
|
||||
__METHOD__,
|
||||
$nodeElement->nodeName
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\Math\Reader;
|
||||
|
||||
use PhpOffice\Math\Math;
|
||||
|
||||
interface ReaderInterface
|
||||
{
|
||||
public function read(string $content): ?Math;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\Math\Reader\Security;
|
||||
|
||||
use PhpOffice\Math\Exception\SecurityException;
|
||||
|
||||
class XmlScanner
|
||||
{
|
||||
public static function getInstance(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
|
||||
*/
|
||||
public function scan(string $xml): string
|
||||
{
|
||||
// Don't rely purely on libxml_disable_entity_loader()
|
||||
$searchDoctype = static::mb_str_split('<!DOCTYPE', 1, 'UTF-8');
|
||||
$patternDoctype = '/\0*' . implode('\0*', is_array($searchDoctype) ? $searchDoctype : []) . '\0*/';
|
||||
$searchDoctypeMath = static::mb_str_split('<!DOCTYPE math', 1, 'UTF-8');
|
||||
$patternDoctypeMath = '/\0*' . implode('\0*', is_array($searchDoctypeMath) ? $searchDoctypeMath : []) . '\0*/';
|
||||
|
||||
if (preg_match($patternDoctype, $xml) && !preg_match($patternDoctypeMath, $xml)) {
|
||||
throw new SecurityException('Detected use of ENTITY in XML, loading aborted to prevent XXE/XEE attacks');
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param int<1, max> $split_length
|
||||
* @param string|null $encoding
|
||||
*
|
||||
* @return array<string>|bool|null
|
||||
*/
|
||||
public static function mb_str_split(string $string, int $split_length = 1, ?string $encoding = null)
|
||||
{
|
||||
if (extension_loaded('mbstring')) {
|
||||
if (function_exists('mb_str_split')) {
|
||||
return mb_str_split($string, $split_length, $encoding);
|
||||
}
|
||||
}
|
||||
// @phpstan-ignore-next-line
|
||||
if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
|
||||
trigger_error('mb_str_split() expects parameter 1 to be string, ' . \gettype($string) . ' given', \E_USER_WARNING);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
if (1 > $split_length = (int) $split_length) {
|
||||
trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $encoding) {
|
||||
$encoding = mb_internal_encoding();
|
||||
}
|
||||
|
||||
if ('UTF-8' === $encoding || \in_array(strtoupper($encoding), ['UTF-8', 'UTF8'], true)) {
|
||||
return preg_split("/(.{{$split_length}})/u", $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$length = mb_strlen($string, $encoding);
|
||||
|
||||
for ($i = 0; $i < $length; $i += $split_length) {
|
||||
$result[] = mb_substr($string, $i, $split_length, $encoding);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user