637 lines
20 KiB
PHP
637 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Portions Copyright 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
|
|
* Copyright 2007-2016 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @license http://www.horde.org/licenses/bsd BSD
|
|
* @category Horde
|
|
* @package Xml_Element
|
|
*/
|
|
|
|
/**
|
|
* Wraps a DOMElement allowing for SimpleXML-like access to attributes.
|
|
*
|
|
* @method mixed TAGNAME() To get the un-wrapped value of a node, use
|
|
* method syntax ($xml_element->tagname()). This will return the
|
|
* string value of the tag if it is a single tag, an array of
|
|
* Horde_Xml_Element objects if there are multiple tags, or null if
|
|
* the tag does not exist.
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @license http://www.horde.org/licenses/bsd BSD
|
|
* @category Horde
|
|
* @package Xml_Element
|
|
*/
|
|
class Horde_Xml_Element implements ArrayAccess
|
|
{
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected static $_namespaces = array(
|
|
'opensearch' => 'http://a9.com/-/spec/opensearchrss/1.0/',
|
|
'atom' => 'http://www.w3.org/2005/Atom',
|
|
'rss' => 'http://blogs.law.harvard.edu/tech/rss',
|
|
'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns',
|
|
);
|
|
|
|
/**
|
|
* Get the full version of a namespace prefix
|
|
*
|
|
* Looks up a prefix (atom:, etc.) in the list of registered
|
|
* namespaces and returns the full namespace URI if
|
|
* available. Returns the prefix, unmodified, if it's not
|
|
* registered.
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function lookupNamespace($prefix)
|
|
{
|
|
return isset(self::$_namespaces[$prefix]) ?
|
|
self::$_namespaces[$prefix] :
|
|
$prefix;
|
|
}
|
|
|
|
/**
|
|
* Add a namespace and prefix to the registered list
|
|
*
|
|
* Takes a prefix and a full namespace URI and adds them to the
|
|
* list of registered namespaces for use by
|
|
* Horde_Xml_Element::lookupNamespace().
|
|
*
|
|
* @param string $prefix The namespace prefix
|
|
* @param string $namespaceURI The full namespace URI
|
|
*/
|
|
public static function registerNamespace($prefix, $namespaceURI)
|
|
{
|
|
self::$_namespaces[$prefix] = $namespaceURI;
|
|
}
|
|
|
|
/**
|
|
* @var DOMElement
|
|
*/
|
|
protected $_element;
|
|
|
|
/**
|
|
* A string representation of the element, used when
|
|
* serializing/unserializing.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_serialized;
|
|
|
|
/**
|
|
* @var Horde_Xml_Element
|
|
*/
|
|
protected $_parentElement;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $_children = null;
|
|
|
|
/**
|
|
* @var boolean
|
|
*/
|
|
protected $_appended = true;
|
|
|
|
/**
|
|
* Horde_Xml_Element constructor.
|
|
*
|
|
* @param DOMElement | Horde_Xml_Element | string $element The DOM element,
|
|
* pre-existing Horde_Xml_Element, or XML string that we're encapsulating.
|
|
*/
|
|
public function __construct($element)
|
|
{
|
|
$this->_element = $element;
|
|
$this->__wakeup();
|
|
}
|
|
|
|
/**
|
|
* Get a DOM representation of the element
|
|
*
|
|
* Returns the underlying DOM object, which can then be manipulated with
|
|
* full DOM methods.
|
|
*
|
|
* @return DOMElement
|
|
*/
|
|
public function getDom()
|
|
{
|
|
return $this->_element;
|
|
}
|
|
|
|
/**
|
|
* Update the object from a DOM element
|
|
*
|
|
* Take a DOMElement object, which may be originally from a call
|
|
* to getDom() or may be custom created, and use it as the
|
|
* DOM tree for this Horde_Xml_Element.
|
|
*
|
|
* @param DOMElement $element
|
|
*/
|
|
public function setDom(DOMElement $element)
|
|
{
|
|
$this->_element = $this->_element->ownerDocument->importNode($element, true);
|
|
}
|
|
|
|
/**
|
|
* Add child elements and attributes to this element from a simple
|
|
* key => value hash. Keys can be:
|
|
*
|
|
* ElementName -> <$ElementName> will be appended with
|
|
* a value of $value
|
|
* #AttributeName -> An attribute $AttributeName will be
|
|
* added to this element with a value
|
|
* of $value
|
|
* ElementName#AttributeName -> <$ElementName> will be appended to this
|
|
* element if it doesn't already exist,
|
|
* and have its attribute $AttributeName
|
|
* set to $value
|
|
*
|
|
* @param $array Hash to import into this element.
|
|
*/
|
|
public function fromArray($array)
|
|
{
|
|
foreach ($array as $key => $value) {
|
|
$element = null;
|
|
$attribute = null;
|
|
|
|
$hash_position = strpos($key, '#');
|
|
if ($hash_position === false) {
|
|
$element = $key;
|
|
} elseif ($hash_position === 0) {
|
|
$attribute = substr($key, 1);
|
|
} else {
|
|
list($element, $attribute) = explode('#', $key, 2);
|
|
}
|
|
|
|
if (!is_null($element)) {
|
|
if (!is_null($attribute)) {
|
|
$this->{$element}[$attribute] = $value;
|
|
} else {
|
|
if (is_array($value)) {
|
|
// Detect numeric arrays and treat them as multiple
|
|
// instances of the same key.
|
|
$firstKey = key($value);
|
|
if ($firstKey === 0) {
|
|
if (strpos($element, ':') !== false) {
|
|
list($ns) = explode(':', $element, 2);
|
|
$baseNode = $this->_element->ownerDocument->createElementNS(Horde_Xml_Element::lookupNamespace($ns), $element);
|
|
} else {
|
|
$baseNode = $this->_element->ownerDocument->createElement($element);
|
|
}
|
|
|
|
foreach ($value as $v) {
|
|
$node = $baseNode->cloneNode();
|
|
if (is_array($v)) {
|
|
$e = new Horde_Xml_Element($node);
|
|
$e->fromArray($v);
|
|
} else {
|
|
$node->nodeValue = $v;
|
|
$e = new Horde_Xml_Element($node);
|
|
}
|
|
$this->appendChild($e);
|
|
}
|
|
} else {
|
|
$this->$element->fromArray($value);
|
|
}
|
|
} else {
|
|
$this->$element = $value;
|
|
}
|
|
}
|
|
} elseif (!is_null($attribute)) {
|
|
$this[$attribute] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append a child node to this element.
|
|
*
|
|
* @param Horde_Xml_Element $element The element to append.
|
|
*/
|
|
public function appendChild(Horde_Xml_Element $element)
|
|
{
|
|
$element->setParent($this);
|
|
$element->_ensureAppended();
|
|
$this->_expireCachedChildren();
|
|
}
|
|
|
|
/**
|
|
* Get an XML string representation of this element
|
|
*
|
|
* Returns a string of this element's XML, including the XML
|
|
* prologue.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function saveXml($formatted = false)
|
|
{
|
|
// Return a complete document including XML prologue.
|
|
$doc = new DOMDocument($this->_element->ownerDocument->version,
|
|
$this->_element->ownerDocument->actualEncoding);
|
|
$doc->formatOutput = $formatted;
|
|
$doc->appendChild($doc->importNode($this->_element, true));
|
|
return $doc->saveXML();
|
|
}
|
|
|
|
/**
|
|
* Get the XML for only this element
|
|
*
|
|
* Returns a string of this element's XML without prologue.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function saveXmlFragment($formatted = false)
|
|
{
|
|
$oldFormatted = $this->_element->ownerDocument->formatOutput;
|
|
$this->_element->ownerDocument->formatOutput = $formatted;
|
|
$xml = $this->_element->ownerDocument->saveXML($this->_element);
|
|
$this->_element->ownerDocument->formatOutput = $oldFormatted;
|
|
return $xml;
|
|
}
|
|
|
|
/**
|
|
* Unserialization handler; handles $this->_element being an instance of
|
|
* DOMElement or Horde_Xml_Element, or parses it as an XML string.
|
|
*/
|
|
public function __wakeup()
|
|
{
|
|
if ($this->_element instanceof DOMElement) {
|
|
return true;
|
|
}
|
|
|
|
if ($this->_element instanceof Horde_Xml_Element) {
|
|
$this->_element = $this->_element->getDom();
|
|
return true;
|
|
}
|
|
|
|
if ($this->_serialized) {
|
|
$this->_element = $this->_serialized;
|
|
$this->_serialized = null;
|
|
}
|
|
|
|
if (is_string($this->_element)) {
|
|
$doc = new DOMDocument();
|
|
$doc->preserveWhiteSpace = false;
|
|
|
|
$extract = false;
|
|
if (substr($this->_element, 0, 5) != '<?xml') {
|
|
$extract = true;
|
|
$preamble = '<?xml version="1.0" encoding="UTF-8" ?><root ';
|
|
foreach (self::$_namespaces as $prefix => $nsUri) {
|
|
$preamble .= " xmlns:$prefix=\"$nsUri\"";
|
|
}
|
|
$preamble .= '>';
|
|
$this->_element = $preamble . $this->_element . '</root>';
|
|
}
|
|
|
|
$loaded = @$doc->loadXML($this->_element);
|
|
if (!$loaded) {
|
|
throw new Horde_Xml_Element_Exception('DOMDocument cannot parse XML: ', error_get_last());
|
|
}
|
|
|
|
if ($extract) {
|
|
$newDoc = new DOMDocument();
|
|
$this->_element = $newDoc->importNode($doc->documentElement->childNodes->item(0), true);
|
|
} else {
|
|
$this->_element = $doc->documentElement;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
throw new InvalidArgumentException('Horde_Xml_Element initialization value must be a DOMElement, a Horde_Xml_Element, or a non-empty string; '
|
|
. (gettype($this->_element) == 'object' ? get_class($this->_element) : gettype($this->_element))
|
|
. ' given');
|
|
}
|
|
|
|
/**
|
|
* Prepare for serialization
|
|
*
|
|
* @return array
|
|
*/
|
|
public function __sleep()
|
|
{
|
|
$this->_serialized = $this->saveXml();
|
|
return array('_serialized');
|
|
}
|
|
|
|
/**
|
|
* Map variable access onto the underlying entry representation.
|
|
*
|
|
* Get-style access returns a Horde_Xml_Element representing the
|
|
* child element accessed. To get string values, use method syntax
|
|
* with the __call() overriding.
|
|
*
|
|
* @param string $var The property to access.
|
|
* @return mixed
|
|
*/
|
|
public function __get($var)
|
|
{
|
|
$nodes = $this->_children($var);
|
|
$length = count($nodes);
|
|
|
|
if ($length == 1) {
|
|
if ($nodes[0] instanceof Horde_Xml_Element) {
|
|
return $nodes[0];
|
|
}
|
|
return new Horde_Xml_Element($nodes[0]);
|
|
} elseif ($length > 1) {
|
|
if ($nodes[0] instanceof Horde_Xml_Element) {
|
|
return $nodes;
|
|
}
|
|
return array_map(function($e) { return new Horde_Xml_Element($e); }, $nodes);
|
|
} else {
|
|
// When creating anonymous nodes for __set chaining, don't
|
|
// call appendChild() on them. Instead we pass the current
|
|
// element to them as an extra reference; the child is
|
|
// then responsible for appending itself when it is
|
|
// actually set. This way "if ($foo->bar)" doesn't create
|
|
// a phantom "bar" element in our tree.
|
|
if (strpos($var, ':') !== false) {
|
|
list($ns, $elt) = explode(':', $var, 2);
|
|
$node = $this->_element->ownerDocument->createElementNS(Horde_Xml_Element::lookupNamespace($ns), $elt);
|
|
} else {
|
|
$node = $this->_element->ownerDocument->createElement($var);
|
|
}
|
|
$node = new Horde_Xml_Element($node);
|
|
$node->setParent($this);
|
|
return $node;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map variable sets onto the underlying entry representation.
|
|
*
|
|
* @param string $var The property to change.
|
|
* @param string $val The property's new value.
|
|
*/
|
|
public function __set($var, $val)
|
|
{
|
|
if (!is_scalar($val)) {
|
|
throw new InvalidArgumentException('Element values must be scalars, ' . gettype($val) . ' given');
|
|
}
|
|
|
|
$this->_ensureAppended();
|
|
|
|
$nodes = $this->_children($var);
|
|
if (!$nodes) {
|
|
if (strpos($var, ':') !== false) {
|
|
list($ns) = explode(':', $var, 2);
|
|
$node = $this->_element->ownerDocument->createElementNS(Horde_Xml_Element::lookupNamespace($ns), $var, $val);
|
|
$this->_element->appendChild($node);
|
|
} else {
|
|
$node = $this->_element->ownerDocument->createElement($var, $val);
|
|
$this->_element->appendChild($node);
|
|
}
|
|
|
|
$this->_expireCachedChildren();
|
|
} elseif (count($nodes) > 1) {
|
|
throw new Horde_Xml_Element_Exception('Cannot set the value of multiple nodes simultaneously.');
|
|
} else {
|
|
$nodes[0]->nodeValue = $val;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map isset calls onto the underlying entry representation.
|
|
*/
|
|
public function __isset($var)
|
|
{
|
|
return (boolean)$this->_children($var);
|
|
}
|
|
|
|
/**
|
|
* Get the value of an element with method syntax.
|
|
*
|
|
* Map method calls to get the string value of the requested
|
|
* element. If there are multiple elements that match, this will
|
|
* return an array of those objects.
|
|
*
|
|
* @param string $var The element to get the string value of.
|
|
*
|
|
* @return mixed The node's value, null, or an array of nodes.
|
|
*/
|
|
public function __call($var, $unused)
|
|
{
|
|
$nodes = $this->_children($var);
|
|
|
|
if (!$nodes) {
|
|
return null;
|
|
} elseif (count($nodes) > 1) {
|
|
if ($nodes[0] instanceof Horde_Xml_Element) {
|
|
return $nodes;
|
|
}
|
|
return array_map(create_function('$e', 'return new Horde_Xml_Element($e);'), $nodes);
|
|
} else {
|
|
if ($nodes[0] instanceof Horde_Xml_Element) {
|
|
return (string)$nodes[0];
|
|
} else {
|
|
return $nodes[0]->nodeValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all children matching $var.
|
|
*/
|
|
public function __unset($var)
|
|
{
|
|
$nodes = $this->_children($var);
|
|
foreach ($nodes as $node) {
|
|
$parent = $node->parentNode;
|
|
$parent->removeChild($node);
|
|
}
|
|
|
|
$this->_expireCachedChildren();
|
|
}
|
|
|
|
/**
|
|
* Returns the nodeValue of this element when this object is used
|
|
* in a string context.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function __toString()
|
|
{
|
|
return $this->_element->nodeValue;
|
|
}
|
|
|
|
/**
|
|
* Set the parent element of this object to another
|
|
* Horde_Xml_Element.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function setParent(Horde_Xml_Element $element)
|
|
{
|
|
$this->_parentElement = $element;
|
|
$this->_appended = false;
|
|
}
|
|
|
|
/**
|
|
* Appends this element to its parent if necessary.
|
|
*
|
|
* @internal
|
|
*/
|
|
protected function _ensureAppended()
|
|
{
|
|
if (!$this->_appended) {
|
|
$parentDom = $this->_parentElement->getDom();
|
|
if (!$parentDom->ownerDocument->isSameNode($this->_element->ownerDocument)) {
|
|
$this->_element = $parentDom->ownerDocument->importNode($this->_element, true);
|
|
}
|
|
|
|
$parentDom->appendChild($this->_element);
|
|
$this->_appended = true;
|
|
$this->_parentElement->_ensureAppended();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds children with tagnames matching $var
|
|
*
|
|
* Similar to SimpleXML's children() method.
|
|
*
|
|
* @param string Tagname to match, can be either namespace:tagName or just tagName.
|
|
* @return array
|
|
*/
|
|
protected function _children($var)
|
|
{
|
|
if (is_null($this->_children)) {
|
|
$this->_cacheChildren();
|
|
}
|
|
|
|
// Honor any explicit getters. Because Horde_Xml_Element has a __call()
|
|
// method, is_callable returns true on every method name. Use
|
|
// method_exists instead.
|
|
$varMethod = 'get' . Horde_String::ucfirst($var);
|
|
if (method_exists($this, $varMethod)) {
|
|
$children = call_user_func(array($this, $varMethod));
|
|
if (is_null($children)) {
|
|
$this->_children[$var] = array();
|
|
} elseif (!is_array($children)) {
|
|
$this->_children[$var] = array($children);
|
|
} else {
|
|
$this->_children[$var] = $children;
|
|
}
|
|
}
|
|
|
|
if (!isset($this->_children[$var])) {
|
|
$this->_children[$var] = array();
|
|
}
|
|
|
|
return $this->_children[$var];
|
|
}
|
|
|
|
/**
|
|
* Build a cache of child nodes.
|
|
*/
|
|
protected function _cacheChildren()
|
|
{
|
|
foreach ($this->_element->childNodes as $child) {
|
|
if (!isset($this->_children[$child->localName])) {
|
|
$this->_children[$child->localName] = array();
|
|
}
|
|
$this->_children[$child->localName][] = $child;
|
|
|
|
if ($child->prefix) {
|
|
if (!isset($this->_children[$child->prefix . ':' . $child->localName])) {
|
|
$this->_children[$child->prefix . ':' . $child->localName] = array();
|
|
}
|
|
$this->_children[$child->prefix . ':' . $child->localName][] = $child;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expire cached children.
|
|
*/
|
|
protected function _expireCachedChildren()
|
|
{
|
|
$this->_children = null;
|
|
}
|
|
|
|
/**
|
|
* Required by the ArrayAccess interface.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function offsetExists($offset)
|
|
{
|
|
if (strpos($offset, ':') !== false) {
|
|
list($ns, $attr) = explode(':', $offset, 2);
|
|
return $this->_element->hasAttributeNS(Horde_Xml_Element::lookupNamespace($ns), $attr);
|
|
} else {
|
|
return $this->_element->hasAttribute($offset);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Required by the ArrayAccess interface.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function offsetGet($offset)
|
|
{
|
|
if (strpos($offset, ':') !== false) {
|
|
list($ns, $attr) = explode(':', $offset, 2);
|
|
return $this->_element->getAttributeNS(Horde_Xml_Element::lookupNamespace($ns), $attr);
|
|
} else {
|
|
return $this->_element->getAttribute($offset);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Required by the ArrayAccess interface.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function offsetSet($offset, $value)
|
|
{
|
|
if (!is_scalar($value)) {
|
|
throw new InvalidArgumentException('Element values must be scalars, ' . gettype($value) . ' given');
|
|
}
|
|
|
|
$this->_ensureAppended();
|
|
|
|
if (strpos($offset, ':') !== false) {
|
|
list($ns) = explode(':', $offset, 2);
|
|
$result = $this->_element->setAttributeNS(Horde_Xml_Element::lookupNamespace($ns), $offset, $value);
|
|
} else {
|
|
$result = $this->_element->setAttribute($offset, $value);
|
|
}
|
|
|
|
if ($result) {
|
|
$this->_expireCachedChildren();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Required by the ArrayAccess interface.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function offsetUnset($offset)
|
|
{
|
|
if (strpos($offset, ':') !== false) {
|
|
list($ns, $attr) = explode(':', $offset, 2);
|
|
$result = $this->_element->removeAttributeNS(Horde_Xml_Element::lookupNamespace($ns), $attr);
|
|
} else {
|
|
$result = $this->_element->removeAttribute($offset);
|
|
}
|
|
|
|
if ($result) {
|
|
$this->_expireCachedChildren();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
}
|