<?php

//
// +----------------------------------------------------------------------+
// | <phpXML/> version 1.0                                                |
// | Copyright (c) 2001 Michael P. Mehl. All rights reserved.             |
// +----------------------------------------------------------------------+
// | Latest releases are available at http://phpxml.org/. For feedback or |
// | bug reports, please contact the author at mpm@phpxml.org. Thanks!    |
// +----------------------------------------------------------------------+
// | The contents of this file are subject to the Mozilla Public License  |
// | Version 1.1 (the "License"); you may not use this file except in     |
// | compliance with the License. You may obtain a copy of the License at |
// | http://www.mozilla.org/MPL/                                          |
// |                                                                      |
// | Software distributed under the License is distributed on an "AS IS"  |
// | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See  |
// | the License for the specific language governing rights and           |
// | limitations under the License.                                       |
// |                                                                      |
// | The Original Code is <phpXML/>.                                      |
// |                                                                      |
// | The Initial Developer of the Original Code is Michael P. Mehl.       |
// | Portions created by Michael P. Mehl are Copyright (C) 2001 Michael   |
// | P. Mehl. All Rights Reserved.                                        |
// +----------------------------------------------------------------------+
// | Authors:                                                             |
// |   Michael P. Mehl <mpm@phpxml.org>                                   |
// +----------------------------------------------------------------------+
//

/**
* Class for accessing XML data through the XPath language.
*
* This class offers methods for accessing the nodes of a XML document using 
* the XPath language. You can add or remove nodes, set or modify their 
* content and their attributes. No additional PHP extensions like DOM XML 
* or something similar are required to use these features.
*
* @package   Include
* @link      http://www.phpxml.org/ Latest release of this class
* @link      http://www.w3.org/TR/xpath W3C XPath Recommendation
* @copyright Copyright (c) 2001 Michael P. Mehl. All rights reserved.
* @author    Michael P. Mehl <mpm@phpxml.org>
* @version   1.0 (2001-03-08)
* @access    public
*/

class XML
{
    
/**
    * List of all document nodes.
    *
    * This array contains a list of all document nodes saved as an
    * associative array.
    *
    * @access private
    * @var    array
    */
    
var $nodes = array();
    
    
/**
    * List of document node IDs.
    *
    * This array contains a list of all IDs of all document nodes that
    * are used for counting when adding a new node.
    *
    * @access private
    * @var    array
    */
    
var $ids = array();
    
    
/**
    * Current document path.
    *
    * This variable saves the current path while parsing a XML file and adding
    * the nodes being read from the file.
    *
    * @access private
    * @var    string
    */
    
var $path "";
    
    
/**
    * Current document position.
    *
    * This variable counts the current document position while parsing a XML
    * file and adding the nodes being read from the file.
    *
    * @access private
    * @var    int
    */
    
var $position 0;
    
    
/**
    * Path of the document root.
    *
    * This string contains the full path to the node that acts as the root
    * node of the whole document.
    *
    * @access private
    * @var    string
    */
    
var $root "";
    
    
/**
    * Current XPath expression.
    *
    * This string contains the full XPath expression being parsed currently.
    *
    * @access private
    * @var    string
    */
    
var $xpath    "";
                                                                                
    
/**
    * List of entities to be converted.
    *
    * This array contains a list of entities to be converted when an XPath
    * expression is evaluated.
    *
    * @access private
    * @var    array
    */
    
var $entities = array ( "&" => "&amp;""<" => "&lt;"">" => "&gt;",
        
"'" => "&apos"'"' => "&quot;" );
    
    
/**
    * List of supported XPath axes.
    *
    * This array contains a list of all valid axes that can be evaluated in an
    * XPath expression.
    *
    * @access private
    * @var    array
    */
    
var $axes = array ( "child""descendant""parent""ancestor",
        
"following-sibling""preceding-sibling""following""preceding",
        
"attribute""namespace""self""descendant-or-self",
        
"ancestor-or-self" );
    
    
/**
    * List of supported XPath functions.
    *
    * This array contains a list of all valid functions that can be evaluated
    * in an XPath expression.
    *
    * @access private
    * @var    array
    */
    
var $functions = array ( "last""position""count""id""name",
        
"string""concat""starts-with""contains""substring-before",
        
"substring-after""substring""string-length""translate",
        
"boolean""not""true""false""lang""number""sum""floor",
        
"ceiling""round""text" );
    
    
/**
    * List of supported XPath operators.
    *
    * This array contains a list of all valid operators that can be evaluated
    * in a predicate of an XPath expression. The list is ordered by the
    * precedence of the operators (lowest precedence first).
    *
    * @access private
    * @var    array
    */
    
var $operators = array( " or "" and ""=""!=""<=""<"">="">",
        
"+""-""*"" div "" mod " );

    
/**
    * Constructor of the class.
    *
    * This constructor initializes the class and, when a filename is given,
    * tries to read and parse the given file.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $file Path and name of the file to read and parsed.
    * @see       load_file()
    */
    
function XML $file "" )
    {
        
// Check whether a file was given.
        
if ( !empty($file) )
        {
            
// Load the XML file.
            
$this->load_file($file);
        } 
/* if ( !empty($file) ) */
    
/* function XML ( $file = "" ) */

    /**
    * Reads a file and parses the XML data.
    *
    * This method reads the content of a XML file, tries to parse its
    * content and upon success stores the information retrieved from
    * the file into an array.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $file Path and name of the file to be read and parsed.
    * @see       handle_start_element(), handle_end_element(),
    *            handle_character_data()
    */
    
function load_file $file )
    {
        
// Check whether the file exists and is readable.
        
if ( file_exists($file) && is_readable($file) )
        {
            
// Read the content of the file.
            
$content implode(""file($file));
            
            
// Check whether content has been read.
            
if ( !empty($content) )
            {
                
// Create an XML parser.
                
$parser xml_parser_create();
                
                
// Set the options for parsing the XML data.
                
xml_parser_set_option($parserXML_OPTION_SKIP_WHITE1); 
                
xml_parser_set_option($parserXML_OPTION_CASE_FOLDING0);
                
                
// Set the object for the parser.
                
xml_set_object($parser, &$this);
                
                
// Set the element handlers for the parser.
                
xml_set_element_handler($parser"handle_start_element",
                    
"handle_end_element");
                
xml_set_character_data_handler($parser,
                    
"handle_character_data");
                
                
// Parse the XML file.
                
if ( !xml_parse($parser$contenttrue) )
                {
                    
// Display an error message.
                    
$this->display_error("XML error in file %s, line %d: %s",
                        
$filexml_get_current_line_number($parser),
                        
xml_error_string(xml_get_error_code($parser)));
                } 
/* if ( !xml_parse($parser, $cont... */
                
                // Free the parser.
                
xml_parser_free($parser);
            } 
/* if ( !empty($content) ) */
        
/* if ( file_exists($file) && is_... */
        
else
        {
            
// Display an error message.
            
$this->display_error("File %s could not be found or read."$file);
        } 
/* else */
    
/* function load_file ( $file ) */
    
    /**
    * Generates a XML file with the content of the current document.
    *
    * This method creates a string containing the XML data being read
    * and modified by this class before. This string can be used to save
    * a modified document back to a file or doing other nice things with
    * it.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     array $highlight Array containing a list of full document
    *            paths of nodes to be highlighted by <font>...</font> tags
    *            in the generated XML string.
    * @param     string $root While doing a recursion with this method, this
    *            parameter is used for internal purpose.
    * @param     int $level While doing a recursion with this method, this
    *            parameter is used for internal purpose.
    * @return    string The returned string contains well-formed XML data
    *            representing the content of this document.
    * @see       load_file(), evaluate(), get_content()
    */
    
function get_file $highlight = array(), $root ""$level )
    {
        
// Create a string to save the generated XML data.
        
$xml "";
        
        
// Create two strings containing the tags for highlighting a node.
        
$highlight_start "<font color=\"#FF0000\"><b>";
        
$highlight_end   "</b></font>";
        
        
// Generate a string to be displayed before the tags.
        
$before "";
        
        
// Calculate the amount of whitespaces to display.
        
for ( $i 0$i < ( $level ); $i++ )
        {
            
// Add a whitespaces to the string.
            
$before .= " ";
        } 
/* for ( $i = 0; $i < ( $level * ... */
        
        // Check whether a root node is given.
        
if ( empty($root) )
        {
            
// Set it to the document root.
            
$root $this->root;
        } 
/* if ( empty($root) ) */
        
        // Check whether the node is selected.
        
$selected in_array($root$highlight);
        
        
// Now add the whitespaces to the XML data.
        
$xml .= $before;
        
        
// Check whether the node is selected.
        
if ( $selected )
        {
            
// Add the highlight code to the XML data.
            
$xml .= $highlight_start;
        } 
/* if ( $selected ) */
        
        // Now open the tag.
        
$xml .= "&lt;".$this->nodes[$root]["name"];
        
        
// Check whether there are attributes for this node.
        
if ( count($this->nodes[$root]["attributes"]) > )
        {
            
// Run through all attributes.
            
foreach ( $this->nodes[$root]["attributes"] as $key => $value )
            {
                
// Check whether this attribute is highlighted.
                
if ( in_array($root."/attribute::".$key$highlight) )
                {
                    
// Add the highlight code to the XML data.
                    
$xml .= $highlight_start;
                } 
/* if ( in_array($root."/attribut... */
                
                // Add the attribute to the XML data.
                
$xml .= " ".$key."=\"".trim(stripslashes($value))."\"";
                
                
// Check whether this attribute is highlighted.
                
if ( in_array($root."/attribute::".$key$highlight) )
                {
                    
// Add the highlight code to the XML data.
                    
$xml .= $highlight_end;
                } 
/* if ( in_array($root."/attribut... */
            
/* foreach ( $this->nodes[$root][... */
        
/* if ( count($this->nodes[$root]... */
        
        // Check whether the node contains character data or has children.
        
if ( empty($this->nodes[$root]["text"]) &&
            !isset(
$this->nodes[$root]["children"]) )
        {
            
// Add the end to the tag.
            
$xml .= "/";
        } 
/* !isset($this->nodes[$root]["ch... */
        
        // Close the tag.
        
$xml .= "&gt;\n";
        
        
// Check whether the node is selected.
        
if ( $selected )
        {
            
// Add the highlight code to the XML data.
            
$xml .= $highlight_end;
        } 
/* if ( $selected ) */
        
        // Check whether the node contains character data.
        
if ( !empty($this->nodes[$root]["text"]) )
        {
            
// Add the character data to the XML data.
            
$xml .= $before."  ".$this->nodes[$root]["text"]."\n";
        } 
/* if ( !empty($this->nodes[$root... */
        
        // Check whether the node has children.
        
if ( isset($this->nodes[$root]["children"]) )
        {
            
// Run through all children with different names.
            
foreach ( $this->nodes[$root]["children"] as $child => $pos )
            {
                
// Run through all children with the same name.
                
for ( $i 1$i <= $pos$i++ )
                {
                    
// Generate the full path of the child.
                    
$fullchild $root."/".$child."[".$i."]";
                    
                    
// Add the child's XML data to the existing data.
                    
$xml .= $this->get_file($highlight$fullchild,
                        
$level 1);
                } 
/* for ( $i = 1; $i <= $pos; $i++... */
            
/* foreach ( $this->nodes[$root][... */
        
/* if ( isset($this->nodes[$root]... */
        
        // Check whether there are attributes for this node.
        
if ( !empty($this->nodes[$root]["text"]) ||
            isset(
$this->nodes[$root]["children"]) )
        {
            
// Add the whitespaces to the XML data.
            
$xml .= $before;
            
            
// Check whether the node is selected.
            
if ( $selected )
            {
                
// Add the highlight code to the XML data.
                
$xml .= $highlight_start;
            } 
/* if ( $selected ) */
            
            // Add the closing tag.
            
$xml .= "&lt;/".$this->nodes[$root]["name"]."&gt;";
            
            
// Check whether the node is selected.
            
if ( $selected )
            {
                
// Add the highlight code to the XML data.
                
$xml .= $highlight_end;
            } 
/* if ( $selected ) */
            
            // Add a linebreak.
            
$xml .= "\n";
        } 
/* isset($this->nodes[$root]["chi... */
        
        // Return the XML data.
        
return $xml;
    } 
/* function get_file ( $highlight... */
    
    /**
    * Adds a new node to the XML document.
    *
    * This method adds a new node to the tree of nodes of the XML document
    * being handled by this class. The new node is created according to the
    * parameters passed to this method.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $content Full path of the parent, to which the new
    *            node should be added as a child.
    * @param     string $name Name of the new node.
    * @return    string The string returned by this method will contain the
    *            full document path of the created node.
    * @see       remove_node(), evaluate()
    */
    
function add_node $context$name )
    {
        
// Check whether a name for this element is already set.
        
if ( empty($this->root) )
        {
            
// Use this tag as the root element.
            
$this->root "/".$name."[1]";
        } 
/* if ( empty($this->root) ) */
        
        // Calculate the full path for this element.
        
$path $context."/".$name;
        
        
// Set the relative context and the position.
        
$position = ++$this->ids[$path];
        
$relative $name."[".$position."]";
        
        
// Calculate the full path.
        
$fullpath $context."/".$relative;
        
        
// Calculate the context position, which is the position of this
        // element within elements of the same name in the parent node.
        
$this->nodes[$fullpath]["context-position"] = $position;
        
        
// Calculate the position for the following and preceding axis
        // detection.
        
$this->nodes[$fullpath]["document-position"] =
            
$this->nodes[$context]["document-position"] + 1;
        
        
// Save the information about the node.
        
$this->nodes[$fullpath]["name"]   = $name;
        
$this->nodes[$fullpath]["text"]   = "";
        
$this->nodes[$fullpath]["parent"] = $context;
        
        
// Add this element to the element count array.
        
if ( !$this->nodes[$context]["children"][$name] )
        {
            
// Set the default name.
            
$this->nodes[$context]["children"][$name] = 1;
        } 
/* if ( !$this->nodes[$context]["... */
        
else
        {
            
// Calculate the name.
            
$this->nodes[$context]["children"][$name] =
                
$this->nodes[$context]["children"][$name] + 1;
        } 
/* else */
        
        // Return the path of the new node.
        
return $fullpath;
    } 
/* function add_node ( $context, ... */

    /**
    * Removes a node from the XML document.
    *
    * This method removes a node from the tree of nodes of the XML document.
    * If the node is a document node, all children of the node and its
    * character data will be removed. If the node is an attribute node,
    * only this attribute will be removed, the node to which the attribute
    * belongs as well as its children will remain unmodified.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $node Full path of the node to be removed.
    * @see       add_node(), evaluate()
    */
    
function remove_node $node )
    {
        
// Check whether the node is an attribute node.
        
if ( ereg("/attribute::"$node) )
        {
            
// Get the path to the attribute node's parent.
            
$parent $this->prestr($node"/attribute::");
            
            
// Get the name of the attribute.
            
$attribute $this->afterstr($node"/attribute::");
            
            
// Check whether the attribute exists.
            
if ( isset($this->nodes[$parent]["attributes"][$attribute]) )
            {
                
// Create a new array.
                
$new = array();
                
                
// Run through the existing attributes.
                
foreach ( $this->nodes[$parent]["attributes"]
                    as 
$key => $value )
                {
                    
// Check whether it's the attribute to remove.
                    
if ( $key != $attribute )
                    {
                        
// Add it to the new array again.
                        
$new[$key] = $value;
                    } 
/* if ( $key != $attribute ) */
                
/* as $key => $value ) */
                
                // Save the new attributes.
                
$this->nodes[$parent]["attributes"] = $new;
            } 
/* if ( isset($this->nodes[$paren... */
        
/* if ( ereg("/attribute::", $nod... */
        
else
        {
            
// Create an associative array, which contains information about
            // all nodes that required to be renamed.
            
$rename = array();
            
            
// Get the name, the parent and the siblings of current node.
            
$name     $this->nodes[$node]["name"];
            
$parent   $this->nodes[$node]["parent"];
            
$siblings $this->nodes[$parent]["children"][$name];
            
            
// Decrease the number of children.
            
$this->nodes[$parent]["children"][$name]--;
            
            
// Create a counter for renumbering the siblings.
            
$counter 1;
            
            
// Now run through the siblings.
            
for ( $i 1$i <= $siblings$i++ )
            {
                
// Create the name of the sibling.
                
$sibling $parent."/".$name."[".$i."]";
                
                
// Check whether it's the name of the current node.
                
if ( $sibling != $node )
                {
                    
// Create the new name for the sibling.
                    
$new $parent."/".$name."[".$counter."]";
                    
                    
// Increase the counter.
                    
$counter++;
                    
                    
// Add the old and the new name to the list of nodes
                    // to be renamed.
                    
$rename[$sibling] = $new;
                } 
/* if ( $sibling != $node ) */
            
/* for ( $i = 1; $i <= $siblings;... */
            
            // Create an array for saving the new node-list.
            
$nodes = array();
            
            
// Now run through through the existing nodes.
            
foreach ( $this->nodes as $name => $values )
            {
                
// Check the position of the path of the node to be deleted
                // in the path of the current node.
                
$position strpos($name$node);

                
// Check whether it's not the node to be deleted.
                
if ( $position === false )
                {
                    
// Run through the array of nodes to be renamed.
                    
foreach ( $rename as $old => $new )
                    {
                        
// Check whether this node and it's parent requires to
                        // be renamed.
                        
$name             str_replace($old$new$name);
                        
$values["parent"] = str_replace($old$new,
                            
$values["parent"]);
                    } 
/* foreach ( $rename as $old => $... */
                    
                    // Add the node to the list of nodes.
                    
$nodes[$name] = $values;
                } 
/* if ( $position === false ) */
            
/* foreach ( $this->nodes as $nam... */
            
            // Save the new array of nodes.
            
$this->nodes $nodes;
        } 
/* else */
    
/* function remove_node ( $node ) */

    /**
    * Add content to a node.
    *
    * This method adds content to a node. If it's an attribute node, then
    * the value of the attribute will be set, otherwise the character data of
    * the node will be set. The content is appended to existing content,
    * so nothing will be overwritten.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $path Full document path of the node.
    * @param     string $value String containing the content to be added.
    * @see       get_content(), evaluate()
    */
    
function add_content $path$value )
    {
        
// Check whether it's an attribute node.
        
if ( ereg("/attribute::"$path) )
        {
            
// Get the path to the attribute node's parent.
            
$parent $this->prestr($path"/attribute::");
            
            
// Get the parent node.
            
$parent $this->nodes[$parent];
            
            
// Get the name of the attribute.
            
$attribute $this->afterstr($path"/attribute::");
            
            
// Set the attribute.
            
$parent["attributes"][$attribute] .= $value;
        } 
/* if ( ereg("/attribute::", $pat... */
        
else
        {
            
// Set the character data of the node.
            
$this->nodes[$path]["text"] .= $value;
        } 
/* else */
    
/* function add_content ( $path, ... */
    
    /**
    * Set the content of a node.
    *
    * This method sets the content of a node. If it's an attribute node, then
    * the value of the attribute will be set, otherwise the character data of
    * the node will be set. Existing content will be overwritten.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $path Full document path of the node.
    * @param     string $value String containing the content to be set.
    * @see       get_content(), evaluate()
    */
    
function set_content $path$value )
    {
        
// Check whether it's an attribute node.
        
if ( ereg("/attribute::"$path) )
        {
            
// Get the path to the attribute node's parent.
            
$parent $this->prestr($path"/attribute::");
            
            
// Get the parent node.
            
$parent $this->nodes[$parent];
            
            
// Get the name of the attribute.
            
$attribute $this->afterstr($path"/attribute::");
            
            
// Set the attribute.
            
$parent["attributes"][$attribute] = $value;
        } 
/* if ( ereg("/attribute::", $pat... */
        
else
        {
            
// Set the character data of the node.
            
$this->nodes[$path]["text"] = $value;
        } 
/* else */
    
/* function set_content ( $path, ... */
    
    /**
    * Retrieves the content of a node.
    *
    * This method retrieves the content of a node. If it's an attribute
    * node, then the value of the attribute will be retrieved, otherwise
    * it'll be the character data of the node.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $path Full document path of the node, from which the
    *            content should be retrieved.
    * @return    string The returned string contains either the value or the
    *            character data of the node.
    * @see       set_content(), evaluate()
    */
    
function get_content $path )
    {
        
// Check whether it's an attribute node.
        
if ( ereg("/attribute::"$path) )
        {
            
// Get the path to the attribute node's parent.
            
$parent $this->prestr($path"/attribute::");
            
            
// Get the parent node.
            
$parent $this->nodes[$parent];
            
            
// Get the name of the attribute.
            
$attribute $this->afterstr($path"/attribute::");
            
            
// Get the attribute.
            
$attribute $parent["attributes"][$attribute];
            
            
// Return the value of the attribute.
            
return $attribute;
        } 
/* if ( ereg("/attribute::", $pat... */
        
else
        {
            
// Return the cdata of the node.
            
return stripslashes($this->nodes[$path]["text"]);
        } 
/* else */
    
/* function get_content ( $path ) */
    
    /**
    * Add attributes to a node.
    *
    * This method adds attributes to a node. Existing attributes will not be
    * overwritten.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $path Full document path of the node, the attributes
    *            should be added to.
    * @param     array $attributes Associative array containing the new
    *            attributes for the node.
    * @see       set_content(), get_content()
    */
    
function add_attributes $path$attributes )
    {
        
// Add the attributes to the node.
        
$this->nodes[$path]["attributes"] = array_merge($attributes,
            
$this->nodes[$path]["attributes"]);
    } 
/* function add_attributes ( $pat... */
    
    /**
    * Sets the attributes of a node.
    *
    * This method sets the attributes of a node and overwrites all existing
    * attributes by doing this.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $path Full document path of the node, the attributes
    *            of which should be set.
    * @param     array $attributes Associative array containing the new
    *            attributes for the node.
    * @see       set_content(), get_content()
    */
    
function set_attributes $path$attributes )
    {
        
// Set the attributes of the node.
        
$this->nodes[$path]["attributes"] = $attributes;
    } 
/* function set_attributes ( $pat... */
    
    /**
    * Retrieves a list of all attributes of a node.
    *
    * This method retrieves a list of all attributes of the node specified in
    * the argument.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $path Full document path of the node, from which the
    *            list of attributes should be retrieved.
    * @return    array The returned associative array contains the all
    *            attributes of the specified node.
    * @see       get_content(), $nodes, $ids
    */
    
function get_attributes $path )
    {
        
// Return the attributes of the node.
        
return $this->nodes[$path]["attributes"];
    } 
/* function get_attributes ( $pat... */
    
    /**
    * Retrieves the name of a document node.
    *
    * This method retrieves the name of document node specified in the
    * argument.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $path Full document path of the node, from which the
    *            name should be retrieved.
    * @return    string The returned array contains the name of the specified
    *            node.
    * @see       get_content(), $nodes, $ids
    */
    
function get_name $path )
    {
        
// Return the name of the node.
        
return $this->nodes[$path]["name"];
    } 
/* function get_name ( $path ) */
    
    /**
    * Evaluates an XPath expression.
    *
    * This method tries to evaluate an XPath expression by parsing it. A
    * XML document has to be read before this method is able to work.
    *
    * @access    public
    * @author    Michael P. Mehl <mpm@phpxml.org>
    * @param     string $path XPath expression to be evaluated.
    * @param     string $context Full path of a document node, starting
    *            from which the XPath expression&nbs