<?php
/**
 * @package XCube
 * @version $Id: XCube_ActionForm.class.php,v 1.1.2.3 2006/10/07 09:26:31 minahito Exp $
 */

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

require_once XOOPS_ROOT_PATH . "/core/XCube_Property.class.php";
require_once XOOPS_ROOT_PATH . "/core/XCube_Validator.class.php";
require_once XOOPS_ROOT_PATH . "/core/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 through the
 * current context object 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.
 * 
 * XCube_ActionForm contains the one-time token feature for CSRF. But, if the
 * current HTTP request is from the web service, the token isn't needed.
 * Therefore, this class decides whether to use the token with the information
 * of the context.
 *
 * @package XCube
 */
class XCube_ActionForm
{
	/**
	 * [READ ONLY] The context object. Enables to access the HTTP-request
	 * information. Basically, this member property is read only. Initialized
	 * in the constructor.
	 * 
	 * @access protected
	 * @var XCube_HttpContext
	 */
	var $mContext = null;
	
	/**
	 * [READ ONLY] The object which has a interface of XCube_Principal. Enables
	 * to check permissions of the current HTTP-request through principal
	 * object. Basically, this member property is read only. Initialized in
	 * constructor.
	 *
	 * @access protected
	 * @var XCube_Principal
	 */
	var $mUser = null;
	
	/**
	 * @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.
	 * [FIXME]
	 *
	 * @var string
	 * @access private
	 */
	var $_mToken = null;
	
	function XCube_ActionForm()
	{
		$root =& XCube_Root::getSingleton();
		$this->mContext =& $root->getContext();
		$this->mUser =& $this->mContext->getUser();
	}
	
	/**
	 * 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;	//< FIXME
	}
	
	/**
	 * 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) {
			if ($this->mFormProperties[$name]->hasFetchControl()) {
				$this->mFormProperties[$name]->fetch($this);
			}
			else {
				$value = $this->mContext->mRequest->getRequest($name);
				$this->mFormProperties[$name]->set($value);
			}
			$methodName = "fetch" . ucfirst($name);
			if (method_exists($this, $methodName)) {
				// call_user_func(array($this,$methodName));
				$this->$methodName();
			}
		}
	}
	
	/**
	 * 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_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];
	}
}

?>