<?php
/**

xmlform.inc.php - Generic class to edit XML via HTML forms based on an XML schema.
---------------

LICENSE INFORMATION
-------------------
Copyright(c) 2003 by CRIA - Centro de Referencia em Informacao Ambiental
http://www.cria.org.br

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details:

http://www.gnu.org/copyleft/gpl.html


Main author: Renato De Giovanni <renato [at] cria.org.br>
-----------

**/

require_once('xpath/XPath.class.php');

///// Functions /////

/** Get value from post/get environment variables or return default 
 *
 * @param string name Parameter name
 * @param mixed defaultVal Default value to be used if parameter was not passed
 *
 * @return mixed Parameter value
 */
function getVal($name, $defaultVal=false)
{
  return (isset($_REQUEST[$name]) ? $_REQUEST[$name] : $defaultVal);
}

/**
 * Escapes XML special chars in a string (shamelessly copied from DiGIR_utils.php...)
 *
 * @param $s string String to be escaped (assumed to be in utf-8)
 *
 * @return string Escaped string
 */
function escChars($s)
{
    // Since "htmlspecialchars" does not work with utf-8 in versions
    // prior than 4.3.0 (stable!), we need to use mb_ereg_replace as an alternative
    if (version_compare(phpversion(), "4.3.0", ">=") > 0)
    {
        $s = htmlspecialchars($s, ENT_COMPAT, 'UTF-8');
    }
    else
    {
        if (function_exists('mb_regex_encoding'))
        {
            mb_regex_encoding('UTF-8');
            $s = mb_ereg_replace('&', '&amp;' , $s);
            $s = mb_ereg_replace('>', '&gt;'  , $s);
            $s = mb_ereg_replace('<', '&lt;'  , $s);
            $s = mb_ereg_replace('"', '&quot;', $s);
        }
        else
        { 
            // TODO: If $s contains any xml special char, we should raise an error here!
            $s = htmlspecialchars($s);
        }
    }

    return $s;
}

///// Classes /////
 
/**
 * This class represents an xml form based on a schema
 *
 */
class xmlForm
{
  var $_xmlFile;
  var $_xps;         # xpath object holding the schema data
  var $_xpx;         # xpath object holding the xml document data
  var $_level;       # just to control the nesting level when processing data
  var $_nextPath;    # array (each value related to a tag name) representing an xpath address to be used by an element
  var $_prevPath;    # array (each value related to a tag name) representing an xpath address to be used to look for previous element values
  var $_errors;      # array
  var $_css;         # css settings to be used by html tags
  var $_loadedElements; # schema elements that were loaded

  /**
   * Constructor - instantiates an Xpath parser to both files (schema and xml) and initializes properties
   *
   * @param mixed schema [optional] Xml schema file name or xpath object with file already loaded
   * @param mixed xml [optional] Xml file name (based on the schema) or xpath object with file already loaded
   * @param hash css [optional] Hash with html tags and their corresponding css elements
   *                            eg. array('textinput' => 'css_class_for_text_inputs',
   *                                      'textarea'  => '',
   *                                      'select'    => '',
   *                                      'label'     => 'css_class_for_field_label',
   *                                      'required'  => 'css_class_for_required_field_label',
   *                                      'div'       => array('box1', 'box2', 'box3'))
   */
  function xmlForm($schema=false, $xml=false, $css=false)
    {
      $this->_errors = array();
      
      if ($schema)
	{
	  $this->setSchema($schema);
	}
     
      if ($xml)
	{
	  $this->setXml($xml);
	}

      if (is_array($css))
	{
	  $this->_css = array_change_key_case($css, CASE_LOWER);
	}
      else
	{
	  $this->_css = array();
	}
    }
  
  /**
   * Set the xml schema
   *
   * @param mixed schema [optional] Xml schema file name or xpath object with file already loaded
   */
  function setSchema($schema)
    {
      $this->_errors = array();

      if (gettype($schema) == 'object' and get_class($schema) == 'xpath' and 
	  $schema->getProperties('hasContent'))
	{
	  $this->_xps = $schema;
	}
      else
	{
	  # Assume $schema is a file or URL
	  $this->_xps = new XPath();
	  $this->_xps->setVerbose(0);
	  $this->_xps->setXmlOption(XML_OPTION_CASE_FOLDING,false);
	  $this->_xps->setXmlOption(XML_OPTION_SKIP_WHITE,true);
	  $this->_xps->importFromFile($schema);
	}
      
      $this->_level = 0;
      $this->_nextPath = array();
      $this->_prevPath = array();

      foreach ($this->_loadedElements as $elPath => $elObj)
	{
	  # Do not change below $this->_loadedElements[$elPath] with $elObj!!
	  # ("foreach" creates a copy on $elObj, not a reference)
	  $this->_loadedElements[$elPath]->setReferences($this);
	}
    }

  /**
   * Set the xml file that should be based on the schema
   *
   * @param mixed schema [optional] Xml schema file name or xpath object with file already loaded
   */
  function setXml($xml)
    {
      if (gettype($xml) == 'object' and get_class($xml) == 'xpath' and 
	  $xml->getProperties('hasContent'))
	{
	  $this->_xpx = $xml;
	  
	  $this->_xmlFile = $xml->getProperties('xmlFile');
	}
      else
	{
	  # Assume $xml is a file name
	  $this->_xpx = new XPath();
	  $this->_xpx->setVerbose(0);
	  $this->_xpx->setXmlOption(XML_OPTION_CASE_FOLDING,false);
	  $this->_xpx->setXmlOption(XML_OPTION_SKIP_WHITE,true);
	  $this->_xpx->importFromFile($xml);
	  
	  $this->_xmlFile = $xml;
	}
    }

  /**
   * Internal method called before serialization
   *
   * @return array Properties that should be considered during serialization
   */
  function __sleep()
    {
      return array('_xmlFile', '_css', '_loadedElements');
    }

  /**
   * Adds an error message to the errors stack
   *
   * @param string msg Error message
   */
  function _setError($msg)
    {
      array_push($this->_errors, $msg);
    }

  /**
   * Returns a copy of the current array of errors
   *
   * @return array Array of errors
   */
  function getErrors()
    {
      return $this->_errors;
    }

  /**
   * Returns the last error
   *
   * @return string Last error
   */
  function getLastError()
    {
      $len = count($this->_errors);

      $msg = ($len) ? $this->_errors[$len-1] : 'Undefined error!';

      return $msg;
    }

  /**
   * Returns the Html code to be used as the css specification of a tag passed as a parameter
   *
   * @param string refName Reference name, which is usually the tag name 
   *                    ('textinput', 'textarea', 'select', 'label', 'required', 'submit' or 'div')
   *
   * @return string Pair class="cssElementMapped"
   */
  function _getCss($refName)
    {
      $retVal = '';

      if (isset($this->_css[$refName]) and !empty($this->_css[$refName]))
	{
	  if ($refName == 'div')
	    {
	      if (is_array($this->_css[$refName]))
		{
		  $size = count($this->_css[$refName]);

		  $n = $this->_level;

		  $fmod = fmod($n, $size);

		  $pos = ($fmod) ? $fmod : $size;

		  $retVal = sprintf('class="%s"', $this->_css[$refName][$pos-1]);
		}
	    }
	  else
	    {
	      $retVal = sprintf('class="%s"', $this->_css[$refName]);
	    }
	}

      return $retVal;
    }

  /**
   * Returns the html representation of some elements selected from a schema. On the first time this method is called, it selects the elements, instantiates an xmlElement object for each one, and add its html representation to the variable that will be returned. If the element is a grouping one, then it calls this method again.
   *
   * @param string pathToElementsInSchema [optional] Xpath to select elements from schema
   * @param string enclosingPathInXML [optional] Common xml path enclosing all elements in the derived xml file
   *
   * @return string Html representing the selected elements
   */
  function getHtml($pathToElementsInSchema='/xsd:schema/xsd:element', $enclosingPathInXML='')
    {
      if (!isset($this->_xps))
	{
	  $msg = 'No schema specified!';
	  $this->_setError($msg);
	  return '';
	}
      
      $this->loadElements($pathToElementsInSchema);

      # This will indicate whether we should first look for values within request variables
      # instead of looking at the xml document (if it exists)
      $out = "\n<input type=\"hidden\" name=\"xmlForm_rendered\" value=\"1\">";

      $fieldSep = '<br>';

      $i = 0;

      foreach ($this->_loadedElements as $pathToElementInSchema => $elObj)
	{
	  $i++;

	  # elements separator
	  $sep = ($i > 1) ? $fieldSep : '';
	  
	  # Finally render the element
	  $out .= "\n$sep".$this->_loadedElements[$pathToElementInSchema]->getHtml($enclosingPathInXML);
	}

      $this->_nextPath = array();
      $this->_prevPath = array();

      return $out;
    }

  /**
   * Load xsd elements
   *
   * @param string [optional] pathToElementsInSchema Xpath to select elements from schema
   *
   * @return boolean 
   */
  function loadElements($pathToElementsInSchema)
    {
      if (!is_array($this->_loadedElements))
	{
	  $this->_loadedElements = array();

	  $elements = $this->_xps->match($pathToElementsInSchema);
	  
	  foreach ($elements as $pathToElementInSchema)
	    {
	      $el = new xmlElement($this, $pathToElementInSchema);

	      $el->loadAllData();

	      $this->_loadedElements[$pathToElementInSchema] = $el;
	    }
	}

      return true;
    }

  /**
   * Tries to save a particular group of elements from a schema in a derived xml file by retrievingspecific environment variables (get/post).
   *
   * @param string pathToElementsInSchema Xpath to select elements from schema
   * @param string enclosingPathInXML Common xml path enclosing all elements in the derived xml file
   * @param array untouchableElements Array [optional] of element names that we should keep untouched in the xml destination file
   *
   * @return boolean Indication of success or failure
   */
  function save($pathToElementsInSchema='/xsd:schema/xsd:element', $enclosingPathInXML, 
		$untouchableElements=array())
    {
      if (!isset($this->_xpx))
	{
	  $msg = 'No xml file specified!';
	  $this->_setError($msg);
	  return false;
	}

      # Get new xml from form
      $xml = $this->getXml($pathToElementsInSchema);

      if (!$xml)
	{
	  return false;
	}

      # Remove existing tags (except the "untouchable" ones)
      $elementsToRemove = "*";

      if (count($untouchableElements))
	{
	  $elementsToRemove .= "[not(name()='".implode("' or name()='", $untouchableElements)."')]";
	}
      
      $xpath = $enclosingPathInXML . '/' . $elementsToRemove;

      $this->_xpx->removeChild($xpath);

      # Add new xml
      # notes: to use appendChild XPath method, we need a valid xml string representing a node,
      #        which is not the case of $xml content (it contains multiple nodes without a
      #        a root). So, the workaround here is to enclose $xml using a dummy tag to
      #        feed it to another Xpath object. This way, we can loop through all nodes
      #        in $xml appending each one to the real xml file.
      $tmpXp = new XPath();
      $tmpXp->setVerbose(1);
      $tmpXp->setXmlOption(XML_OPTION_CASE_FOLDING,false);
      $tmpXp->setXmlOption(XML_OPTION_SKIP_WHITE,true);
      $tmpXp->importFromString('<dummynode>'.$xml.'</dummynode>');

      $nodes = $tmpXp->match('/dummynode/*');

      foreach ($nodes as $npath)
	{
	  $this->_xpx->appendChild($enclosingPathInXML, $tmpXp->getNode($npath));
	}

      # Save file
      if (!$this->_xpx->exportToFile($this->_xmlFile, '', XML_HEADER))
	{
	  $msg = 'Could not save changes to file!';
	  $this->_setError($msg);
	  return false;
	}

      return true;
    }


  /**
   * Returns the xml representation of some elements selected from a schema. On the first time this method is called, it selects the elements, instantiates an xmlElement object for each one, and add its xml representation to the variable that will be returned. If the element is a grouping one, then it calls this method again.
   *
   * @param string pathToElementsInSchema Xpath to select elements from schema
   *
   * @return string Xml representing the selected elements
   */
  function getXml($pathToElementsInSchema='/xsd:schema/xsd:element')
    {
      if (!isset($this->_xps))
	{
	  $msg = 'No schema specified!';
	  $this->_setError($msg);
	  return '';
	}

      $this->loadElements($pathToElementsInSchema);

      $out = '';

      foreach ($this->_loadedElements as $pathToElementInSchema => $elObj)
	{
	  # Get the xml of the element
	  $out .= $this->_loadedElements[$pathToElementInSchema]->getXml();
	}

      $this->_prevPath = array();

     if (count($this->_errors))
	{
	  return false;
	}

      return $out;
    }

  /**
   * Returns an element's value. It fist looks in the environment variables (get/post), and then it looks in the xml file content (if it exists).
   *
   * @param string relativeAddress Relative address of the element (excluding the xml enclosing tags common to all elements)
   * @param string enclosingPathInXML [optional] Common xml path enclosing all elements in the file
   *
   * @return string Element's value
   */
  function getValue($relativeAddress, $enclosingPathInXML='')
    {
      $value = '';
      
      if ($relativeAddress)
	{
	  if (isset($_REQUEST[urlencode($relativeAddress)]))
	    {
	      $value = $_REQUEST[urlencode($relativeAddress)];
	    }
	  elseif (isset($this->_xpx))
	    {
	      $pathInXml = $enclosingPathInXML . '/' . $relativeAddress;

	      # XPath library does not understand queries on the form:
	      # /tagx[1]/tagy[@name][3] 
	      # (which means: select "name" attribute from third tag y inside first tag x)
	      # Then, to be more effective, the same address above is now represented as
	      # /tagx[1]/tagy[3][@name]
	      # So, if path contains an attribute, we should remove it and then use getAttribute
	      $attrPosition = strpos($pathInXml, '[@');

	      if ($attrPosition)
		{
		  $attrName = substr($pathInXml, $attrPosition + 2);
		  $attrName = substr($attrName, 0, strlen($attrName) -1);

		  # /tagx[1]/tagy[3][@name] becomes /tagx[1]/tagy[3]
		  $pathInXml = substr($pathInXml, 0, $attrPosition);
		}

	      $matched = $this->_xpx->match($pathInXml);

	      if (count($matched))
		{
		  if ($attrName)
		    {
		      $value = $this->_xpx->getAttributes($pathInXml, $attrName);
		    }
		  else
		    {
		      $value = $this->_xpx->getData($pathInXml);
		    }
		}
	    }
	}

      return $value;
    }
}


/**
 * This class represents an element from an xml schema
 *
 */
class xmlElement
{
  var $_xmlForm;   # Reference to the root xmlForm object
  var $_xp;        # Reference to XPath object (property) from the xmlForm object (just a shortcut)
  var $_pathInSchema; # xpath to element in schema
  var $_pathToAncestor; # If element inherits from another, this property holds its ancestor xpath

  var $_name;
  var $_type;
  var $_pattern;
  var $_minOccurs;
  var $_maxOccurs;
  var $_doc;
  var $_pathToSubElements;
  var $_pathToExtension;
  var $_elements;
  var $_attributes;

  /**
   * Constructor - just store some object reference and the element xpath in schema
   *
   * @param object [reference] xmlForm xmlForm object reference
   * @param string pathInSchema Xpath of element in schema
   */
  function xmlElement(&$xmlForm, $pathInSchema)
    {
      $this->setReferences($xmlForm);

      $this->_pathInSchema = $pathInSchema;
    }
  
  /**
   * Set _xmlForm and _xp properties after unserializing object from cache
   *
   * @param object [reference] xmlForm Reference to root xmlForm object
   */
  function setReferences(&$xmlForm)
    {
      $this->_xmlForm =& $xmlForm;
      $this->_xp      =& $xmlForm->_xps;

      foreach ($this->_attributes as $attrName => $attrObj)
	{
	  # Do not change below $this->_attributes[$attrName] with $attrObj!!
	  # ("foreach" creates a copy on $attrObj, not a reference)
	  $this->_attributes[$attrName]->setReferences($xmlForm);
	}

      foreach ($this->_elements as $elPath => $elObj)
	{
	  $this->_elements[$elPath]->setReferences($xmlForm);
	}
    }

  /**
   * Internal method called before serialization
   *
   * @return array Properties that should be considered during serialization
   */
  function __sleep()
    {
      return array('_pathInSchema', '_pathToAncestor', '_name', '_type', '_pattern', 
		   '_minOccurs', '_maxOccurs', '_doc', '_pathToSubElements', '_pathToExtension',
		   '_attributes', '_elements');
    }
  
  /**
   * Returns the element name (name attribute of xsd:element)
   *
   * @return string Element name
   */
  function getName()
    {
      if (!isset($this->_name))
	{
	  $this->_name = $this->_xp->getAttributes($this->_pathInSchema, 'name');
	}
      
      return $this->_name;
    }

  /**
   * Returns the element type according to the schema, also loading 
   * its pattern and path to sub elements
   *
   * @return string Type (such as xsd:string, xsd:dateTime, etc)
   */
  function getType()
    {
      if (!isset($this->_type))
	{
	  $this->_type = $this->_xp->getAttributes($this->_pathInSchema, 'type');
	  $this->_pattern = '';
	  $this->_pathToSubElements = '';
	  $this->_pathToExtension = '';

	  # Does it have a "type" attribute defined?
	  if ($this->_type)
	    {
	      # If type does not begin with "xsd:", then it should be a custom type...
	      if (substr($this->_type, 0, 4) <> "xsd:")
		{
		  # Check if type is based on a simpleType
		  $pathToSimpleType = sprintf("//xsd:simpleType[@name='%s']", $this->_type);
		  
		  if ($this->_xp->match($pathToSimpleType))
		    {
		      $this->_pathToAncestor = $pathToSimpleType;
		      
		      # If so, get its primary type
		      $pathToBaseElement = sprintf("//xsd:simpleType[@name='%s']/xsd:restriction", $this->_type);
		      
		      $this->_type = $this->_xp->getAttributes($pathToBaseElement, 'base');
		      
		      if ($this->_type == 'xsd:string')
			{
			  $pathToPattern = $pathToBaseElement . '/xsd:pattern';
			  
			  $this->_pattern = $this->_xp->getAttributes($pathToPattern, 'value');
			}
		      else
			{
			  # Another type to check here??
			}
		    }
		  else
		    {
		      # Check if type is based on a complexType
		      $pathToComplexType = sprintf("//xsd:complexType[@name='%s']", $this->_type);
		      
		      if ($this->_xp->match($pathToComplexType))
			{
			  $this->_pathToAncestor = $pathToComplexType;

			  # If so, get path to sub elements
			  $this->_pathToSubElements = $pathToComplexType . '/xsd:sequence/xsd:element';
			}
		    }
		}
	      else
		{
		  # type begins with "xsd:", let's assume it's a primary type
		}
	    }
	  # It does not have a "type" attribute...
	  else
	    {
	      # Check if it is a grouping tag by trying to get the direct path to its sub elements
	      $pathToSubElements = $this->_pathInSchema . '/xsd:complexType[1]/xsd:sequence[1]/xsd:element';
	      
	      $subnodes = $this->_xp->match($pathToSubElements);
	      
	      $this->_pathToSubElements = (count($subnodes)) ? $pathToSubElements : '' ;
	      
	      # If it is not a direct grouping tag
	      if (!$this->_pathToSubElements)
		{
		  # Check if it has an extension inside a simple content
		  $pathToExtension = $this->_pathInSchema . '/xsd:complexType[1]/xsd:simpleContent[1]/xsd:extension';
		  if ($this->_xp->match($pathToExtension))
		    {
		      $this->_pathToExtension = $pathToExtension;
		      
		      $this->_type = $this->_xp->getAttributes($pathToExtension, 'base');
		      
		      # TODO: further checks here (if type does not begin with xsd...)
		    }
		}
	    }
	}

      return $this->_type;
    }

  /**
   * Indicates if the element has possible sub elements according to the schema
   *
   * @return boolean True or False
   */
  function hasSubElements()
  {
      if (!isset($this->_pathToSubElements))
	{
	  $type = $this->getType();
	}

    return ($this->_pathToSubElements) ? true : false ;
  }

  /**
   * Returns the element pattern (xsd:pattern value)
   *
   * @return string Regular expression
   */
  function getPattern()
    {
      if (!isset($this->_pattern))
	{
	  $this->getType();
	}

      return $this->_pattern;
    }

  /**
   * Returns the Xpath query to catch sub elements from the schema
   *
   * @return string Xpath query
   */
  function getPathToSubelements()
    {
      if (!isset($this->_pathToSubElements))
	{
	  $this->getType();
	}

      return $this->_pathToSubElements;
    }

  /**
   * Returns the documentation about the element (content of xsd:documentation tag in schema)
   *
   * @return string Documentation about the element
   */
  function getDoc()
    {
      if (!isset($this->_doc))
	{
	  $pathToDoc = $this->_pathInSchema . '/xsd:annotation[1]/xsd:documentation[1]';

	  $this->_doc = ($this->_xp->match($pathToDoc)) ? $this->_xp->getData($pathToDoc) : '';
	}

      return $this->_doc;
    }

  /**
   * Returns the minimum acceptable occurences of the element in an xml
   *
   * @return integer Minimum occurences
   */
  function getMinOccurs()
    {
      if (!isset($this->_minOccurs))
	{
	  $this->_minOccurs = $this->_xp->getAttributes($this->_pathInSchema, 'minOccurs');
	}

      return (!strlen($this->_minOccurs)) ? 1 : $this->_minOccurs;
    }

  /**
   * Returns the maxiimum acceptable occurences of the element in an xml
   *
   * @return integer Maximum occurences
   */
  function getMaxOccurs()
    {
      if (!isset($this->_maxOccurs))
	{
	  $this->_maxOccurs = $this->_xp->getAttributes($this->_pathInSchema, 'maxOccurs');
	}

      return (!strlen($this->_maxOccurs)) ? 1 : $this->_maxOccurs;
    }

  /**
   * Returns the number of possible attributes in this element
   *
   * @return integer Number of attributes
   */
  function hasAttributes()
    {
      if (!is_array($this->_attributes))
	{
	  $this->_loadAttributes();
	}

      return count($this->_attributes);
    }

  /**
   * Loads all possible attributes from this element
   *
   * @return boolean TRUE
   */
  function _loadAttributes()
    {
      if (!is_array($this->_attributes))
	{
	  $this->_attributes = array();

	  # call getType to load _pathToAncestor and _pathToExtension
	  $this->getType();

	  if ($this->_pathToAncestor)
	    {
	      $pathToAttributes =  $this->_pathToAncestor;
	    }
	  elseif ($this->_pathToExtension)
	    {
	      $pathToAttributes =  $this->_pathToExtension;
	    }
	  else
	    {
	      $pathToAttributes =  $this->_pathInSchema;
	    }
	  
	  $pathToAttributes .= '/xsd:attribute';

	  $attributes = $this->_xp->match($pathToAttributes);

	  foreach ($attributes as $pathToAttribute)
	    {
	      $attr = new xmlAttribute($this->_xmlForm, $pathToAttribute);

	      $this->_attributes[$attr->getName()] = $attr;
	    }
	}

      return true;
    }

  /**
   * Gives the related attributes hash
   *
   * @return hash Attributes as name => object
   */
  function getAttributes()
    {
      $this->_loadAttributes();

      return $this->_attributes;
    }
  
  /**
   * Returns the html representation of the element
   *
   * @param string enclosingPathInXML Common xml path enclosing all elements in the file
   * @param boolean isNew [optional] Indication of whether the elements is a sub element of a new element
   *
   * @return string Html representing the element
   */
  function getHtml($enclosingPathInXML, $isNew=0)
  {
    ++$this->_xmlForm->_level;

    $out = '';

    $name = $this->getName();
    $type = $this->getType();
    $doc  = $this->getDoc();
    $doc = ($doc) ? strtr($doc, array('"'=>'``', "'"=>"`")) : 'No documentation about this item';

    # Define a relative xpath address to the element
    $address = '/' . $name;
    
    if (count($this->_xmlForm->_prevPath))
      {
	$address = '/' . implode('/', $this->_xmlForm->_prevPath) . $address;
      }

    # How many times ($n) do we need to loop on this tag? (at least one)
    $minOccurs = $this->getMinOccurs();

    $n = max(1, $minOccurs);
    
    if (getVal('xmlForm_rendered'))
      {
	$cnt = 1;
	
	$lookFor = substr($address, 1) . "[%s]";
	
	while (getVal(urlencode(sprintf($lookFor, $cnt))))
	  {
	    $cnt++;
	  }
	
	$n = max($cnt-1, $n);
      }
    else
      {
	# If we have an xml document...
	if (isset($this->_xmlForm->_xpx))
	  {
	    # How many tags like this are there?
	    $path = $enclosingPathInXML . $address;

	    $tags = $this->_xmlForm->_xpx->match($path);
	    
	    $n = max(count($tags), $n);
	  }
      }

    # Check if user requested to add a new element of this type, and also if
    # user requested deletion of one the elements from the same type
    # (only if we are not already dealing with a new element!)
    $requestedNewTag = 0;
    $requestedDeletion = 0;

    if (getVal('xmlForm_rendered') and !$isNew) 
      {
	$addParam = urlencode('add_' . substr($address, 1));
	
	if (getVal($addParam))
	  {
	    $n++; 
	    # loop once more!
	    $requestedNewTag = 1;
	  }
	else
	  {
	    $baseAddress = $name;

	    if ($this->_xmlForm->_prevPath)
	      {
		$baseAddress = implode('/', $this->_xmlForm->_prevPath).'/'.$name;
	      }

	    # Check if one of the elements was deleted
	    for ($j = 1; $j <= $n; $j++)
	      {
		$delParam = urlencode('del_'.$enclosingPathInXML.'/'.$baseAddress."[$j]");

		if (getVal($delParam))
		  {
		    $requestedDeletion = 1;
		    break;
		  }
	      }
	  }
      }

    # Check limit of occurences
    $maxOccurs = $this->getMaxOccurs();
    
    $passedMinLimit = (($n - $requestedDeletion) > $minOccurs) ? true : false;
    $reachedMaxLimit = (strcasecmp($maxOccurs, 'unbounded') and ($n - $requestedDeletion) >= $maxOccurs) ? true : false;

    # Flag to control deletion
    $deleted = false;

    # Render the element "n" times
    for ($j = 1; $j <= $n; $j++)
      {
	$nextIdx = $prevIdx = $j;
	
	if ($deleted)
	  {
	    $nextIdx--;
	  }

	$isLastToShow = (($j == $n) or (($j == $n-1) and (!$deleted and $requestedDeletion))) ? true : false;
	
	$showAddButton = ($isLastToShow and !$reachedMaxLimit) ? true : false;
	$showDelButton = ($passedMinLimit and $n > 1) ? true : false;

	array_push($this->_xmlForm->_nextPath, $name . "[$nextIdx]");
	array_push($this->_xmlForm->_prevPath, $name . "[$prevIdx]");
	
	# nextAddress = Relative address of the element 
	#               (excluding the xml enclosing tags common to all elements) to be used as 
	#               the new element reference (name of the parameter to be used in the form)
	# prevAddress = Relative address of the element 
	#               (excluding the xml enclosing tags common to all elements) that was used to 
	#               pass the elements value in the current environment variables 
	#               (name of the parameter that was used in the previous form)
	$nextAddress = implode('/', $this->_xmlForm->_nextPath);

	if ($isNew or ($isLastToShow and $requestedNewTag))
	  {
	    $prevAddress = '';
	  }
	else
	  {
	    $prevAddress = implode('/', $this->_xmlForm->_prevPath);

	    # Check if element has been deleted
	    if (!$deleted and getVal('xmlForm_rendered'))
	      {
		$delParam = urlencode('del_'.$enclosingPathInXML.'/'.$prevAddress);
		
		if (getVal($delParam))
		  {
		    $deleted = true;
		    array_pop($this->_xmlForm->_nextPath);
		    array_pop($this->_xmlForm->_prevPath);
		    continue;
		  }
	      }
	  }

	$nextPathInXml = $enclosingPathInXML.'/'.$nextAddress;

	# addition parameter (address without position on the last tag)
	$nextAddParam = "add_" . substr($nextAddress, 0, strrpos($nextAddress, "["));

	# deletion parameter 
	$nextDelParam = "del_" . $nextPathInXml;

	# documentation
	$js = sprintf("onClick=\"window.open('help.php?name=%s&doc=%s','help','width=400,height=200,menubar=no,toolbar=no,scrollbars=yes,resizable=yes,personalbar=no,locationbar=no,statusbar=no').focus(); return false;\" onMouseOver=\"window.status='%s'; return true;\" onMouseOut=\"window.status=''; return true;\" style=\"text-decoration:none;\"", ucfirst($name), urlencode($doc), $doc);

	$cssLabel = ($minOccurs > 0 and $j <= $minOccurs) ? 
	  $this->_xmlForm->_getCss('required') :
	  $this->_xmlForm->_getCss('label');
	
	if ($this->hasSubElements() or $this->hasAttributes())
	  {
	    $css = $this->_xmlForm->_getCss('div');
	    
	    $out .= sprintf('<div %s>', $css) . "\n";
	    
	    $openedDiv = true;
	  }

	if ($this->hasSubElements())
	  {
	    # Show only main label
	    $out .= sprintf('<input type="hidden" name="%s" value=" ">', urlencode($nextAddress))."\n";
	    
	    $out .= sprintf('<center><a href="help.php?name=%s&doc=%s" %s %s>%s</a></center>',
			    ucfirst($name), urlencode($doc), $js, $cssLabel, ucfirst($name));
	  }
	else
	  {
	    # Show label and value
	    $out .= sprintf('<a href="help.php?name=%s&doc=%s" %s %s>%s: </a><br>',
			    ucfirst($name), urlencode($doc), $js, $cssLabel, ucfirst($name));
	    
	    $value = $this->_xmlForm->getValue($prevAddress, $enclosingPathInXML);

	    if (mb_strlen($value, 'utf-8') < 80)
	      {
		$css = $this->_xmlForm->_getCss('textinput');
		
		$out .= sprintf('<input type="text" name="%s" value="%s" size="60" %s>', 
				urlencode($nextAddress), $value, $css);
	      }
	    else
	      {
		$css = $this->_xmlForm->_getCss('textarea');
		
		$out .= sprintf('<textarea name="%s" rows="5" cols="60" wrap="1" %s>%s</textarea>', 
				urlencode($nextAddress), $css, $value);
	      }
	  }

	# Show possible attributes
	if ($this->hasAttributes())
	  {
	    $attributes = $this->getAttributes();
	    
	    foreach ($attributes as $attName => $attr)
	      {
		# This is not a valid xpath address, however, since the php XPath class
		# at the present moment would not understand a valid query to represent
		# attribute x from occurence n of element y, we are simplifying the
		# address representation:
		$nextAttAddress = $nextAddress . "[@$attName]";
		$prevAttAddress = $prevAddress . "[@$attName]";
		
		$out .= $attr->getHtml($nextAttAddress, $prevAttAddress, $enclosingPathInXML);
	      }
	  }
	
	if ($this->hasSubElements())
	  {
	    # Show sub elements...
	    $fieldSep = '<br>';
	    
	    $i = 0;

	    foreach ($this->_elements as $subelementPath => $subelementObj)
	      {
		$i++;

		# elements separator
		$sep = ($i > 1 or $this->_xmlForm->_level > 1 or $this->hasAttributes()) ? $fieldSep : '';

		$out .= "\n$sep".$this->_elements[$subelementPath]->getHtml($enclosingPathInXML, 
									    empty($prevAddress));
	      }
	    
	    $out .= "\n<br><br>\n";
	  }
	
	$css = $this->_xmlForm->_getCss('submit');
	
	if ($showDelButton)
	  {
	    $delButton = sprintf('<input type="submit" name="%s" value="remove this %s" %s>', 
				 urlencode($nextDelParam), $name, $css);
	    
	    $spacer = '&nbsp;&nbsp;';
	    
	    $out = ($this->hasSubElements()) ? $out.$delButton.$spacer : $out.$spacer.$delButton.'<br>';
	  }
	
	if ($showAddButton)
	  {
	    if (!$this->hasSubElements() and !$showDelButton)
	      {
		$out .= '<br>';
	      }
	    
	    $out .= sprintf('<input type="submit" name="%s" value="add another %s" %s>', 
			    urlencode($nextAddParam), $name, $css);
	  }
	
	if ($openedDiv)
	  {
	    $out .= "\n</div>\n";
	  }

	#--------------------

	array_pop($this->_xmlForm->_nextPath);
	array_pop($this->_xmlForm->_prevPath);
      }

    --$this->_xmlForm->_level;

    return $out;
  }

  /**
   * Returns the xml representation of the element
   *
   * @return string Xml representing the element
   */
  function getXml()
    {
      ++$this->_xmlForm->_level;

      $out = '';
      
      $name = $this->getName();

      $address = '/' . $name;
      
      if (count($this->_xmlForm->_prevPath))
	{
	  $address = '/' . implode('/', $this->_xmlForm->_prevPath) . $address;
	}

      # How many elements ($n) of this are over there? (at least one)
      $cnt = 1;
      
      $lookFor = substr($address, 1) . "[%s]";

      while (isset($_REQUEST[urlencode(sprintf($lookFor, $cnt))]))
	{
	  $cnt++;
	}
      
      $n = $cnt -1;

      # Check element quantity
      $minOccurs = $this->getMinOccurs();
      $maxOccurs = $this->getMaxOccurs();

      if ($n < $minOccurs)
	{
	  $msg = "Element '$name' should appear at least $minOccurs time(s)";
	  $this->_xmlForm->_setError($msg);
	}
      
      if (strcasecmp($maxOccurs, "unbounded") and $n > $maxOccurs)
	{
	  $msg = "Element '$name' should not appear more than $maxOccurs time(s)";
	  $this->_xmlForm->_setError($msg);
	}

      for ($j = 1; $j <= $n; $j++)
	{
	  array_push($this->_xmlForm->_prevPath, $name."[$j]");
	  
	  $prevAddress = implode('/', $this->_xmlForm->_prevPath);

	  $out .= "\n<$name";
	  
	  $attributes = $this->getAttributes();
	  
	  foreach ($attributes as $attrName => $attr)
	    {
	      $attrAddress = $prevAddress . "[@$attrName]";
	      
	      $attrVal = $this->_xmlForm->getValue($attrAddress);
	      
	      $pattern = $attr->getPattern();
	      
	      if (mb_strlen(trim($attrVal), 'utf-8'))
		{
		  if ($pattern)
		    {
		      # The only character we need to escape is the pattern delimiter
		      $pattern = strtr($pattern, array('/'=>'\\/'));

		      # Decode html special chars 
		      if (version_compare(phpversion(), '4.3.0', '>=') > 0)
			{
			  $pattern = html_entity_decode($pattern, ENT_QUOTES, 'utf-8');
			}
		      else
			{
			  $pattern = strtr($pattern, array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)));
			}

		      # Note that xml schema regular expressions are matched against entire 
		      # lexical representations, so we need to put them between ^ and $.
		      # As "$" is a special php character, it needs to be escaped.
		      if (!preg_match("/^$pattern\$/i", $attrVal))
			{
			  $msg = "Value '$attrVal' does not match pattern assigned to '$attrName' attributes from '$name' elements";
			  $this->_xmlForm->_setError($msg);
			}
		    }
		}
	      else
		{
		  if (!strcasecmp($attr->getUse(), 'required'))
		    {
		      $msg = "Attribute '$attrAddress' should have a value";
		      $this->_xmlForm->_setError($msg);
		    }
		}
	      
	      $out .= sprintf(" %s=\"%s\"", $attrName, escChars($attrVal));
	    }
	  
	  $out .= '>';
	  
	  if ($this->hasSubElements())
	    {
	      foreach ($this->_elements as $subelementPath => $subelementObj)
		{
		  $out .= $this->_elements[$subelementPath]->getXml();
		}
	    }
	  else
	    {
	      $value = $this->_xmlForm->getValue($prevAddress); 
	      
	      $pattern = $this->getPattern();
	      
	      if (mb_strlen(trim($value), 'utf-8'))
		{
		  if ($pattern)
		    {
		      # The only character we need to escape is the pattern delimiter
		      $pattern = strtr($pattern, array('/'=>'\\/'));

		      # Decode html special chars 
		      if (version_compare(phpversion(), '4.3.0', '>=') > 0)
			{
			  $pattern = html_entity_decode($pattern, ENT_QUOTES, 'utf-8');
			}
		      else
			{
			  $pattern = strtr($pattern, array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)));
			}

		      # Note that xml schema regular expressions are matched against entire 
		      # lexical representations, so we need to put them between ^ and $.
		      # As "$" is a special php character, it needs to be escaped.
		      if (!preg_match("/^$pattern\$/i", $value))
			{
			  $msg = "Value '$value' of '$prevAddress' does not match pattern";
			  $this->_xmlForm->_setError($msg);
			}
		    }
		}
	      else
		{
		  if ($minOccurs and $minOccurs >= $j)
		    {
		      $msg = "Element '$prevAddress' should have a value";
		      $this->_xmlForm->_setError($msg);
		    }
		}
	      
	      $out .= escChars($value);
	    }
	  
	  $out .= "</$name>";

	  array_pop($this->_xmlForm->_prevPath);
      	}
      
      --$this->_xmlForm->_level;
      
      return $out;
    }


  /**
   * Loads all data from element
   *
   */
  function loadAllData()
    {
      $this->getName();
      $this->getType();
      $this->getDoc();
      $this->getMinOccurs();
      $this->getMaxOccurs();
      $this->_loadAttributes();
      $this->_loadSubElements();

      return true;
    }


  /**
   * Load possible sub elements from the schema
   *
   */
  function _loadSubElements()
    {
      $pathToSubElements = $this->getPathToSubElements();

      if (!is_array($this->_elements) and $this->hasSubElements())
	{
	  $this->_elements = array();

	  $elements = $this->_xp->match($pathToSubElements);
	  
	  foreach ($elements as $pathToElementInSchema)
	    {
	      $el = new xmlElement($this->_xmlForm, $pathToElementInSchema);

	      $el->loadAllData();

	      $this->_elements[$pathToElementInSchema] = $el;
	    }
	}

      return true;
    }
}


/**
 * This class represents an attribute of an element inside an xml schema
 *
 */
class xmlAttribute
{
  var $_xmlForm;   # Reference to the root xmlForm object
  var $_xp;        # Reference to XPath object (property) from the xmlForm object (just a shortcut)
  var $_pathInSchema; # xpath to attribute in schema
  var $_name;
  var $_use;
  var $_type;
  var $_pattern;
  var $_enumeration;
  var $_doc;

  /**
   * Constructor - just store some object references and the attribute xpath in schema
   *
   * @param object [reference] xmlForm Reference to root xmlForm object
   * @param string pathInSchema Xpath of attribute element in schema
   */
  function xmlAttribute(&$xmlForm, $pathInSchema)
    {
      $this->setReferences($xmlForm);

      $this->_pathInSchema = $pathInSchema;
    }
  
  /**
   * Set _xmlForm and _xp properties after unserializing object from cache
   *
   * @param object [reference] xmlForm Reference to root xmlForm object
   */
  function setReferences(&$xmlForm)
    {
      $this->_xmlForm =& $xmlForm;
      $this->_xp      =& $xmlForm->_xps;
    }
  
  /**
   * Internal method called before serialization
   *
   * @return array Properties that should be considered during serialization
   */
  function __sleep()
    {
      return array('_pathInSchema', '_name', '_use', '_type', '_pattern', '_enumeration', '_doc');
    }
  
  /**
   * Returns the attribute name (name attribute of xsd:attribute)
   *
   * @return string Attribute name
   */
  function getName()
    {
      if (!isset($this->_name))
	{
	  $this->_name = $this->_xp->getAttributes($this->_pathInSchema, 'name');
	}
      
      return $this->_name;
    }

  /**
   * Returns the attribute type (type attribute of xsd:attribute)
   *
   * @return string Attribute type
   */
  function getType()
    {
      if (!isset($this->_type))
	{
	  $this->_type = $this->_xp->getAttributes($this->_pathInSchema, 'type');
	  $this->_pattern = '';
	  $this->_enumeration = array();

	  # Does it have a "type" attribute defined?
	  if ($this->_type)
	    {
	      # If type does not begin with "xsd:", then it should be a custom type...
	      if (substr($this->_type, 0, 4) <> 'xsd:')
		{
		  # Check if type is based on a simpleType
		  $pathToSimpleType = sprintf("//xsd:simpleType[@name='%s']", $this->_type);
		  
		  if ($this->_xp->match($pathToSimpleType))
		    {
		      # If so, get its primary type
		      $pathToBaseElement = sprintf("//xsd:simpleType[@name='%s']/xsd:restriction", $this->_type);
		      
		      $this->_type = $this->_xp->getAttributes($pathToBaseElement, 'base');
		      
		      if ($this->_type == 'xsd:string')
			{
			  $pathToPattern = $pathToBaseElement . '/xsd:pattern';
			  
			  $this->_pattern = $this->_xp->getAttributes($pathToPattern, 'value');
			}
		      else
			{
			  # Anything else to do here?
			}
		      
		      $possibleEnumerationPath = $pathToSimpleType . '/xsd:enumeration';
		    }
		  else
		    {
		      # Any other possibility here?
		    }
		}
	      else
		{
		  # type begins with "xsd:", thewn we assume it is a primary type
		}
	    }
	  # It does not have a "type" attribute, so look at a possible "simpleType" sub element
	  else
	    {
	      # Possible path to simpleType
	      $pathToSimpleType = $this->_pathInSchema . '/xsd:simpleType[1]/xsd:restriction[1]';
	      
	      if ($this->_xp->match($pathToSimpleType))
		{
		  $this->_type = $this->_xp->getAttributes($pathToSimpleType, 'base');
		  
		  $possibleEnumerationPath = $pathToSimpleType . '/xsd:enumeration';
		}
	    }
	  
	  if ($possibleEnumerationPath)
	    {
	      $options = $this->_xp->match($possibleEnumerationPath);
	      
	      foreach ($options as $pathToOption)
		{
		  $option = $this->_xp->getAttributes($pathToOption, 'value');
		  
		  array_push($this->_enumeration, $option);
		}
	    }
	}

      return $this->_type;
    }

  /**
   * Returns the attribute pattern (xsd:pattern value)
   *
   * @return string Regular expression
   */
  function getPattern()
    {
      if (!isset($this->_pattern))
	{
	  $this->getType();
	}

      return $this->_pattern;
    }

  /**
   * Returns the attribute use
   *
   * @return string Attribute use
   */
  function getUse()
    {
      if (!isset($this->_use))
	{
	  $this->_use = $this->_xp->getAttributes($this->_pathInSchema, 'use');
	}

      return $this->_use;
    }

  /**
   * Returns the documentation about the attribute (content of xsd:documentation tag in schema)
   *
   * @return string Documentation about the attribute
   */
  function getDoc()
    {
      if (!isset($this->_doc))
	{
	  $pathToDoc = $this->_pathInSchema . '/xsd:annotation[1]/xsd:documentation[1]';

	  $this->_doc = ($this->_xp->match($pathToDoc)) ? $this->_xp->getData($pathToDoc) : '';
	}

      return $this->_doc;
    }

  /**
   * Indicates if attribute has an associated enumeration
   *
   * @return integer Number of elements in enumeration
   */
  function hasEnumeration()
    {
      $this->getType();

      return count($this->_enumeration);
    }

  /**
   * Returns the html representation of the attribute
   *
   * @param string nextAddress Relative address of the attribute (excluding the xml enclosing tags common to all elements) to be used as the new attribute reference (name of the parameter to be used in the form)
   * @param string prevAddress Relative address of the attribute (excluding the xml enclosing tags common to all elements) that was used to pass the attribute's value in the current environment variables (name of the parameter that was used in the previous form)
   * @param string enclosingPathInXML Common xml path enclosing all elements in the file
   *
   * @return string Html representing the attribute
   */
  function getHtml($nextAddress, $prevAddress, $enclosingPathInXML)
  {
    $out = '';

    $name = $this->getName();
    $type = $this->getType();
    $doc = $this->getDoc();

    $doc = ($doc) ? strtr($doc, array('"'=>'``', "'"=>"`")) : 'No documentation about this item';

    # documentation
    $js = sprintf("onClick=\"window.open('help.php?name=%s&doc=%s','help','width=400,height=200,menubar=no,toolbar=no,scrollbars=yes,resizable=yes,personalbar=no,locationbar=no,statusbar=no').focus(); return false;\" onMouseOver=\"window.status='%s'; return true;\" onMouseOut=\"window.status=''; return true;\" style=\"text-decoration:none;\"", ucfirst($name), urlencode($doc), $doc);

    $value = $this->_xmlForm->getValue($prevAddress, $enclosingPathInXML);

    $css = (!strcasecmp($this->getUse(), 'required')) ? 
      $this->_xmlForm->_getCss('required') :
      $this->_xmlForm->_getCss('label');

    $out .= sprintf('<br><a href="help.php?name=%s&doc=%s" %s %s>%s: </a><br>', 
		    ucfirst($name), urlencode($doc), $js, $css, ucfirst($name));

    if ($this->hasEnumeration())
      {
	$css = $this->_xmlForm->_getCss('select');

	$out .= sprintf('<select name="%s" %s>', urlencode($nextAddress), $css);

	$selected = ($value) ? '': 'selected';

	$out .= sprintf('<option value="" %s>-- select an option --', $selected);
	
	foreach ($this->_enumeration as $option)
	  {
	    $selected = ($value == $option) ? 'selected' : '';

	    $out .= sprintf('<option value="%s" %s>%s', $option, $selected, $option);
	  }

	$out .= '</select>';
      }
    else
      {
	$css = $this->_xmlForm->_getCss('textinput');

	$out .= sprintf('<input type="text" name="%s" value="%s" size="60" %s>', urlencode($nextAddress), $value, $css);
      }

    return $out;
  }
}

?>