<?php

/**
 * Project: 
 * Contenido Content Management System
 * 
 * Description: 
 * Contenido XML Parser
 * 
 *
 * @package    Contenido_XML
 * @version    1.0.9
 * @author     Jan Lengowski
 * @copyright  four for business AG <www.4fb.de>
 * @license    http://www.contenido.org/license/LIZENZ.txt
 * @link       http://www.4fb.de
 * @link       http://www.contenido.org
 * @since      file available since contenido release <= 4.6
 */

if (!defined('CON_FRAMEWORK')) {
    die('Illegal call');
}
 /**
 * @author Jan Lengowski <Jan.Lengowski@4fb.de>
 * @copyright four for business AG <www.4fb.de>
 * @version 1.0
 */
class clXmlParser {

    /**
     * XML Parser autofree
     *
     * @var bool
     */
    var $autofree = true;

    /**
     * XML Parser object
     *
     * @var object
     * @access private
     */
    var $parser;

    /**
     * Error message
     *
     * @var string
     * @access private
     */
    var $error = '';

    /**
     * Element depth
     * @var int
     * @access private
     */
    var $depth = -1;

    /**
     * Element counter
     * @var int
     * @access private
     */
    var $count = 0;

    /**
     * Path counter
     * @var int
     * @access private
     */
    var $pcount = 0;

    /**
     * Array for creating the path
     * @var array
     * @access private
     */
    var $paths = array();

    /**
     * Data storage container for the path data
     * @var array
     * @access private
     */
    var $pathdata = array();

    /**
     * String storing the active path
     * @var string
     * @access private
     */
    var $activepath = '';

    /**
     * The active node
     * @var string
     * @access private
     */
    var $activenode = '';

    /**
     * The defined events
     * @var array
     * @access private
     */
    var $events = array();

    /**
     * Constructor function
     *
     * @access private
     * @param string	$sEncoding	Encoding used when parsing files (default: UTF-8, as in PHP5) 
     * @return void
     */
    function __construct($sEncoding = false) {
        if (!$sEncoding) {
            $sEncoding = "UTF-8";
        }

        $this->_init($sEncoding);
    }

    /**
     * Initialize the XML Parser object and sets all options
     *
     * @access private
     * @param string	$sEncoding	Encoding used when parsing files (default: UTF-8, as in PHP5)
     * @return void
     */
    function _init($sEncoding = false) {
        if (!$sEncoding) {
            $sEncoding = "UTF-8";
        }
        // Create parser instance
        $this->parser = xml_parser_create($sEncoding);

        // Set parser options
        xml_set_object($this->parser, $this);
        xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, false);
        xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, $sEncoding);

        xml_set_element_handler($this->parser, '_startElement', '_endElement');
        xml_set_character_data_handler($this->parser, '_characterData');
        xml_set_processing_instruction_handler($this->parser, '_processingInstruction');

        // Misc stuff
        $this->events['paths'] = array(NULL);
    }

    /**
     * Returns the XML error message
     *
     * @return string XML Error message
     * @access private
     */
    function _error() {
        $this->error = "XML error: " . xml_error_string(xml_get_error_code($this->parser)) . " at line " . xml_get_current_line_number($this->parser);
        return $this->error;
    }

    /**
     * Define events for the XML parser
     *
     * You can define handler functions/objects for start, end, PI and data sections (1.) or
     * your can define path which will trigger the defined event when encountered (2.)
     *
     * Example:
     *
     * 1.) $parser->setEvents(array("startElement"  		=> "myFunction", 
     * 								"endElement"    		=> "myFunction", 
     * 								"characterData" 		=> "myFunction", 
     * 								"processingInstruction" => "myFunction");
     *
     * The value can also be an array with the object reference and the method to call.
     * i.e. "startElement"=>array(&$myObj, "myMethod") instead of "startelement"=>"myFunction"
     *
     * 2.) $parser->setEvents(array("/root/foo/bar"=>"myFunction"));
     * 
     * Valid array keys are: 'startElement', 'endElement', 'characterData', 'processingInstruction' and paths
     * folowing the scheme '/root/element'. The path MUST begin from the root element and MUST start with '/'.
     *
     * The value can also be an array with the object reference and the method to call.
     * i.e. "/foo/bar"=>array(&$myObj, "myMethod") instead of "/foo/bar"=>"myFunction"
     *
     * @param array Options array, valid keys are 'startElement', 'endElement', 'characterData', 'processingInstruction', or a path
     *
     * @access public
     * @return void
     */
    function setEventHandlers($options = array(NULL)) {
        $options = $this->_changeKeyCase($options);

        if (array_key_exists('startelement', $options)) {
            $this->events['startelement'] = $options['startelement'];
        }

        if (array_key_exists('endelement', $options)) {
            $this->events['endelement'] = $options['endelement'];
        }

        if (array_key_exists('characterdata', $options)) {
            $this->events['characterdata'] = $options['characterdata'];
        }

        if (array_key_exists('processinginstruction', $options)) {
            $this->events['processinginstruction'] = $options['processinginstruction'];
        }

        $paths = $this->_getDefinedPaths($options);

        $this->events['paths'] = $paths;
    }

    /**
     * Set the processing instruction handler
     *
     * @param string Processing instruction handler
     * 
     * @return void
     * @access private
     */
    function _processingInstruction($parser, $target, $data) {
        $handler = $this->_getEventHandler('processinginstruction');

        if ($handler) {
            if (is_array($handler)) {
                $handler[0]->$handler[1]($target, $data);
            } else {
                $handler($target, $data);
            }
        }
    }

    /**
     * Change all array keys to lowercase 
     * (PHP function change_key_case is available at PHP 4.2 +)
     * 
     * @param array Source array
     * 
     * @return array Array with lowercased keys
     * @access private
     */
    function _changeKeyCase($options = array()) {
        $tmp = array();

        foreach ($options as $key => $value) {
            $tmp[strtolower($key)] = $value;
        }

        return $tmp;
    }

    /**
     * Returns events handlers if set
     *
     * @param string Event type
     *
     * @return sring Event handler name
     * @access private
     */
    function _getEventHandler($event) {
        // Standard events
        if (array_key_exists($event, $this->events)) {
            return $this->events[$event];
        }

        // Paths events
        if (array_key_exists($event, $this->events['paths'])) {
            return $this->events['paths'][$event];
        }

        // No events found
        return false;
    }

    /**
     * Return all defined paths from the options
     * array and returns a new array containing
     * only the paths to function bindings
     * 	
     * @param array Options array
     *
     * @return array Paths array
     * @access private
     */
    function _getDefinedPaths($options) {
        $tmp = array();

        foreach ($options as $key => $value) {
            if (strstr($key, '/')) {
                $tmp[$key] = $value;
            }
        }

        return $tmp;
    }

    /**
     * Add a path to the stack
     * 	
     * @param string Element node name
     * @access private
     * @return void
     */
    function _addPath($depth, $name) {
        $this->paths[$depth] = $name;
    }

    /**
     * Returns the current active path
     * 		 
     * @access private
     */
    function _getActivePath() {
        $tmp = array();

        for ($i = 0; $i <= $this->depth; $i++) {
            $tmp[] = $this->paths[$i];
        }

        $path = '/' . join('/', $tmp);
        return $path;
    }

    /**
     * XML start element handler 
     *
     * @param resource XML Parser resource
     * @param string XML Element node name
     * @param array XML Element node attributes
     *
     * @return void
     * @access private
     */
    function _startElement($parser, $name, $attribs) {
        // Increase depth
        $this->depth ++;

        // Set active node
        $this->activenode = $name;

        // Increase element counter
        if ($this->activenode == $this->pathdata[$this->activepath][$this->count]['name']) {
            $this->count ++;
        } else {
            $this->count = 0;
        }

        // Entering element context, add subpath
        $this->_addPath($this->depth, $name);

        // Get the handler for this event
        $handler = $this->_getEventHandler('startelement');

        // If a handler is defined call it
        if ($handler) {
            if (is_array($handler)) {
                $handler[0]->$handler[1]($name, $attribs);
            } else {
                $handler($name, $attribs);
            }
        }

        // Check for defined path handlers
        $this->activepath = $this->_getActivePath();

        // Save path data
        $this->pathdata[$this->activepath][$this->count] = array('name' => $name, 'attribs' => $attribs);
    }

    /**
     * XML character data handler 
     *
     * @param resource XML Parser resource
     * @param string XML node data
     *
     * @return void
     * @access private
     */
    function _characterData($parser, $data) {
        // Reset node count 
        if ($this->activenode != $this->pathdata[$this->activepath][$this->count]['name']) {
            $this->count = 0;
        }

        // Save path data
        $this->pathdata[$this->activepath][$this->count]['content'] .= $data;

        // Get the handler for this event
        $handler = $this->_getEventHandler('characterdata');

        // If a handler is defined call it
        if ($handler) {
            if (is_array($handler)) {
                $handler[0]->$handler[1]($data);
            } else {
                $handler($data);
            }
        }
    }

    /**
     * XML end element handler 
     *
     * @param resource XML Parser resource
     * @param string XML Element node name
     *
     * @return void
     * @access private
     */
    function _endElement($parser, $name) {
        // Get the handler for this event
        $handler = $this->_getEventHandler('endelement');

        // Call Element handler
        if ($handler) {
            if (is_array($handler)) {
                $handler[0]->$handler[1]($name);
            } else {
                $handler($name);
            }
        }

        // Reset the active path
        $this->activepath = $this->_getActivePath();

        // Get handler for the active path
        $handler = $this->_getEventHandler($this->activepath);

        // Call path handler
        if ($handler) {
            if (is_array($handler)) { // Handler is an object method
                $handler[0]->$handler[1]($this->pathdata[$this->activepath][$this->count]['name'],
                        $this->pathdata[$this->activepath][$this->count]['attribs'],
                        $this->pathdata[$this->activepath][$this->count]['content']);
            } else { // Handler is a function
                $handler($this->pathdata[$this->activepath][$this->count]['name'],
                        $this->pathdata[$this->activepath][$this->count]['attribs'],
                        $this->pathdata[$this->activepath][$this->count]['content']);
            }
        }

        // Decrease depth
        $this->depth --;
    }

    /**
     * Parse a XML string
     *
     * @param string XML data	 
     *
     * @return bool
     * @access public
     */
    function parse($data, $final = false) {
        $success = xml_parse($this->parser, trim($data), $final);

        if ($final && $this->autofree) {
            xml_parser_free($this->parser);
        }

        if (!$success) {
            return $this->_error();
        }

        return $success;
    }

    /**
     * Parse a XML file
     *
     * @param string File location	 
     *
     * @return bool
     * @access public
     */
    function parseFile($file) {
        if (!($fp = fopen($file, "rb"))) {
            
        }

        while ($sData = fread($fp, 4096)) {
            if (!xml_parse($this->parser, $sData, feof($fp))) {
                $this->_error();
                return false;
            }
        }

        if ($this->autofree) {
            xml_parser_free($this->parser);
        }

        return true;
    }

}

// XML_Parser
?>