* @copyright 2014 Fabian Grutschus. All rights reserved. * @license BSD * @link http://github.com/fabiang/xmpp */ namespace Fabiang\Xmpp\Stream; use Fabiang\Xmpp\Event\EventManagerAwareInterface; use Fabiang\Xmpp\Event\EventManagerInterface; use Fabiang\Xmpp\Event\EventManager; use Fabiang\Xmpp\Event\XMLEvent; use Fabiang\Xmpp\Event\XMLEventInterface; use Fabiang\Xmpp\Exception\XMLParserException; /** * Xml stream class. * * @package Xmpp\Stream */ class XMLStream implements EventManagerAwareInterface { const NAMESPACE_SEPARATOR = ':'; /** * Eventmanager. * * @var EventManagerInterface */ protected $events; /** * Document encoding. * * @var string */ protected $encoding; /** * Current parsing depth. * * @var integer */ protected $depth = 0; /** * * @var \DOMDocument */ protected $document; /** * Collected namespaces. * * @var array */ protected $namespaces = []; /** * Cache of namespace prefixes. * * @var array */ protected $namespacePrefixes = []; /** * Element cache. * * @var array */ protected $elements = []; /** * XML parser. * * @var resource */ protected $parser; /** * Event object. * * @var XMLEventInterface */ protected $eventObject; /** * Collected events while parsing. * * @var array */ protected $eventCache = []; /** * Constructor. */ public function __construct($encoding = 'UTF-8', XMLEventInterface $eventObject = null) { $this->encoding = $encoding; $this->reset(); if (null === $eventObject) { $eventObject = new XMLEvent(); } $this->eventObject = $eventObject; } /** * Free XML parser on desturct. */ public function __destruct() { xml_parser_free($this->parser); } /** * Parse XML data and trigger events. * * @param string $source XML source * @return \DOMDocument */ public function parse($source) { $this->clearDocument($source); $this->eventCache = []; if (0 === xml_parse($this->parser, $source, false)) { throw XMLParserException::create($this->parser); } // trigger collected events. $this->trigger(); $this->eventCache = []; // was not there, so lets close the document if ($this->depth > 0) { $this->document->appendChild($this->elements[0]); } return $this->document; } /** * Clear document. * * Method resets the parser instance if document->documentElement; // collect xml declaration if ('reset(); $matches = []; if (preg_match('/^<\?xml.*encoding=(\'|")([\w-]+)\1.*?>/i', $source, $matches)) { $this->encoding = $matches[2]; xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, $this->encoding); } } elseif (null !== $documentElement) { // clean the document /* @var $childNode \DOMNode */ while ($documentElement->hasChildNodes()) { $documentElement->removeChild($documentElement->firstChild); } } } /** * Starting tag found. * * @param resource $parser XML parser * @param string $name Element name * @param attribs $attribs Element attributes * @return void */ protected function startXml() { list (, $name, $attribs) = func_get_args(); $elementData = explode(static::NAMESPACE_SEPARATOR, $name, 2); $elementName = $elementData[0]; $prefix = null; if (isset($elementData[1])) { $elementName = $elementData[1]; $prefix = $elementData[0]; } $attributesNodes = $this->createAttributeNodes($attribs); $namespaceAttrib = false; // current namespace if (array_key_exists('xmlns', $attribs)) { $namespaceURI = $attribs['xmlns']; } else { $namespaceURI = $this->namespaces[$this->depth - 1]; } // namespace of the element if (null !== $prefix) { $namespaceElement = $this->namespacePrefixes[$prefix]; } else { $namespaceAttrib = true; $namespaceElement = $namespaceURI; } $this->namespaces[$this->depth] = $namespaceURI; // workaround for multiple xmlns defined, since we did have parent element inserted into the dom tree yet if (true === $namespaceAttrib) { $element = $this->document->createElement($elementName); } else { $elementNameFull = $elementName; if (null !== $prefix) { $elementNameFull = $prefix . static::NAMESPACE_SEPARATOR . $elementName; } $element = $this->document->createElementNS($namespaceElement, $elementNameFull); } foreach ($attributesNodes as $attributeNode) { $element->setAttributeNode($attributeNode); } $this->elements[$this->depth] = $element; $this->depth++; $event = '{' . $namespaceElement . '}' . $elementName; $this->cacheEvent($event, true, [$element]); } /** * Turn attribes into attribute nodes. * * @param array $attribs Attributes * @return array */ protected function createAttributeNodes(array $attribs) { $attributesNodes = []; foreach ($attribs as $name => $value) { // collect namespace prefixes if ('xmlns:' === substr($name, 0, 6)) { $prefix = substr($name, 6); $this->namespacePrefixes[$prefix] = $value; } else { $attribute = $this->document->createAttribute($name); $attribute->value = $value; $attributesNodes[] = $attribute; } } return $attributesNodes; } /** * End tag found. * * @return void */ protected function endXml() { $this->depth--; $element = $this->elements[$this->depth]; if ($this->depth > 0) { $parent = $this->elements[$this->depth - 1]; } else { $parent = $this->document; } $parent->appendChild($element); $localName = $element->localName; // Frist: try to get the namespace from element. $namespaceURI = $element->namespaceURI; // Second: loop over namespaces till namespace is not null if (null === $namespaceURI) { $namespaceURI = $this->namespaces[$this->depth]; } $event = '{' . $namespaceURI . '}' . $localName; $this->cacheEvent($event, false, [$element]); } /** * Data found. * * @param resource $parser XML parser * @param string $data Element data * @return void */ protected function dataXml() { $data = func_get_arg(1); if (isset($this->elements[$this->depth - 1])) { $element = $this->elements[$this->depth - 1]; $element->appendChild($this->document->createTextNode($data)); } } /** * Add event to cache. * * @param string $event * @param boolean $startTag * @param array $params * @return void */ protected function cacheEvent($event, $startTag, $params) { $this->eventCache[] = [$event, $startTag, $params]; } /** * Trigger cached events * * @return void */ protected function trigger() { foreach ($this->eventCache as $event) { list($event, $startTag, $param) = $event; $this->eventObject->setStartTag($startTag); $this->getEventManager()->setEventObject($this->eventObject); $this->getEventManager()->trigger($event, $this, $param); } } /** * Reset class properties. * * @return void */ public function reset() { $parser = xml_parser_create($this->encoding); xml_set_object($parser, $this); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); xml_set_element_handler($parser, 'startXml', 'endXml'); xml_set_character_data_handler($parser, 'dataXml'); $this->parser = $parser; $this->depth = 0; $this->document = new \DOMDocument('1.0', $this->encoding); $this->namespaces = []; $this->namespacePrefixes = []; $this->elements = []; } /** * Get XML parser resource. * * @return resource */ public function getParser() { return $this->parser; } /** * {@inheritDoc} */ public function getEventManager() { if (null === $this->events) { $this->setEventManager(new EventManager()); } return $this->events; } /** * {@inheritDoc} */ public function setEventManager(EventManagerInterface $events) { $this->events = $events; $events->setEventObject($this->getEventObject()); return $this; } /** * Get event object. * * @return XMLEventInterface */ public function getEventObject() { return $this->eventObject; } /** * Set event object. * * @param XMLEventInterface $eventObject * @return $this */ public function setEventObject(XMLEventInterface $eventObject) { $this->eventObject = $eventObject; return $this; } }