<?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');
}
}