<?php
/**
 * @version		$Id: form.php 347 2009-03-02 22:39:53Z robs $
 * @package		JXtended.Libraries
 * @subpackage	Form
 * @copyright	Copyright (C) 2008 - 2009 JXtended, LLC. All rights reserved.
 * @license		GNU General Public License <http://www.gnu.org/copyleft/gpl.html>
 * @link		http://jxtended.com
 */

defined('JPATH_BASE') or die;

/**
 * Form Class for JXtended Libraries.
 *
 * This class implements a robust API for constructing, populating,
 * filtering, and validating forms. It uses XML definitions to
 * construct form fields and a variety of field and rule classes
 * to render and validate the form.
 *
 * <code>
 *  <?php
 *  jximport('jxtended.form.form');
 *  $form = &JXForm::getInstance({XML FILE}, {OPTIONS});
 *  ?>
 * </code>
 *
 * @package		JXtended.Libraries
 * @subpackage	Forms
 * @version		2.0
 */
class JXForm extends JObject
{
	/**
	 * The form action URL.
	 *
	 * @access	private
	 * @since	2.0
	 * @var		string
	 */
	var $_action	= null;

	/**
	 * The form id.
	 *
	 * @access	private
	 * @since	2.0
	 * @var		string
	 */
	var $_id		= null;

	/**
	 * The form name.
	 *
	 * @access	private
	 * @since	2.0
	 * @var		string
	 */
	var $_name 		= null;

	/**
	 * Flag to indicate a multi-part form for uploads.
	 *
	 * @access	private
	 * @since	2.0
	 * @var		boolean
	 */
	var $_multipart	= false;

	/**
	 * The form fieldsets as XML objects.
	 *
	 * @access	private
	 * @since	2.0
	 * @var		array
	 */
	var $_fieldsets	= array();

	/**
	 * Method to get an instance of a form.
	 *
	 * @access	public
	 * @param	string		$file		The name of the form to load.
	 * @param	array		$options	An array of options to pass to the form.
	 * @return	object		A JXForm instance.
	 * @since	2.0
	 */
	function &getInstance($file, $options = array())
	{
		static $instances = null;

		if ($instances == null) {
			$instances = array();
		}

		// Only load the form once.
		if (!isset($instances[$file]))
		{
			// Instantiate the form.
			$instances[$file] = new JXForm($options);
			$instances[$file]->load($file, true, true);
		}

		return $instances[$file];
	}

	/**
	 * Method to construct the object on instantiation.
	 *
	 * @access	protected
	 * @param	array		$options	An array of form options.
	 * @return	void
	 * @since	2.0
	 */
	function __construct($options = array())
	{
		// Set the options if specified.
		$this->_action		= array_key_exists('action', $options) ? $options['action'] : null;
		$this->_id			= array_key_exists('id', $options) ? $options['id'] : null;
		$this->_multipart	= array_key_exists('multipart', $options) ? $options['multipart'] : false;
		$this->_name		= array_key_exists('name', $options) ? $options['name'] : 'jxform';
	}

	/**
	 * Method to bind data to the form fields.
	 *
	 * @access	public
	 * @param	mixed		$data	An array or object of form values.
	 * @param	string		$group	The group to bind the fields to.
	 * @return	boolean		True on success, false otherwise.
	 * @since	2.0
	 */
	function bind($data, $group = null)
	{
		if (!is_object($data) && !is_array($data)) {
			return false;
		}

		// Iterate through the fieldsets.
		foreach ($this->_fieldsets as $fieldset => $fields)
		{
			// Bind if no group is specified or if the group matches the current fieldset.
			if ($group === null || ($group !== null && $fieldset === $group))
			{
				// Iterate through the values.
				foreach((array)$data as $k => $v)
				{
					// Bind the value to the field if it exists.
					if (isset($this->_fieldsets[$fieldset][$k]) && is_object($this->_fieldsets[$fieldset][$k])) {
						$this->_fieldsets[$fieldset][$k]->setData($v);
					}
				}
			}
		}

		return true;
	}

	/**
	 * Method to load the form description from a file or string.
	 *
	 * If $data is a file name, $file must be set to true. The reset option
	 * works on a group basis. If the XML file or string references groups
	 * that have already been created they will be replaced with the fields
	 * in the new file unless the $reset parameter has been set to false.
	 *
	 * @access	public
	 * @param	string		$data		The name of an XML file or an XML string.
	 * @param	string		$file		Flag to toggle whether the $data is a file path or a string.
	 * @param	string		$reset		Flag to toggle whether the form description should be reset.
	 * @return	boolean		True on success, false otherwise.
	 * @since	2.0
	 */
	function load($data, $file = true, $reset = true)
	{
		$return = false;

		// Make sure we have data.
		if (!empty($data))
		{
			// Get the XML parser and load the data.
			$parser	= &JFactory::getXMLParser('Simple');

			// If the data is a file, load the XML from the file.
			if ($file)
			{
				// If we were not given the absolute path of a form file, attempt to find one.
				if (!file_exists($data)) {
					jimport('joomla.filesystem.path');
					$data = JPath::find(JXForm::addFormPath(), strtolower($data).'.xml');
				}

				// Attempt to load the XML file.
				$loaded = $parser->loadFile($data);
			}
			// Load the data as a string.
			else {
				$loaded	= $parser->loadString($data);
			}

			// Make sure the XML was loaded.
			if ($loaded)
			{
				// Check if any fieldsets exist.
				if (isset($parser->document->fields))
				{
					// Load the form fieldsets.
					foreach ($parser->document->fields as $fieldset)
					{
						$this->loadFieldsXML($fieldset, $reset);
						$return = true;
					}
				}

				// Check if an action is set.
				if ($parser->document->attributes('action') && $reset) {
					$this->setAction($parser->document->attributes('action'));
				}

				// Check if a name is set.
				if ($parser->document->attributes('name') && $reset) {
					$this->setName($parser->document->attributes('name'));
				}

				// Check if an id is set.
				if ($parser->document->attributes('id') && $reset) {
					$this->setId($parser->document->attributes('id'));
				}
			}
		}

		return $return;
	}

	/**
	 * Method to filter an array of data based on the form fields.
	 *
	 * @access	public
	 * @param	array		$data	The data to filter.
	 * @param	string		$group	An optional group to limit the filtering to.
	 * @return	array		An array of filtered data.
	 * @since	2.0
	 */
	function filter($data, $group = null)
	{
		$return = array();

		// Static input filters for specific settings
		static $noHtmlFilter	= null;
		static $safeHtmlFilter	= null;

		// Get the safe HTML filter if not set.
		if (is_null($safeHtmlFilter)) {
			$safeHtmlFilter = &JFilterInput::getInstance(null, null, 1, 1);
		}

		// Get the no HTML filter if not set.
		if (is_null($noHtmlFilter)) {
			$noHtmlFilter = &JFilterInput::getInstance(/* $tags, $attr, $tag_method, $attr_method, $xss_auto */);
		}

		// Iterate through the fieldsets.
		foreach ($this->_fieldsets as $fieldset => $fields)
		{
			// Filter if no group is specified or if the group matches the current fieldset.
			if ($group === null || ($group !== null && $fieldset === $group))
			{
				// Filter the fields.
				foreach ($fields as $name => $field)
				{
					// Get the field information.
					$filter	= $field->attributes('filter');

					// Check for a value to filter.
					if (isset($data[$name]))
					{
						// Handle the different filter options.
						switch (strtoupper($filter))
						{
							case 'RAW':
								// No Filter.
								$return[$name] = $data[$name];
								break;

							case 'SAFEHTML':
								// Filter safe HTML.
								$return[$name] = $safeHtmlFilter->clean($data[$name], $filter);
								break;

							default:
								// Check for a callback filter.
								if (function_exists($filter)) {
									// Filter using the callback.
									$return[$name] = call_user_func($filter, $data[$name]);
								} else {
									// Filter out HTML.
									$return[$name] = $noHtmlFilter->clean($data[$name], $filter);
								}
								break;
						}
					}
				}
			}
		}

		return $return;
	}

	/**
	 * Method to validate form data.
	 *
	 * Validation warnings will be pushed into JXForm::_errors and should be
	 * retrieved with JXForm::getErrors() when validate returns boolean false.
	 *
	 * @access	public
	 * @param	array		$data	An array of field values to validate.
	 * @param	string		$group	An option group to limit the validation to.
	 * @return	mixed		Boolean on success, JException on error.
	 * @since	2.0
	 */
	function validate($data, $group = null)
	{
		$return = true;
		$data	= (array)$data;

		// Check if the group exists.
		if ($group !== null && !isset($this->_fieldsets[$group])) {
			// The group that was supposed to be filtered does not exist.
			return new JException(JText::sprintf('JX_LIBRARIES_FORM_VALIDATOR_GROUP_NOT_FOUND', $group), 0, E_ERROR);
		}

		// Get a validator object.
		jximport('jxtended.form.validator');
		$validator = new JXFormValidator();

		// Iterate through the fieldsets.
		foreach ($this->_fieldsets as $fieldset => $fields)
		{
			// Filter if no group is specified or if the group matches the current fieldset.
			if ($group === null || ($group !== null && $fieldset === $group))
			{
				// Run the validator over the group.
				$results = $validator->validate($this->_fieldsets[$fieldset], $data);

				// Check for a error.
				if (JError::isError($results) && $results->level === E_ERROR) {
					return new JException($results->getMessage(), 0, E_ERROR);
				}

				// Check the validation results.
				if (count($results))
				{
					// Get the validation messages.
					foreach ($results as $result) {
						if (JError::isError($result) && $result->level === E_WARNING) {
							$this->setError($result);
							$return = false;
						}
					}
				}
			}
		}

		return $return;
	}

	/**
	 * Method to get the form action URL.
	 *
	 * @access	public
	 * @param	string		$default	The default form action URL if it is not set.
	 * @return	string		The form action URL.
	 * @since	2.0
	 */
	function getAction($default = null)
	{
		return !is_null($this->_action) ? $this->_action : $default;
	}

	/**
	 * Method to set the form action URL.
	 *
	 * @access	public
	 * @param	string		$action		The form action URL.
	 * @return	void
	 * @since	2.0
	 */
	function setAction($action)
	{
		$this->_action = $action;
	}

	/**
	 * Method to get the form id.
	 *
	 * @access	public
	 * @param	string		$default	The default form id if it is not set.
	 * @return	string		The form id.
	 * @since	2.0
	 */
	function getId($default = null)
	{
		return !is_null($this->_id) ? $this->_id : $default;
	}

	/**
	 * Method to set the form id.
	 *
	 * @access	public
	 * @param	string		$value	The new form id.
	 * @return	void
	 * @since	2.0
	 */
	function setId($value)
	{
		$this->_id = $value;
	}

	/**
	 * Method to get the form name.
	 *
	 * @access	public
	 * @param	string		$default	The default form name if it is not set.
	 * @return	string		The form name.
	 * @since	2.0
	 */
	function getName($default = null)
	{
		return !is_null($this->_name) ? $this->_name : $default;
	}

	/**
	 * Method to set the form name.
	 *
	 * @access	public
	 * @param	string		$value	The new form name.
	 * @return	void
	 * @since	2.0
	 */
	function setName($value)
	{
		$this->_name = $value;
	}

	/**
	 * Method to add a field to a group.
	 *
	 * @access	public
	 * @param	object		$field	The field object to add.
	 * @param	string		$group	The group to add the field to.
	 * @return	void
	 * @since	2.0
	 */
	function addField(&$field, $group = '_default')
	{
		// Add the field to the group.
		$this->_fieldsets[$group][$field->attributes('name')] = &$field;
	}

	/**
	 * Method to add an array of fields to a group.
	 *
	 * @access	public
	 * @param	array		$fields	An array of field objects to add.
	 * @param	string		$group	The group to add the fields to.
	 * @return	void
	 * @since	2.0
	 */
	function addFields(&$fields, $group = '_default')
	{
		// Add the fields to the group.
		foreach ($fields as $field) {
			$this->_fieldsets[$group][$field->attributes('name')] = $field;
		}
	}

	/**
	 * Method to get a form field.
	 *
	 * @access	public
	 * @param	string		$name			The name of the form field.
	 * @param	string		$group			The group the field is in.
	 * @param	mixed		$controlName	The control name of the field.
	 * @return	object		Rendered Form Field object
	 * @since	2.0
	 */
	function getField($name, $group = '_default', $controlName = 'jxform')
	{
		// Get the XML node.
		$node = isset($this->_fieldsets[$group][$name]) ? $this->_fieldsets[$group][$name] : null;

		if (empty($node)) {
			return false;
		}

		// Get the field info.
		$type	= $node->attributes('type');
		$data	= $node->data();
		$value	= !empty($data) ? $data : $node->attributes('default');

		// Load the field.
		$field = &$this->loadFieldType($type);

		// If the field could not be loaded, get a text field.
		if ($field === false) {
			$field = &$this->loadFieldType('text');
		}

		// Render the field.
		return $field->render($node, $value, $controlName);
	}

	/**
	 * Method to replace a field in a group.
	 *
	 * @access	public
	 * @param	object		$field	The field object to replace.
	 * @param	string		$group	The group to replace the field in.
	 * @return	void
	 * @since	2.0
	 */
	function setField(&$field, $group = '_default')
	{
		$return = false;

		// Add the fields to the group if it exists.
		if (isset($this->_fieldsets[$group][$field->attributes('name')])) {
			$this->_fieldsets[$group][$field->attributes('name')] = $field;
			$return = true;
		}

		return $return;
	}

	/**
	 * Method to get the fields in a group.
	 *
	 * @access	public
	 * @param	string	$controlName	The control name for the form field group
	 * @param	string	$group			The form field group
	 * @return	array	Associative array of rendered Form Field object by field name
	 * @since	2.0
	 */
	function getFields($group = '_default', $controlName = 'jxform')
	{
		$results = array();

		// Check if the group exists.
		if (isset($this->_fieldsets[$group]))
		{
			// Get the fields in the group.
			foreach ($this->_fieldsets[$group] as $name => $node)
			{
				// Get the field info.
				$type	= $node->attributes('type');
				$data	= $node->data();
				$value	= !empty($data) ? $data : $node->attributes('default');

				// Load the field.
				$field = &$this->loadFieldType($type);

				// If the field could not be loaded, get a text field.
				if ($field === false) {
					$field = &$this->loadFieldType('text');
				}

				// Render the field.
				$results[$name] = $field->render($node, $value, $controlName);
			}
		}

		return $results;
	}

	/**
	 * Method to assign an array of fields to a group.
	 *
	 * @access	public
	 * @param	array		$fields	An array of field objects to assign.
	 * @param	string		$group	The group to assign the fields to.
	 * @return	void
	 * @since	2.0
	 */
	function setFields(&$fields, $group = '_default')
	{
		// Reset the fields group,
		$this->_fieldsets[$group] = array();

		// Add the fields to the group.
		foreach ($fields as $field) {
			$this->_fieldsets[$group][$field->attributes('name')] = $field;
		}
	}

	/**
	 * Method to get a list of fieldset groups.
	 *
	 * @access	public
	 * @return	array	An array of fieldset groups.
	 * @since	2.0
	 */
	function getGroups()
	{
		return array_keys($this->_fieldsets);
	}

	/**
	 * Method to remove a fieldset group.
	 *
	 * @access	public
	 * @param	string		$group	The fieldset group to remove.
	 * @return	void
	 * @since	2.0
	 */
	function removeGroup($group)
	{
		unset($this->_fieldsets[$group]);
	}

	/**
	 * Method to get the input control for a field.
	 *
	 * @access	public
	 * @param	string	The field name.
	 * @param	string	The control name for the form field.
	 * @param	string	The group the field is in.
	 * @param	mixed	The optional value to render as the default for the field.
	 * @return	string	The form field input control.
	 * @since	2.0
	 */
	function getInput($name, $controlName = 'jxform', $group = '_default', $value = null)
	{
		// Get the XML node.
		$node = isset($this->_fieldsets[$group][$name]) ? $this->_fieldsets[$group][$name] : null;

		// If there is no XML node for the given field name, return false.
		if (empty($node)) {
			return false;
		}

		// Load the field type.
		$type	= $node->attributes('type');
		$field	= & $this->loadFieldType($type);

		// If the field could not be loaded, get a text field.
		if ($field === false) {
			$field = & $this->loadFieldType('text');
		}

		// Get the value for the form field.
		if ($value === null) {
			$data	= $node->data();
			$value	= !empty($data) ? $data : $node->attributes('default');
		}

		// Render the field label.
		$input = $field->fetchField($name, $value, $node, $controlName);
		return $input;
	}

	/**
	 * Method to get the label for a field.
	 *
	 * @access	public
	 * @param	string	The field name.
	 * @param	string	The control name for the form field.
	 * @param	string	The group the field is in.
	 * @return	string	The form field label.
	 * @since	2.0
	 */
	function getLabel($name, $controlName = 'jxform', $group = '_default')
	{
		// Get the XML node.
		$node = isset($this->_fieldsets[$group][$name]) ? $this->_fieldsets[$group][$name] : null;

		// If there is no XML node for the given field name, return false.
		if (empty($node)) {
			return false;
		}

		// Load the field type.
		$type	= $node->attributes('type');
		$field	= & $this->loadFieldType($type);

		// If the field could not be loaded, get a text field.
		if ($field === false) {
			$field = & $this->loadFieldType('text');
		}

		// Get some field attributes for rendering the label.
		$label	= $node->attributes('label');
		$descr	= $node->attributes('description');

		// Make sure we have a valid label.
		$label = $label ? $label : $name;

		// Render the field label.
		$label = $field->fetchLabel($label, $descr, $node, $controlName, $name);
		return $label;
	}

	/**
	 * Method to get the value of a field.
	 *
	 * @access	public
	 * @param	string		$field		The field to set.
	 * @param	mixed		$default	The default value of the field if empty.
	 * @param	string		$group		The group the field is in.
	 * @return	boolean		The value of the field or the default value if empty.
	 * @since	2.0
	 */
	function getValue($field, $default = null, $group = '_default')
	{
		$return = null;

		// Get the field value if it exists.
		if (isset($this->_fieldsets[$group][$field]) && is_object($this->_fieldsets[$group][$field])) {
			$return = $this->_fieldsets[$group][$field]->data() ? $this->_fieldsets[$group][$field]->data() : $default;
		}

		return $return;
	}

	/**
	 * Method to set the value of a field.
	 *
	 * @access	public
	 * @param	string		$field	The field to set.
	 * @param	mixed		$value	The value to set the field to.
	 * @param	string		$group	The group the field is in.
	 * @return	boolean		True if field exists, false otherwise.
	 * @since	2.0
	 */
	function setValue($field, $value, $group = '_default')
	{
		// Set the field if it exists.
		if (isset($this->_fieldsets[$group][$field]) && is_object($this->_fieldsets[$group][$field])) {
			$this->_fieldsets[$group][$field]->setData($value);
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Loads form fields from an XML fieldset element optionally reseting fields before loading new ones.
	 *
	 * @access	public
	 * @param	object		$xml	The XML fieldset object.
	 * @param	boolean		$reset	Flag to toggle whether the form fieldset should be reset.
	 * @return	boolean		True on success, false otherwise.
	 * @since	2.0
	 */
	function loadFieldsXML(&$xml, $reset = true)
	{
		// Check for an XML object.
		if (!is_object($xml)) {
			return false;
		}

		// Get the group name.
		$group = ($xml->attributes('group')) ? $xml->attributes('group') : '_default';

		if ($reset) {
			// Reset the field group.
			$this->setFields($xml->children(), $group);
		} else {
			// Add to the field group.
			$this->addFields($xml->children(), $group);
		}

		// Check if there is a field path to handle.
		if ($xml->attributes('addfieldpath'))
		{
			jimport('joomla.filesystem.folder');
			jimport('joomla.filesystem.path');
			$path = JPath::clean(JPATH_ROOT.DS.$xml->attributes('addfieldpath'));

			// Add the field path to the list if it exists.
			if (JFolder::exists($path)) {
				JXForm::addFieldPath($path);
			}
		}

		return true;
	}

	/**
	 * Method to load a form field object.
	 *
	 * @access	public
	 * @param	string		$type	The field type.
	 * @param	boolean		$new	Flag to toggle whether we should get a new instance of the object.
	 * @return	mixed		Field object on success, false otherwise.
	 * @since	2.0
	 */
	function &loadFieldType($type, $new = false)
	{
		$false	= false;
		$key	= md5($type);
		$class	= 'JXFieldType'.ucfirst($type);

		// Return the field object if it already exists and we don't need a new one.
		if (isset($this->_fieldTypes[$key]) && $new === false) {
			return $this->_fieldTypes[$key];
		}

		if (!class_exists('JXFormFieldList') || !class_exists('JXFormFieldList')) {
			jximport('jxtended.form.formfield');
			jximport('jxtended.form.fields.list');
		}

//		TODO: Is this needed?
//		// Check if the field is a complex type.
//		if ($pos = strpos($type, '_'))
//		{
//			// Load the base field type.
//			$base = substr($type, 0, $pos);
//			$this->loadFieldType($base);
//		}

		if(!class_exists($class))
		{
			$paths = JXForm::addFieldPath();

			// If the type is complex, add the base type to the paths.
			if ($pos = strpos($type, '_'))
			{
				// Add the complex type prefix to the paths.
				for ($i = 0, $n = count($paths); $i < $n; $i++)
				{
					// Derive the new path.
					$path = $paths[$i].DS.substr($type, 0, $pos);

					// If the path does not exist, add it.
					if (!in_array($path, $paths)) {
						array_unshift($paths, $path);
					}
				}

				// Break off the end of the complex type.
				$type = substr($type, $pos+1);
			}

			// Try to find the field file.
			jimport('joomla.filesystem.path');
			if ($file = JPath::find($paths, strtolower($type).'.php')) {
				require_once $file;
			} else {
				return $false;
			}

			// Check once and for all if the class exists.
			if (!class_exists($class)) {
				return $false;
			}
		}

		// Instantiate a new field object.
		$this->_fieldTypes[$key] = new $class($this);

		return $this->_fieldTypes[$key];
	}

	/**
	 * Method to add a path to the list of form include paths.
	 *
	 * @access	public
	 * @param	mixed		$new	A path or array of paths to add.
	 * @return	array		The list of paths that have been added.
	 * @since	2.0
	 * @static
	 */
	function addFormPath($new = null)
	{
		static $paths;

		if (!isset($paths)) {
			$paths = array(dirname(__FILE__).DS.'forms');
		}

		// Force path to an array.
		settype($new, 'array');

		// Add the new paths to the list if not already there.
		foreach ($new as $path) {
			if (!in_array($path, $paths)) {
				array_unshift($paths, trim($path));
			}
		}

		return $paths;
	}

	/**
	 * Method to add a path to the list of field include paths.
	 *
	 * @access	public
	 * @param	mixed		$new	A path or array of paths to add.
	 * @return	array		The list of paths that have been added.
	 * @since	2.0
	 * @static
	 */
	function addFieldPath($new = null)
	{
		static $paths;

		if (!isset($paths)) {
			$paths = array(dirname(__FILE__).DS.'fields');
		}

		// Force path to an array.
		settype($new, 'array');

		// Add the new paths to the list if not already there.
		foreach ($new as $path) {
			if (!in_array($path, $paths)) {
				array_unshift($paths, trim($path));
			}
		}

		return $paths;
	}
}