<?php
/**
* @version	$Id: xml_helper.php 14001 2010-10-19 19:10:59Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	class kXMLHelper extends kHelper {

		var $RootElement = null;

		/**
		 * Enter description here...
		 *
		 * @var kXMLNode
		 */
		var $CurrentElement = null;

		var $Mode;

		var $XMLNodeClassName = 'kXMLNode';

		function Init($prefix, $special, $event_params = null)
		{
			parent::Init($prefix, $special, $event_params);

			if ( version_compare(PHP_VERSION, '5.0.0') === 1 ) {
				$this->XMLNodeClassName = 'kXMLNode5';
				k4_include_once( dirname(__FILE__) . DIRECTORY_SEPARATOR . 'xml_helper5.php' );
			}
		}

		/**
		 * Parses XML data specified and returns root node
		 *
		 * @param string $xml
		 * @param int $mode
		 * @param bool $no_case_folding
		 * @return kXMLNode
		 */
		function &Parse($xml = null, $mode = null, $no_case_folding = false)
		{
			$xml = trim($xml);
			$this->Mode = !isset($mode) ? XML_NO_TEXT_NODES : $mode;
			$this->Clear(); // in case if Parse method is called more then one time
			$xml_parser = xml_parser_create();

			if ($no_case_folding) {
				xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, 0);
			}

			xml_set_element_handler( $xml_parser, Array(&$this, 'startElement'), Array(&$this, 'endElement') );
			xml_set_character_data_handler( $xml_parser, Array(&$this, 'characterData') );

			if ( !xml_parse($xml_parser, $xml, 1) ) {
				$class_name = $this->XMLNodeClassName;
				$byte = xml_get_current_byte_index($xml_parser);
				$extract = '...' . mb_substr($xml, $byte-50, 50) . ' !!![' . mb_substr($xml, $byte, 1) . ']!!! '.mb_substr($xml, $byte+1, 50) . '...';

				$message = sprintf(
					'XML error number %s: %s at line %d col %d, byte %d, extract: %s',
					xml_get_error_code($xml_parser),
					xml_error_string(xml_get_error_code($xml_parser)),
					xml_get_current_line_number($xml_parser),
					xml_get_current_column_number($xml_parser),
					xml_get_current_byte_index($xml_parser),
					$extract
				);

				$this->RootElement =& new $class_name(
					'ERROR',
					array(
						'code' => xml_get_error_code($xml_parser),
						'message' => $message
					)
				);

			    trigger_error($message, E_USER_WARNING);
			}

			xml_parser_free($xml_parser);

			$root_copy = $this->RootElement;
			unset($this->RootElement);
			unset($this->CurrentElement);

			return $root_copy;
		}

		function ConvertHTMLEntities($s)
		{
			//build first an assoc. array with the entities we want to match
			$table1 = get_html_translation_table(HTML_ENTITIES, ENT_QUOTES);

			$patterns = array();
			$replacements = array();
			//now build another assoc. array with the entities we want to replace (numeric entities)
			foreach ($table1 as $k=>$v){
			  $patterns[] = "/$v/";
		//	  $c = htmlentities($k,ENT_QUOTES,"UTF-8");
			  $replacements[] = "&#".ord($k).";";
			}

			//now perform a replacement using preg_replace
			//each matched value in array 1 will be replaced with the corresponding value in array 2
			$s = preg_replace($patterns,$replacements,$s);

			return $s;
		}

		function startElement(&$Parser, &$Elem, $Attrs)
		{
			$parent =& $this->CurrentElement; // 1. $parent is now reference to $this->CurrentElement
			$class_name = $this->XMLNodeClassName;
			$this->CurrentElement =& new $class_name($Elem, $Attrs); // 2. =& ensures, that new object won't be assigned to $parent as well (don't remove)

			if (!isset($this->RootElement) || is_null($this->RootElement)) {
				$this->RootElement =& $this->CurrentElement;
			}

			if (!is_null($parent)) {
				$parent->AddChild($this->CurrentElement);
			}
		}

		function characterData($Parser, $Line)
		{
			if ($this->Mode == XML_WITH_TEXT_NODES) {
				$class_name = $this->XMLNodeClassName;
				$text_node = new $class_name('_TEXT_');
				$text_node->AppendData($Line);
				$this->CurrentElement->AddChild( $text_node );
			}
			else {
				$this->CurrentElement->AppendData($Line);
			}
		}

		function endElement($Parser, $Elem)
		{
			if ($this->Mode == XML_WITH_TEXT_NODES) {
				/*if (count($this->CurrentElement->Children) == 1 && $this->CurrentElement->firstChild->Name == '_TEXT_') {
					$this->CurrentElement->Children = array();
				}*/
			}

			if ($this->CurrentElement->Parent != null) {
				$this->CurrentElement =& $this->CurrentElement->Parent;
			}
		}

		function Clear()
		{
			unset($this->RootElement);
			unset($this->CurrentElement);
		}

		function &CreateNode($name, $value=null, $attributes=array())
		{
			$class_name = $this->XMLNodeClassName;
			$node = new $class_name($name, $attributes);
			/* @var $node kXMLNode */

			if ($value) {
				$node->SetData($value);
			}
			return $node;
		}
	}

	class kXMLNode {

		/**
		 * Casefolded name of this node
		 *
		 * @var string
		 */
		var $Name = null;

		/**
		 * Original name of this node
		 *
		 * @var string
		 */
		var $OriginalName = null;

		/**
		 * Casefolded attributes of this node
		 *
		 * @var Array
		 */
		var $Attributes = array();

		/**
		 * Original attributes of this node
		 *
		 * @var Array
		 */
		var $OriginalAttributes = array();

		/**
		 * List of node childnodes
		 *
		 * @var Array
		 */
		var $Children = array();

		/**
		 * Node content (usually text)
		 *
		 * @var string
		 */
		var $Data = null;

		/**
		 * Reference to first child
		 *
		 * @var kXMLNode
		 */
		var $firstChild = null;

		/**
		 * Last child of this node
		 *
		 * @var kXMLNode
		 */
		var $lastChild = null;

		/**
		 * Parent node
		 *
		 * @var kXMLNode
		 */
		var $Parent = null;

		/**
		 * Node position relative to other nodes of it's parent
		 *
		 * @var int
		 */
		var $Position = 0;

		/**
		 * Node identifier
		 *
		 * @var int
		 */
		var $CRC = null;

		function kXMLNode($name, $attrs = array())
		{
			$this->Name = strtoupper($name);
			$this->OriginalName = $name;
			$this->OriginalAttributes = $attrs;

			foreach ($attrs as $attr => $value) {
				$this->Attributes[ strtoupper($attr) ] = $value;
			}

			$this->CRC = crc32($this->Name.join(array_keys($this->Attributes)).join(array_values($this->Attributes)));
		}

		/**
		 * Returns attribute value, first checking it casesensitively, then caseinsensitively
		 * If attribute is not set returns default value (if passed), or false otherwise
		 *
		 * @param string $name
		 * @param mixed $default
		 * @return string
		 */
		function GetAttribute($name, $default=false)
		{
			if (isset($this->OriginalAttributes[$name])) {
				return $this->OriginalAttributes[$name];
			}

			return isset($this->Attributes[strtoupper($name)]) ? $this->Attributes[strtoupper($name)] : $default;
		}

		function SetParent(&$elem)
		{
			$this->Parent =& $elem;
		}

		/**
		 * Adds new child to current node
		 *
		 * @param kXMLNode $a_child
		 */
		function AddChild(&$a_child)
		{
			$node_count = count($this->Children);
			$a_child->Position = $node_count;

			if ($node_count == 0) {
				$this->firstChild =& $a_child;
				$this->lastChild =& $a_child;
			}
			else {
				$this->lastChild =& $a_child;
			}

			$this->Children[] =& $a_child;
			$a_child->SetParent($this);
		}

		/**
		 * Appends data to current node
		 *
		 * @param string $data
		 */
		function AppendData($data)
		{
			$this->Data .= $data;
		}

		/**
		 * Returns child node by given path
		 *
		 * @param string $path
		 * @return kXMLNode
		 */
		function &GetChild($path)
		{
			$entries = explode('/', strtoupper($path));
			$cur = array_shift($entries);
			if ($cur == $this->Name) $cur = array_shift($entries);
			if (!$cur) return $this;
			if (!isset($this->Children[$cur])) return false;
			$left = implode('/', $entries);
			if (!$left) return $this->Children[$cur];
			return $this->Children[$cur]->GetChild($left);
		}

		function &GetFirstChild()
		{
			return $this->firstChild;
		}

		/**
		 * Returns node value by given path
		 *
		 * @param string $path
		 * @return string
		 */
		function GetChildValue($path)
		{
			$child =& $this->GetChild($path);
			if ($child !== false) {
				return $child->Data;
			}
		}

		/**
		 * Returns child node by given position among it siblings
		 *
		 * @param int $position
		 * @return kXMLNode
		 */
		function &GetChildByPosition($position)
		{
			if ($position < count($this->Children) ) {
				return $this->Children[$position];
			}
			else {
				$false = false;
				return $false;
			}
		}

		/**
		 * Recursively searches for child with given name under current node
		 *
		 * @param string $name
		 * @return kXMLNode
		 */
		function &FindChild($name)
		{
			$name = strtoupper($name);
			if ($this->Name == $name) return $this;
	//		if (isset($this->Children[$name])) return $this->Children[$name];
	//		$children = array_keys($this->Children);
			foreach ($this->Children as $elem)
			{
				$child =& $elem->FindChild($name);
				if ($child !== false)
				{
					return $child;
				}
			}
			if (isset($child) && is_object($child)) {
				$child->_destruct();
			}
			unset($child);
			$false = false;
			return $false;
		}

		/**
		 * Retruns value of given child or value of it's attribute
		 *
		 * @param string $name
		 * @param string $attr
		 * @return string
		 */
		function FindChildValue($name, $attr=null)
		{
			$child =& $this->FindChild($name);

			if ($child !== false) {
				if (isset($attr)) {
					return $child->Attributes[strtoupper($attr)];
				}

				return $child->Data;
			}
		}

		/**
		 * Returns next node to this, false in case of end list
		 *
		 * @return kXMLNode
		 */
		function &PrevSibling()
		{
			if (!is_null($this->Parent) && $this->Position > 0) {
				$pos = $this->Position - 1;
				do {
					$ret =& $this->Parent->GetChildByPosition($pos--);
				} while ($ret->Name == '_TEXT_' && $pos >= 0);
				if ($ret->Name == '_TEXT_') $ret = false;
				return $ret;
			}
			else {
				$false = false;
				return $false;
			}
		}

		/**
		 * Returns next node to this, false in case of end list
		 *
		 * @return kXMLNode
		 */
		function &NextSibling()
		{
			if (!is_null($this->Parent)) {
				$pos = $this->Position + 1;
				do {
					$ret =& $this->Parent->GetChildByPosition($pos++);
				} while ($pos < count($this->Parent->Children) && ($ret->Name == '_TEXT_'));

				if (is_object($ret) && ($ret->Name == '_TEXT_')) {
					$ret = false;
				}
				return $ret;
			}
			else {
				$false = false;
				return $false;
			}
		}
		/**
		 * Reconstructs XML of the node and subnodes
		 *
		 * $param bool $content_only
		 */
		function GetXML($content_only = false)
		{
			$xml = '';
			$single = (!$this->Data && count($this->Children) == 0);

			if (!$content_only) {
				$xml = '<'.$this->OriginalName;

				if (count($this->OriginalAttributes)) {
					$xml .= ' ';
					$att_contents = array();
					foreach ($this->OriginalAttributes as $name => $value) {
						$att_contents[] = $name.'="'.htmlspecialchars($value).'"';
					}
					$xml .= implode(' ', $att_contents);
				}

				$xml .= $single ? '/>' : '>';
			}

			if (!$single) {
				if ($content_only) {
					$xml .= $this->Data;
				}
				else {
					$xml .= preg_match('/&|</', $this->Data) ? '<![CDATA['.$this->Data.']]>' : $this->Data;
				}

				foreach ($this->Children as $node) {
					/* @var $node kXMLNode */

					$xml .= $node->GetXML($node->Name == '_TEXT_' ? true : false);
				}

				if (!$content_only) {
					$xml .= '</'.$this->OriginalName.'>';
				}
			}

			return $xml;
		}

		function RemoveChild($name)
		{
			$child =& $this->FindChild($name);
			$parent =& $child->Parent;
			$pos = $child->Position;
			array_splice($parent->Children, $pos, 1);
			for ($i=$pos; $i < count($parent->Children); $i++) {
				$parent->Children[$i]->Position = $i;
			}
			$parent->firstChild =& $parent->Children[0];
			$parent->lastChild =& $parent->Children[count($parent->Children)-1];
		}

		function ReplaceChild($name, &$replacement)
		{
			$child =& $this->FindChild($name);
			$parent =& $child->Parent;
			$pos = $child->Position;
			array_splice($parent->Children, $pos, 1, array($replacement));
			$replacement->Parent =& $parent;
			$replacement->Position = $pos;
			$parent->firstChild =& $parent->Children[0];
			$parent->lastChild =& $parent->Children[count($parent->Children)-1];
		}

		function SetName($name)
		{
			$this->Name = strtoupper($name);
			$this->OriginalName = $name;
		}

		function SetData($data)
		{
			$this->Data = $data;
		}

		function SetAttribute($name, $value)
		{
			$this->Attributes[strtoupper($name)] = $value;
			$this->OriginalAttributes[$name] = $value;
		}
	}