<?php
# XMLFile -- Initial version June 1, 2000 
# May 29, 2002
#  Fixed to work with later versions of PHP that have deprecated the call-time
#  pass by reference syntax.  This may break versions of PHP older than 3, so
#  if you are using this on those versions, go with an earlier version.
#  Also added some more comments on how to use it to create XML files.  Some
#  people (a surprising number of them, in fact) were having difficulty
#  understanding the recursive nature of XML files, and that each tag has
#  subtags, each of which can have subtags, each of which....
# July 12, 2001
#  Fixed an oops in the find_subtags_by_name function.
# July 11, 2001
#  Incorporated Kevin Howe's read_xml_string function.
#  Incorporated Mike Konkov's find_subtags_by_name function (with some changes).
#  Fixed an errant documentation comment (open instead of fopen -- oops).
#
# September 29, 2000
# by Chris Monson -- e408345b17be3ce90059d01d96be0599@orangatango.com This PHP module is licensed under the GNU LGPL (www.gnu.org)
# Please become familiar with this license before sending this thing all over
# the place!  I would like to have any changes submitted back to me, and this
# comment must be included with any distribution of this component.
#
# The purpose of this module was to provide a simple interface for dealing with
# small to medium sized XML files.  It is probably not suitable for very large
# files since it reads the entire file into a structure in memory.  For very
# large files, the XML parsing functions should be used more or less directly
# so that pieces of the file can be dealt with as they are read in.
#
# The basic idea is this: Read the XML file into an internal tree structure
# that is easy to traverse.  This module also allows you to create such a
# structure in memory and then write it out to disk.  The output is formatted
# in a nice, readable way, using whitespace to delimit tag containment, etc.
# It makes it very easy to write nice-looking XML files.
#
# I have included some usage comments.  They are almost certainly incomplete.
# If you get stumped, first use the source, then email me.
#
#------------------------------------------------------------------------------
### USAGE ###
#------------------------------------------------------------------------------
# Reading an XML file:
#------------------------------------------------------------------------------
#
# $xml = new XMLFile();
# $fh = fopen( 'myxmlfile.xml', 'r' );
# $xml->read_file_handle( $fh );
# close( $fh );
#
# Now the tags can be accessed via the root node of $xml:
#
# $root = &$xml->roottag;
# $tagname = $root->name;
# $tagdata = $root->cdata;
#
# Note that a tag is of the following form:
# <NAME attribute=value>CDATA</NAME>
# Each tag contains an attributes array, which is associative by nature.  In
# other words, you would access the value of "attribute" as follows:
#
# $value = $root->attributes['attribute'];
#
# Also, each tag has a 'tags' proprerty, which is an ordered array (integer
# indices, not associative!) which has the tags that were contained within
# this tag in their order of appearance.  The reason that this is not
# associative is that there can be multiple tags of the same name.  There is
# nothing in the XML spec (barring a DTD) that declares the uniqueness of tag
# names.  For example:
#
# <OUTER>
#     <INNER>CDATA</INNER>
#     <INNER name="hello"/>
# </OUTER>
#
# In the above example, the outer tag would have a tags array that has two
# entries, each of which has a tag name of "INNER".  The one with CDATA wrapped
# inside would be at index 0, and the other would be at index 1.
#
# Once you have finished with the XMLFile object, you need to call the cleanup
# method.  If you don't, you will get a memory leak, since PHP is reference
# counted and each element in the tree refers to its parent.  'cleanup' simply
# traverses the tree and disconnects the parent pointers.  The PHP cleans up
# everything else.
#
# $xml->cleanup();
# 
# Note that you can change the elements, delete tags, and do other things
# to the tree once it has been read from the file.  After it has been changed,
# you can write it back out and the file will reflect your changes.
#------------------------------------------------------------------------------
# Writing a new file:
# 
# $xml = new XMLFile();
# $xml->create_root(); # necessary -- no root is created until requested
# $xml->roottag->name = 'ROOT';
# $xml->roottag->add_subtag( 'INNER', array() );
# $innertag = &$xml->roottag->curtag;
# $innertag->add_subtag( 'REALLYINNER', array() );
# # Or, you can do this:
# $xml->roottag->curtag->add_subtag( 'INNER2', array() );
# # The point is that each tag can have subtags.  The most recently added
# # subtag is always the curtag of its parent.
# $xml->roottag->add_subtag( 'INNER', array( 'name' => 'value' ) );
# $xml->roottag->curtag->cdata = "Hello!"; # curtag is the most recent addition
# $fh = fopen( 'myxmlfile.xml', 'w' );
# $xml->write_file_handle( $fh );
# close( $fh );
#
# The file will look like this: (no space between ? and >)
#
# <?xml version="1.0" encoding="UTF-8" ? >
# <ROOT>
#     <INNER>
#           <REALLYINNER/>
#           <INNER2/>
#     </INNER>
#     <INNER name="value">Hello!</INNER>
# </ROOT>
#
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
#
if (!isset($XMLFile_Included) || !$XMLFile_Included) {
$XMLFile_Included = 1;

###############################################################################
class XMLTag
{
    var $cdata;
    var $attributes;
    var $name;
    var $tags;
    var $parent;

    var $curtag;

    function XMLTag(&$parent)
    {
        if (is_object( $parent ))
        {
            $this->parent = &$parent;
        }
        $this->_init();
    }

    function _init()
    {
        $this->attributes = array();
        $this->cdata = '';
        $this->name = '';
        $this->tags = array();
    }

    function add_subtag($name, $attributes=0)
    {
        $tag = new XMLTag( $this );
        $tag->set_name( $name );
        if (is_array($attributes)) {
            $tag->set_attributes( $attributes );
        }
        $this->tags[] = &$tag;
        $this->curtag = &$tag;
    }

    function find_subtags_by_name( $name )
    {
        $result = array();
        $found=false;
        for($i=0;$i<$this->num_subtags();$i++) 
		{
            if($this->tags[$i]->name==$name )
			{
                $found=true;
                $array2return[]=&$this->tags[$i];
            }
        }
        if($found) {
            return $array2return;
        }
        else {
            return false;
        }
    }

    function clear_subtags()
    {
        # Traverse the structure, removing the parent pointers
        $numtags = sizeof($this->tags);
        $keys = array_keys( $this->tags );
        foreach( $keys as $k ) {
            $this->tags[$k]->clear_subtags();
            unset($this->tags[$k]->parent);
        }

        # Clear the tags array
        $this->tags = array();
        unset( $this->curtag );
    }

    function remove_subtag($index)
    {
        if (is_object($this->tags[$index])) {
            unset($this->tags[$index]->parent);
            unset($this->tags[$index]);
        }
    }

    function num_subtags()
    {
        return sizeof( $this->tags );
    }

    function add_attribute( $name, $val )
    {
        $this->attributes[$name] = $val;
    }

    function clear_attributes()
    {
        $this->attributes = array();
    }

    function set_name( $name )
    {
        $this->name = $name;
    }

    function set_attributes( $attributes )
    {
        $this->attributes = (is_array($attributes)) ? $attributes : array();
    }

    function add_cdata( $data )
    {
        $this->cdata .= $data;
    }

    function clear_cdata()
    {
        $this->cdata = "";
    }

    function write_file_handle( $fh, $prepend_str='' )
    {
        # Get the attribute string
        $attrs = array();
        $attr_str = '';
        foreach( $this->attributes as $key => $val )
        {
            $attrs[] = $key . "=\"$val\"";
        }
        if ($attrs) {
            $attr_str = join( " ", $attrs );
        }
        # Write out the start element
        $tagstr = "$prepend_str<{$this->name}";
        if ($attr_str) {
            $tagstr .= " $attr_str";
        }

        $keys = array_keys( $this->tags );
        $numtags = sizeof( $keys );
        # If there are subtags and no data (only whitespace), 
        # then go ahead and add a carriage
        # return.  Otherwise the tag should be of this form:
        # <tag>val</tag>
        # If there are no subtags and no data, then the tag should be
        # closed: <tag attrib="val"/>
        $trimmeddata = trim( $this->cdata );
        if ($numtags && ($trimmeddata == "")) {
            $tagstr .= ">\n";
        }
        elseif (!$numtags && ($trimmeddata == "")) {
            $tagstr .= "/>\n";
        }
        else {
            $tagstr .= ">";
        }

        fwrite( $fh, $tagstr );

        # Write out the data if it is not purely whitespace
        if ($trimmeddata != "") {
            fwrite( $fh, $trimmeddata );
        }

        # Write out each subtag
        foreach( $keys as $k ) {
            $this->tags[$k]->write_file_handle( $fh, "$prepend_str\t" );
        }

        # Write out the end element if necessary
        if ($numtags || ($trimmeddata != "")) {
            $tagstr = "</{$this->name}>\n";
            if ($numtags) {
                $tagstr = "$prepend_str$tagstr";
            }
            fwrite( $fh, $tagstr );
        }
    }

}
###############################################################################
class XMLFile
{
    var $parser;
    var $roottag;
    var $curtag;

    function XMLFile()
    {
        $this->init();
    }

    # Until there is a suitable destructor mechanism, this needs to be
    # called when the file is no longer needed.  This calls the clear_subtags
    # method of the root node, which eliminates all circular references
    # in the xml tree.
    function cleanup()
    {
        if (is_object( $this->roottag )) {
            $this->roottag->clear_subtags();
        }
    }

    function init()
    {
        $this->roottag = "";
        $this->curtag = &$this->roottag;
    }

    function create_root()
    {
        $null = 0;
        $this->roottag = new XMLTag($null);
        $this->curtag = &$this->roottag;
    }

    # read_xml_string
    # Same as read_file_handle, but you pass it a string.  Note that
    # depending on the size of the XML, this could be rather memory intensive.
    # Contributed July 06, 2001 by Kevin Howe
    function read_xml_string( $str )
    {
        $this->init();
        $this->parser = xml_parser_create("UTF-8");
        xml_set_object( $this->parser, $this );
		xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0);
        xml_set_element_handler( $this->parser, "_tag_open", "_tag_close" );
        xml_set_character_data_handler( $this->parser, "_cdata" );
        xml_parse( $this->parser, $str );
        xml_parser_free( $this->parser );
    }

    function read_file_handle( $fh )
    {
        $this->init();
        $this->parser = xml_parser_create("UTF-8");
        xml_set_object( $this->parser, $this );
		xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0);
        xml_set_element_handler( $this->parser, "_tag_open", "_tag_close" );
        xml_set_character_data_handler( $this->parser, "_cdata" );

        while( $data = fread( $fh, 4096 )) {
            if (!xml_parse( $this->parser, $data, feof( $fh ) )) {
                die(sprintf("XML error: %s at line %d",
                    xml_error_string(xml_get_error_code($this->parser)),
                    xml_get_current_line_number($this->parser)));
            }
        }

        xml_parser_free( $this->parser );
    }

    function write_file_handle( $fh, $write_header=1 )
    {
        if ($write_header) {
            fwrite( $fh, "<?xml version='1.0' encoding='UTF-8'?>\n" );
        }

        # Start at the root and write out all of the tags
        $this->roottag->write_file_handle( $fh );
    }

    ###### UTIL #######

    function _tag_open( $parser, $tag, $attributes )
    {
        #print "tag_open: $parser, $tag, $attributes\n";
        # If the current tag is not set, then we are at the root
        if (!is_object($this->curtag)) {
            $null = 0;
            $this->curtag = new XMLTag($null);
            $this->curtag->set_name( $tag );
            $this->curtag->set_attributes( $attributes );
			
			
        }
        else { # otherwise, add it to the tag list and move curtag
            $this->curtag->add_subtag( $tag, $attributes );
            $this->curtag = &$this->curtag->curtag;
        }
    }

    function _tag_close( $parser, $tag )
    {
        # Move the current pointer up a level
        $this->curtag = &$this->curtag->parent;
    }

    function _cdata( $parser, $data )
    {
        $this->curtag->add_cdata( $data );
    }
}
###############################################################################
} // included
###############################################################################

