444 lines
12 KiB
PHP

<?php
/**
* Copyright 2014 Fabian Grutschus. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the copyright holders.
*
* @author Fabian Grutschus <f.grutschus@lubyte.de>
* @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 = [];
// </stream> 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 <?xml is found. Overwise it clears the DOM document.
*
* @return void
*/
protected function clearDocument($source)
{
$documentElement = $this->document->documentElement;
// collect xml declaration
if ('<?xml' === substr($source, 0, 5)) {
$this->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;
}
}