<?php
/**
 * @package XCube
 * @version $Id: XCube_ActionForm.class.php,v 1.1.2.37 2006/10/01 09:46:19 minahito Exp $
 */

if (!defined('XOOPS_ROOT_PATH')) exit();

require_once XOOPS_ROOT_PATH . "/class/XCube_Validator.class.php";
require_once XOOPS_ROOT_PATH . "/class/XCube_FormFile.class.php";

//
// TODO The difference of array and no-array is too big.
// TODO Form object should have getValue(), isNull(), toString().
//

/**
 * This class fetches the input value from the request value and validate
 * those values. It separates fetching & validating from your main logic. Such
 * classes is important in web program.
 * 
 * Plus, this action form has features of one time token. It seems one kinds of
 * validations. The token is registered in templates.
 * 
 * This is suggestion of a simple action form. We do not force a module
 * developer to use this. You can learn more full-scale action forms from JAVA
 * and .NET and other PHP. And, you must use auto-generating tool when you need
 * to ActionForm that is sub-class of this class.
 * 
 * @package XCube
 */
class XCube_ActionForm
{
	/**
	 * @var array of XCube_FormProperty
	 * @access protected
	 */
	var $mFormProperties = array();

	/**
	 * @var array of XCube_FieldProperty
	 * @access protected
	 */
	var $mFieldProperties = array();
	
	/**
	 * NOTICE: This is temporary until we will decide the method of managing error.
	 * @access protected
	 * @var bool
	 */
	var $mErrorFlag = false;
	
	/**
	 * @var array of string
	 * @access protected
	 */
	var $mErrorMessages = array();
	
	/**
	 * Token string as one time token.
	 * 
	 * @var string
	 * @access private
	 */
	var $_mToken = null;

	function XCube_ActionForm()
	{
	}

	/**
	 * Set up form properties and field properties.
	 */	
	function prepare()
	{
	}
	
	/**
	 * Return token name. If the sub-class doesn't override this member
	 * function, features about one time tokens aren't used.
	 * 
	 * @access public
	 * @return string
	 */
	function getTokenName()
	{
		return null;
	}
	
	/**
	 * Generate token value, register it to sessions, return it. This member
	 * function should be called in templates. The subclass can override this
	 * to change the logic for generating token value.
	 * 
	 * @access public
	 * @return string
	 */
	function getToken()
	{
		if ($this->_mToken == null) {
			srand(microtime() * 100000);
			$this->_mToken = md5(XOOPS_SALT . uniqid(rand(), true));
			
			$_SESSION['XCUBE_TOKEN'][$this->getTokenName()] = $this->_mToken;
		}

		return $this->_mToken;
	}
	
	/**
	 * Return message when the validation of token is fail.
	 * 
	 * @return string
	 */
	function getTokenErrorMessage()
	{
		return _TOKEN_ERROR;
	}
	
	/**
	 * Set raw value as the value of the form property.
	 * 
	 * Example (1):
	 *  $this->set('name', 'Bob');  // Set 'Bob' to 'name'.
	 * 
	 * Example (2):
	 *  $this->set('names', 0, 'Bob');  // Set 'Bob' to 'name[0]'.
	 */
	function set()
	{
		if (isset($this->mFormProperties[func_get_arg(0)])) {
			if (func_num_args() == 2) {
				$value = func_get_arg(1);
				$this->mFormProperties[func_get_arg(0)]->setValue($value);
			}
			elseif (func_num_args() == 3) {
				$index = func_get_arg(1);
				$value = func_get_arg(2);
				$this->mFormProperties[func_get_arg(0)]->setValue($index, $value);
			}
		}
	}

	/**
	 * @deprecated
	 */	
	function setVar()
	{
		if (isset($this->mFormProperties[func_get_arg(0)])) {
			if (func_num_args() == 2) {
					$this->mFormProperties[func_get_arg(0)]->setValue(func_get_arg(1));
			}
			elseif (func_num_args() == 3) {
					$this->mFormProperties[func_get_arg(0)]->setValue(func_get_arg(1), func_get_arg(2));
			}
		}
	}
	
	/**
	 * Return raw value. If the return value is used in templates, escaping has
	 * to be used together.
	 * 
	 * @param $key   string Name of form property.
	 * @param $index string Subscript for array.
	 * @return mixed
	 */
	function get($key, $index=null)
	{
		return isset($this->mFormProperties[$key]) ? $this->mFormProperties[$key]->getValue($index) : null;
	}
	
	/**
	 * @deprecated
	 */
	function getVar($key,$index=null)
	{
		return $this->get($key, $index);
	}
	
	/**
	 * Return form properties of this member property.
	 * 
	 * @return XCube_AbstractProperty[]
	 */
	function &getFormProperties()
	{
		return $this->mFormProperties;
	}

	/**
	 * Fetch the input value, set it and form properties. Those values can be
	 * got, through get() method. the sub-class can define own member function
	 * to fetch. Define member functions whose name is "fetch" + "form name".
	 * For example, to fetch "message" define "fetchMessage()" function. Those
	 * function of the sub-class set value to this action form.
	 * 
	 * Example:
	 *  function fetchModifytime()
	 *  {
	 *    $this->set('modifytime', time());
	 *  }
	 * 
	 * @return void
	 * @see getFromRequest
	 */
	function fetch()
	{
		foreach (array_keys($this->mFormProperties) as $name) {
			$this->mFormProperties[$name]->fetch();
			
			$methodName = "fetch" . ucfirst($name);
			if (method_exists($this, $methodName)) {
				// call_user_func(array($this,$methodName));
				$this->$methodName();
			}
		}
	}

	/**
	 * Get the value from requests, strip slashes if magic_quotes_gps is on, 
	 * return the value. This member function supports the sub-class that
	 * defines own member functions to fetch.
	 * 
	 * @access protected
	 * @return mixed
	 */	
	function getFromRequest($key)
	{
		$value = isset($_REQUEST[$key]) ? $_REQUEST[$key] : null;
			
		if (get_magic_quotes_gpc()) {
			$token = stripslashes($value);
		}
		
		return $value;
	}

	/**
	 * Execute validation, so if a input value is wrong, error messages are
	 * added to error message buffer. The procedure of validation is the
	 * following:
	 * 
	 * 1. If this object have token name, validate one time tokens.
	 * 2. Call the validation member function of all field properties.
	 * 3. Call the member function that is defined in the sub-class.
	 * 
	 * For a basis, validations are done by functions of each field properties.
	 * But, the sub-class can define own validation logic. Define member
	 * functions whose name is "validate" + "form name". For example, to
	 * validate "message" define "validateMessage()" function.
	 * 
	 * @return void
	 */
	function validate()
	{
		//
		// check onetime & transaction token
		//
		if ($this->getTokenName() != null) {
			$key = strtr($this->getTokenName(), '.', '_');
			$token = isset($_REQUEST[$key]) ? $_REQUEST[$key] : null;
			
			if (get_magic_quotes_gpc()) {
				$token = stripslashes($token);
			}
		
			$flag = true;
			
			if (!isset($_SESSION['XCUBE_TOKEN'][$this->getTokenName()])) {
				$flag = false;
			}
			elseif ($_SESSION['XCUBE_TOKEN'][$this->getTokenName()] != $token) {
				unset($_SESSION['XCUBE_TOKEN'][$this->getTokenName()]);
				$flag = false;
			}
			
			if (!$flag) {
				$message = $this->getTokenErrorMessage();
				if ($message == null) {
					$this->mErrorFlag = true;
				}
				else {
					$this->addErrorMessage($message);
				}
			}
			
			//
			// clear token
			//
			unset($_SESSION['XCUBE_TOKEN'][$this->getTokenName()]);
		}
		
		foreach (array_keys($this->mFormProperties) as $name) {
			if (isset($this->mFieldProperties[$name])) {
				if ($this->mFormProperties[$name]->isArray()) {
					foreach (array_keys($this->mFormProperties[$name]->mProperties) as $_name) {
						$this->mFieldProperties[$name]->validate($this->mFormProperties[$name]->mProperties[$_name]);
					}
				}
				else {
					$this->mFieldProperties[$name]->validate($this->mFormProperties[$name]);
				}
			}
		}

		//
		// If this class has original validation methods, call it.
		//
		foreach (array_keys($this->mFormProperties) as $name) {
			$methodName = "validate" . ucfirst($name);
			if (method_exists($this, $methodName)) {
				// call_user_func(array($this,$methodName));
				$this->$methodName();
			}
		}
	}

	/**
	 * If the action form keeps error messages or the error flag, return true.
	 * 
	 * @access public
	 * @return bool
	 */
	function hasError()
	{
		return (count($this->mErrorMessages) > 0 || $this->mErrorFlag);
	}

	/**
	 * Add $message to error message buffer.
	 * 
	 * @access protected
	 */	
	function addErrorMessage($message)
	{
		$this->mErrorMessages[] = $message;
	}
	
	/**
	 * Return error messages.
	 * 
	 * @access public
	 * @return array
	 */
	function getErrorMessages()
	{
		return $this->mErrorMessages;
	}

	/**
	 * Set initial values to this action form from a object. This member
	 * function mediates between the logic and the validation. For example,
	 * developers can use this method to load values from XoopsSimpleObject.
	 * 
	 * This member function is abstract. But, the sub-class of this class
	 * doesn't have to implement this.
	 * 
	 * @param $obj mixed
	 * @return void
	 */
	function load(&$obj)
	{
	}
	
	/**
	 * Set input values to a object from this action form. This member function
	 * mediates between the logic and the result of validations. For example,
	 * developers can use this method to set values to XoopsSimpleObject.
	 * 
	 * This member function is abstract. But, the sub-class of this class
	 * doesn't have to implement this.
	 * 
	 * @param $obj mixed
	 * @return void
	 */
	function update(&$obj)
	{
	}
}

class XCube_PropertyInterface
{
	function XCube_PropertyInterface($name)
	{
	}
	
	function fetch($key = null)
	{
	}
	
	function setValue($arg0 = null, $arg1 = null)
	{
	}

	function getValue($arg0 = null)
	{
	}
	
	/**
	 * @return bool
	 */
	function isArray()
	{
	}
}

class XCube_AbstractProperty extends XCube_PropertyInterface
{
	var $mName=null;
	var $mValue=null;
	
	function XCube_AbstractProperty($name)
	{
		$this->mName=$name;
	}

	function setValue($value, $dmy = null)
	{
		$this->mValue = $value;
	}

	function getValue($index = null)
	{
		return $this->mValue;
	}

	function fetch($key = null)
	{
		$value = null;

		if ($key !== null && is_array($_REQUEST[$this->mName]) && isset($_REQUEST[$this->mName][$key])) {
			$value = $_REQUEST[$this->mName][$key];
		}
		elseif (isset($_REQUEST[$this->mName]) && !is_array($_REQUEST[$this->mName])) {
			$value = $_REQUEST[$this->mName];
		}
		
	    if (get_magic_quotes_gpc()) {
			$this->mValue = stripslashes($value);
		}
		else {
			$this->mValue = $value;
		}
	}
	
	function isArray()
	{
		return false;
	}
	
	function isNull()
	{
		return (strlen(trim($this->mValue)) == 0);
	}
	
	function toNumber()
	{
		return $this->mValue;
	}
	
	function toString()
	{
		return $this->mValue;
	}
}

class XCube_AbstractArrayProperty extends XCube_PropertyInterface
{
	var $mName = null;
	var $mProperties = array();
	
	var $mPropertyClassName = null;
	
	function XCube_AbstractArrayProperty($name)
	{
		$this->mName = $name;
	}
	
	function fetch($key = null)
	{
		unset($this->mProperties);
		$this->mProperties = array();
		if (isset($_REQUEST[$this->mName]) && is_array($_REQUEST[$this->mName])) {
			foreach ($_REQUEST[$this->mName] as $_key => $_val) {
				$this->mProperties[$_key] =& new $this->mPropertyClassName($this->mName);
				$this->mProperties[$_key]->fetch($_key);
			}
		}
	}

	function setValue($index, $value)
	{
		if (!isset($this->mProperties[$index])) {
			$this->mProperties[$index] = new $this->mPropertyClassName($this->mName);
		}
		$this->mProperties[$index]->setValue($value);
	}

	function getValue($index=null)
	{
		if($index==null) {
			$ret = array();
			
			foreach ($this->mProperties as $name => $value) {
				$ret[$name] = $value->getValue();
			}

			return $ret;
		}

		return isset($this->mProperties[$index]) ? $this->mProperties[$index]->getValue() : null;
	}
	
	function isArray()
	{
		return true;
	}
}

class XCube_BoolProperty extends XCube_AbstractProperty
{
	function fetch($key = null)
	{
		parent::fetch($key);
		if (strlen(trim($this->mValue)) > 0) {
			$this->mValue = (intval($this->mValue)>0) ? 1 : 0;
		}
		else {
			$this->mValue = 0;
		}
	}
}

class XCube_BoolArrayProperty extends XCube_AbstractArrayProperty
{
	var $mPropertyClassName = "XCube_BoolProperty";
}

class XCube_IntProperty extends XCube_AbstractProperty
{
	function fetch($key = null)
	{
		parent::fetch($key);
		if (strlen(trim($this->mValue)) > 0) {
			$this->mValue = intval($this->mValue);
		}
		else {
			$this->mValue = null;
		}
	}
}

class XCube_IntArrayProperty extends XCube_AbstractArrayProperty
{
	var $mPropertyClassName = "XCube_IntProperty";
}

class XCube_FloatProperty extends XCube_AbstractProperty
{
	function fetch($key = null)
	{
		parent::fetch($key);
		if (strlen(trim($this->mValue)) > 0) {
			$this->mValue = floatval($this->mValue);
		}
		else {
			$this->mValue = null;
		}
	}
}

class XCube_FloatArrayProperty extends XCube_AbstractArrayProperty
{
	var $mPropertyClassName = "XCube_FloatProperty";
}

/**
 *  This class shows the property of string. Check whether a request includes control
 * code. If it does, stop own process.
 */
class XCube_StringProperty extends XCube_AbstractProperty
{
	function fetch($key = null)
	{
		parent::fetch($key);
		if (preg_match_all("/[\\x00-\\x1f]/", $this->mValue, $matches, PREG_PATTERN_ORDER)) {
			foreach ($matches[0] as $match) {
				die("[". $this->mName . "]Get control code :" . ord($match));
			}
		}
	}
	
	function toNumber()
	{
		return intval($this->mValue);
	}
}

class XCube_StringArrayProperty extends XCube_AbstractArrayProperty
{
	var $mPropertyClassName = "XCube_StringProperty";
}

/**
 *  This class shows the property of text. Check whether a request includes control
 * code. If it does, stop own process.
 */
class XCube_TextProperty extends XCube_AbstractProperty
{
	function fetch($key = null)
	{
		parent::fetch($key);
		$matches = array();
		$allow_codes = array(9,10,13);

		if (preg_match_all("/[\\x00-\\x09]|[\\x0b-\\x0c]|[\\x0e-\\x1f]/", $this->mValue, $matches,PREG_PATTERN_ORDER)) {
			foreach ($matches[0] as $match) {
				if (!in_array(ord($match),$allow_codes) && ord($match)<32)
					die("Get control code :" . ord($match));
			}
		}
	}
	
	function toNumber()
	{
		return intval($this->mValue);
	}
}

class XCube_TextArrayProperty extends XCube_AbstractArrayProperty
{
	var $mPropertyClassName = "XCube_TextProperty";
}

class XCube_FileProperty extends XCube_AbstractProperty
{
	function XCube_FileProperty($name)
	{
		parent::XCube_AbstractProperty($name);
		$this->mValue =& new XCube_FormFile($name);
	}
	
	function fetch($key = null)
	{
		if (!is_object($this->mValue)) {
			return false;
		}
		
		$this->mValue->mKey = $key;
		
		$this->mValue->fetch();
		if (!$this->mValue->hasUploadFile()) {
			$this->mValue = null;
		}
	}
	
	function isNull()
	{
		if (!is_object($this->mValue)) {
			return true;
		}
		
		return !$this->mValue->hasUploadFile();
	}
	
	function toString()
	{
		return null;
	}
	
	function toNumber()
	{
		return null;
	}
}

class XCube_FileArrayProperty extends XCube_AbstractArrayProperty
{
	var $mPropertyClassName = "XCube_FileProperty";
	
	function fetch($key = null)
	{
		unset($this->mProperties);
		$this->mProperties = array();
		if (isset($_FILES[$this->mName]) && is_array($_FILES[$this->mName]['name'])) {
			foreach ($_FILES[$this->mName]['name'] as $_key => $_val) {
				$this->mProperties[$_key] =& new $this->mPropertyClassName($this->mName);
				$this->mProperties[$_key]->fetch($_key);
			}
		}
	}
}

class XCube_ImageFileProperty extends XCube_FileProperty
{
	function XCube_ImageFileProperty($name)
	{
		parent::XCube_AbstractProperty($name);
		$this->mValue =& new XCube_FormImageFile($name);
	}
}

class XCube_ImageFileArrayProperty extends XCube_FileArrayProperty
{
	var $mPropertyClassName = "XCube_ImageFileProperty";
}

class XCube_FieldProperty
{
	var $mForm;
	
	var $mDepends;
	var $mMessages;
	var $mVariables;

	function XCube_FieldProperty(&$form)
	{
		$this->mForm=&$form;
	}
	
	function setDependsByArray($dependsArr)
	{
		foreach($dependsArr as $dependName){
			$instance =& XCube_DependClassFactory::factoryClass($dependName);
			if($instance!==null)
				$this->mDepends[$dependName]=&$instance;

			unset($instance);
		}
	}
	
	function addMessage($name,$message)
	{
		if(func_num_args()>=2) {
			$args=func_get_args();
			$this->mMessages[$args[0]]['message']=$args[1];
			for($i=0;isset($args[$i+2]);$i++) {
				$this->mMessages[$args[0]]['args'][$i]=$args[$i+2];
			}
		}
	}
	
	function renderMessage($name)
	{
		if(!isset($this->mMessages[$name]))
			return null;

		$message=$this->mMessages[$name]['message'];

		if(isset($this->mMessages[$name]['args'])) {
			for($i=0;$i<count($this->mMessages[$name]['args']);$i++) {
				$message=str_replace("{".$i."}",$this->mMessages[$name]['args'][$i],$message);
			}
		}
		
		return $message;
	}

	function addVar($name,$value)
	{
		$this->mVariables[$name]=$value;
	}
	
	/**
	 * TODO This class already has form property instance.
	 */
	function validate(&$form)
	{
		if(is_array($this->mDepends) && count($this->mDepends)>0) {
			foreach($this->mDepends as $name => $depend) {
				if(!$depend->isValid($form, $this->mVariables)) {
					// Error
					// NOTICE: This is temporary until we will decide the method of managing error.
					$this->mForm->mErrorFlag=true;
					
					// TEST!!
					$this->mForm->addErrorMessage($this->renderMessage($name));
				}
				else {
					// OK
				}
			}
		}
	}
}

class XCube_DependClassFactory
{
	function &factoryClass($dependName)
	{
		static $_cache;
		
		if (!is_array($_cache)) {
			$_cache = array();
		}
		
		if (!isset($_cache[$dependName])) {
			// or switch?
			$class_name = "XCube_" . ucfirst($dependName) . "Validator";
			if(class_exists($class_name)) {
				$_cache[$dependName] =& new $class_name();
			}
			else {
				// FIXME:: use delegate?
				die ("This is an error message of Alpha or Beta series. ${dependName} Validator is not found.");
			}
		}

		return $_cache[$dependName];
	}
}

?>