<?php /** * Holds a bundle definition in a DOM document */ class Loco_config_XMLModel extends Loco_config_Model { /** * @var DOMDocument */ private $dom; /** * @var DOMXpath */ private $xpath; /** * {@inheritdoc} */ public function createDom(){ $dom = new DOMDocument('1.0','utf-8'); $dom->formatOutput = true; $dom->registerNodeClass('DOMElement','LocoConfig_DOMElement'); $this->xpath = new DOMXPath($dom); $this->dom = $dom; } /** * @return DOMDocument */ public function getDom(){ return $this->dom; } /** * {@inheritdoc} * @return LocoConfigNodeListIterator */ public function query( $query, $context = null ){ $list = $this->xpath->query( $query, $context ); return new LocoConfigNodeListIterator( $list ); } /** * @return void */ public function loadXml( $source ){ if( ! $source ){ throw new Loco_error_XmlParseException( __('XML supplied is empty','loco-translate') ); } $dom = $this->getDom(); // parse with silent errors, clearing after $used_errors = libxml_use_internal_errors(true); $dom->loadXML( $source, LIBXML_NONET ); unset( $source ); // fetch errors and ensure clean for next run. $errors = libxml_get_errors(); $used_errors || libxml_use_internal_errors(false); libxml_clear_errors(); // Throw exception if error level exceeds current tolerance if( $errors ){ foreach( $errors as $error ){ if( $error->level >= LIBXML_ERR_FATAL ){ throw new Loco_error_XmlParseException( trim($error->message) ); // ->setContext( $error->line, $error->column, $source ); } } } // Not currently validating against a DTD, but will preempt generic model loading errors $root = $dom->documentElement; if( ! $root instanceof DOMNode ){ throw new Loco_error_XmlParseException('Expected <bundle> document element'); } if( 'bundle' !== strtolower($root->nodeName) ){ throw new Loco_error_XmlParseException('Expected <bundle> document element, got <'.$root->nodeName.'>'); } $this->xpath = new DOMXPath($dom); } /** * {@inheritdoc} * Overridden to avoid empty text nodes in XML files, preferring <file>.</file> to <file /> */ protected function setFileElementPath( $node, $path ){ if( ! $path && '0' !== $path ){ $path = '.'; } return parent::setFileElementPath( $node, $path ); } } /** * @internal */ class LocoConfig_DOMElement extends DOMElement implements IteratorAggregate, Countable { #[ReturnTypeWillChange] public function getIterator(){ return new LocoConfigNodeListIterator( $this->childNodes ); } #[ReturnTypeWillChange] public function count(){ return $this->childNodes->length; } } /** * @internal * Cos NodeList doesn't iterate */ class LocoConfigNodeListIterator implements Iterator, Countable, ArrayAccess { /** * @var DOMNodeList */ private $nodes; /** * @var int */ private $i; /** * @var int */ private $n; public function __construct( DOMNodeList $nodes ){ $this->nodes = $nodes; $this->n = $nodes->length; } #[ReturnTypeWillChange] public function count(){ return $this->n; } #[ReturnTypeWillChange] public function rewind(){ $this->i = -1; $this->next(); } #[ReturnTypeWillChange] public function key(){ return $this->i; } #[ReturnTypeWillChange] public function current(){ return $this->nodes->item( $this->i ); } #[ReturnTypeWillChange] public function valid(){ return is_int($this->i); } #[ReturnTypeWillChange] public function next(){ while( true ){ $this->i++; if( $child = $this->nodes->item($this->i) ){ break; } $this->i = null; break; } } #[ReturnTypeWillChange] public function offsetExists( $i ){ return $i >= 0 && $i < $this->n; } #[ReturnTypeWillChange] public function offsetGet( $i ){ return $this->nodes->item($i); } /** * @codeCoverageIgnore */ #[ReturnTypeWillChange] public function offsetSet( $i, $value ){ throw new Exception('Read only'); } /** * @codeCoverageIgnore */ #[ReturnTypeWillChange] public function offsetUnset( $i ){ throw new Exception('Read only'); } }