Files
server/usr/share/psa-pear/pear/php/Horde/Xml/Element.php
2026-01-07 20:52:11 +01:00

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;
}
}
}