<?php
/*
 * e107 website system
 *
 * Copyright (C) 2008-2010 e107 Inc (e107.org)
 * Released under the terms and conditions of the
 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
 *
 * e107 Base Model
 *
 * $Id$
 * $Author$
*/

if (!defined('e107_INIT')) { exit; }

/**
 * Base e107 Object class
 *
 * @package e107
 * @category e107_handlers
 * @version 1.0
 * @author SecretR
 * @copyright Copyright (C) 2010, e107 Inc.
 */
class e_object
{
    /**
     * Object data
     *
     * @var array
     */
    protected $_data = array();

	/**
	 * Model parameters passed mostly from external sources
	 *
	 * @var array
	 */
	protected $_params = array();


    /**
    * Name of object id field
    * Required for {@link getId()()} method
    *
    * @var string
    */
    protected $_field_id;

    /**
     * Constructor - set data on initialization
     *
     * @param array $data
     */
	function __construct($data = array())
	{
		if(is_array($data)) $this->setData($data);
	}

    /**
     * Set name of object's field id
     *
     * @see getId()
     *
     * @param   string $name
     * @return object e_object
     */
    public function setFieldIdName($name)
    {
        $this->_field_id = $name;
        return $this;
    }

	/**
	 * Retrieve name of object's field id
	 *
	 * @return  string
	 * @see getId()
	 *
	 */
    public function getFieldIdName()
    {
        return $this->_field_id;
    }

    /**
     * Retrieve object primary id field value
     *
     * @return mixed
     */
    public function getId()
    {

        if ($this->getFieldIdName())
        {
            return $this->get($this->getFieldIdName(), 0); // default of NULL will break MySQL strict in most cases.
        }
        return $this->get('id', 0);
    }

    /**
     * Set object primary id field value
     *
     * @return e_object
     */
    public function setId($id)
    {
        if ($this->getFieldIdName())
        {
            return $this->set($this->getFieldIdName(), $id);
        }
        return $this;
    }

    /**
     * Retrieves data from the object ($_data) without
     * key parsing (performance wise, prefered when possible)
     *
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
	public function get($key, $default = null)
    {
    	return (isset($this->_data[$key]) ? $this->_data[$key] : $default);
    }

    /**
     * Get object data
     * @return array
     */
	public function getData()
    {
    	return $this->_data;
    }

    /**
     * Overwrite data in the object for a single field.
     *
     * @param string $key
     * @param mixed $value
     * @return e_object
     */
	public function set($key, $value)
    {
    	$this->_data[$key] = $value;
    	return $this;
    }

    /**
     * Set object data
     * @return e_object
     */
	public function setData($data)
    {
    	$this->_data = $data;
    	return $this;
    }

    /**
     * Update object data
     * @return e_object
     */
	public function addData($data)
    {
    	foreach($data as $key => $val)
		{
			$this->set($key, $val);
		}
    	return $this;
    }

    /**
     * Remove object data key
     *
     * @param string $key
     * @return e_object
     */
	public function remove($key)
    {
    	unset($this->_data[$key]);
    	return $this;
    }

    /**
     * Reset  object data key
     *
     * @return e_object
     */
	public function removeData()
    {
    	$this->_data = array();
    	return $this;
    }

    /**
     * Check if key is set
     * @param string $key
     * @return boolean
     */
    public function is($key)
    {
    	return (isset($this->_data[$key]));
    }

    /**
     * Check if key is set and not empty
     * @param string $key
     * @return boolean
     */
    public function has($key)
    {
    	return (isset($this->_data[$key]) && !empty($this->_data[$key]));
    }

    /**
     * Check if object has data
     * @return boolean
     */
    public function hasData()
    {
    	return !empty($this->_data);
    }

	/**
	 * Set parameter array
	 * @param array $params
	 * @return e_object
	 */
	public function setParams(array $params)
	{
		$this->_params = $params;
		return $this;
	}

	/**
	 * Update parameter array
	 * @param array $params
	 * @return object e_object
	 */
	public function updateParams(array $params)
	{
		foreach ($params as $k => $v)
		{
			$this->setParam($k, $v);
		}

		return $this;
	}

	/**
	 * Get parameter array
	 *
	 * @return array parameters
	 */
	public function getParams()
	{
		return $this->_params;
	}

	/**
	 * Set parameter
	 *
	 * @param string $key
	 * @param mixed $value
	 * @return e_object
	 */
	public function setParam($key, $value)
	{
		if(null === $value)
		{
			unset($this->_params[$key]);
		}
		else $this->_params[$key] = $value;

		return $this;
	}

	/**
	 * Get parameter
	 *
	 * @param string $key
	 * @param mixed $default
	 */
	public function getParam($key, $default = null)
	{
		return (isset($this->_params[$key]) ? $this->_params[$key] : $default);
	}

	/**
	 * Convert object data to simple shortcodes (e_vars object)
	 * @return e_vars
	 */
	public function toSc()
	{
		return new e_vars($this->_data);
	}

	/**
	 * Convert object data to array
	 * @return string
	 */
	public function toJson()
	{
		return json_encode($this->_data);
	}

	/**
	 * Convert object to array
	 * @return array object data
	 */
	public function toArray()
	{
		return $this->_data;
	}

	/**
	 * Magic method - convert object data to an array
	 *
	 * @return array
	 */
	public function __toArray()
	{
		return $this->toArray();
	}

	/**
	 * Convert object data to a string
	 *
	 * @param boolean $AddSlashes
	 * @return string
	 */
	public function toString($AddSlashes = false)
	{
		return (string) e107::serialize($this->toArray(), $AddSlashes);
	}

	/**
	 * Magic method - convert object data to a string
	 * NOTE: before PHP 5.2.0 the __toString method was only
	 * called when it was directly combined with echo() or print()
	 *
	 * NOTE: PHP 5.3+ is throwing parse error if __toString has optional arguments.
	 *
	 * @return string
	 */
	public function __toString()
	{
		return $this->toString(false);
	}

	/**
	 * Magic setter
	 * Triggered on e.g. <code><?php $e_object->myKey = 'someValue'; </code>
	 *
	 * @param string $key
	 * @param mixed $value
	 */
	public function __set($key, $value)
	{
		// Unset workaround - PHP < 5.1.0
		if(null === $value) $this->remove($key);
		else $this->set($key, $value);
	}

	/**
	 * Magic getter
	 * Triggered on e.g. <code><?php print($e_object->myKey); </code>
	 * @param string $key
	 * @return mixed value or null if key not found
	 */
	public function __get($key)
	{
		if($this->is($key))
		{
			return $this->get($key);
		}

		return null;
	}

	/**
	 * Magic method to check if given data key is set.
	 * Triggered on <code><?php isset($e_object->myKey); </code>
	 * NOTE: works on PHP 5.1.0+
	 *
	 * @param string $key
	 * @return boolean
	 */
	public function __isset($key)
	{
		return $this->is($key);
	}

	/**
	 * Magic method to unset given data key.
	 * Triggered on <code><?php unset($e_object->myKey); </code>
	 * NOTE: works on PHP 5.1.0+
	 *
	 * @param string $key
	 */
	public function __unset($key)
	{
		$this->remove($key);
	}
}


/**
 * Data object for e_parse::simpleParse()
 * NEW - not inherits core e_object
 * Moved from e_parse_class.php
 * Could go in separate file in the future, together with e_object class
 */
class e_vars extends e_object
{
	/**
	 * Get data array
	 *
	 * @return array
	 */
	public function getVars()
	{
		return $this->getData();
	}

	/**
	 * Set array data
	 *
	 * @param array $array
	 * @return e_vars
	 */
	public function setVars(array $array)
	{
		$this->setData($array);
		return $this;
	}

	/**
	 * Add array data to the object (merge with existing)
	 *
	 * @param array $array
	 * @return void
	 */
	public function addVars(array $array)
	{
		$this->addData($array);
	}

	/**
	 * Reset object data
	 *
	 * @return e_vars
	 */
	public function emptyVars()
	{
		$this->removeData();
		return $this;
	}

	/**
	 * Check if there is data available
	 *
	 * @return boolean
	 */
	public function isEmpty()
	{
		return (!$this->hasData());
	}

	/**
	 * Check if given data key is set
	 * @param string $key
	 * @return boolean
	 */
	public function isVar($key)
	{
		return $this->is($key);
	}

	/**
	 * No need of object conversion, optional cloning
	 * @param boolean $clone return current object clone
	 * @return e_vars
	 */
	public function toSc($clone = false)
	{
		if($clone) return clone $this;
		return $this;
	}
}

/**
 * Base e107 Model class
 *
 * @package e107
 * @category e107_handlers
 * @version 1.0
 * @author SecretR
 * @copyright Copyright (C) 2010, e107 Inc.
 */
class e_model extends e_object
{
    /**
     * Data structure (types) array, required for {@link e_front_model::sanitize()} method,
     * it also serves as a map (find data) for building DB queries,
     * copy/sanitize posted data to object data, etc.
     *
     * This can/should be overwritten by extending the class
     *
     * @var array
     */
    protected $_data_fields = array();



	/**
	 * Current model field types eg. text, bbarea, dropdown etc.
	 *
	 *
	 * @var string
	 */
	protected $_field_input_types = array();


	/**
	 * Current model DB table, used in all db calls
	 *
	 * This can/should be overwritten/set by extending the class
	 *
	 * @var string
	 */
	protected $_db_table;

    /**
     * Current url Profile data
	 * Example: array('route'=>'page/view/index', 'vars' => array('id' => 'page_id', 'sef' => 'page_sef'), 'name' => 'page_title', 'description' => '');
     * @var string
     */
    protected $_url = array();


    /**
     * Current Featurebox Profile data
	 * Example: array('title' => 'page_title', 'text' => '');
     * @var string
     */
    protected $_featurebox = array();

    /**
     * Runtime cache of parsed from {@link _getData()} keys
     *
     * @var array
     */
    protected $_parsed_keys = array();

    /**
     * Avoid DB calls if data is not changed
     *
     * @see _setData()
     * @var boolean
     */
    protected $data_has_changed = false;

	/**
	 * Namespace to be used for model related system messages in {@link eMessage} handler
	 *
	 * @var string
	 */
	protected $_message_stack = 'default';

	/**
	 * Cache string to be used from _get/set/clearCacheData() methods
	 *
	 * @var string
	 */
	protected $_cache_string = null;

	/**
	 * Force Cache even if system cahche is disabled
	 * Default is false
	 *
	 * @var boolean
	 */
	protected $_cache_force = false;


	/**
	 * Optional DB table - used for auto-load data from the DB
	 * @return string
	 */
	public function getModelTable()
	{
		return $this->_db_table;
	}

	/**
	 * Set model DB table
	 * @param string $table
	 * @return e_model
	 */
	public function setModelTable($table)
	{
		$this->_db_table = $table;
		return $this;
	}


    /**
     * Set model Url Profile
     * @param string $url
     * @return e_model
     */
    public function setUrl($url)
    {
    	if(!is_array($url)) $url = array('route' => $url);
        $this->_url = $url;
        return $this;
    }

    /**
     * Get url profile
     * @return array
     */
    public function getUrl()
    {
        return $this->_url;
    }

    /**
     * Set model Featurebox  Profile
     * @param string $fb
     * @return e_model
     */
    public function setFeaturebox($fb)
    {
    //	if(!is_array($url)) $url = array('route' => $url);
        $this->_featurebox = $fb;
        return $this;
    }

    /**
     * Get Featurebox profile
     * @return array
     */
    public function getFeaturebox()
    {
        return $this->_featurebox;
    }


    /**
     * Generic URL assembling method
	 * @param array $options [optional] see eRouter::assemble() for $options structure
	 * @param boolean $extended [optional] if true, method will return an array containing url, title and description of the url
     * @return mixed URL string or extended array data
     */
    public function url($ids, $options = array(), $extended = false)
    {
        $urldata = $this->getUrl();
		if(empty($urldata) || !vartrue($urldata['route'])) return ($extended ? array() : null);

		$eurl = e107::getUrl();

	    if (empty($options))
	    {
		    $options = array();
	    }
	    elseif(is_string($options))
	    {
		    parse_str($options, $options);
	    }

	    $vars = $this->toArray();
		if(!isset($options['allow']) || empty($options['allow']))
		{
			if(vartrue($urldata['vars']) && is_array($urldata['vars']))
			{
				$vars = array();
				foreach ($urldata['vars'] as $var => $field)
				{
					if($field === true) $field = $var;
					$vars[$var] = $this->get($field);
				}
			}
		}

		$method = isset($options['sc']) ? 'sc' : 'create';

		$url = e107::getUrl()->$method($urldata['route'], $vars, $options);

		if(!$extended)
		{
			return $url;
		}

		return array(
			'url' => $url,
			'name' => vartrue($urldata['name']) ? $this->get($urldata['name']) : '',
			'description' => vartrue($urldata['description']) ? $this->get($urldata['description']) : '',
		);
    }


     /**
     * Generic Featurebox assembling method
     * @return void URL string or extended array data
     */
    public function featurebox($options = array(), $extended = false)
    {


    }













    /**
     * Get data fields array
     * @return array
     */
    public function getDataFields()
    {
    	return $this->_data_fields;
    }


	/**
	 * @param $key
	 * @return bool
	 */
	public function getFieldInputType($key)
    {
        if(isset($this->_field_input_types[$key]))
        {
            return $this->_field_input_types[$key];
        }

        return false;
    }

    /**
     * Set Predefined data fields in format key => type
     * @return object e_model
     */
    public function setDataFields($data_fields)
    {
    	$this->_data_fields = $data_fields;
		return $this;
    }

	/**
     * Set Predefined data fields in format key => type
     * @return e_model
     */
    public function setFieldInputTypes($fields)
    {
    	$this->_field_input_types = $fields;
		return $this;
    }

    /**
     * Set Predefined data field
     * @return e_model
     */
    public function setDataField($field, $type)
    {
    	$this->_data_fields[$field] = $type;
		return $this;
    }

    /**
     * Retrieves data from the object ($_data) without
     * key parsing (performance wise, prefered when possible)
     *
     * @see _getDataSimple()
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
	public function get($key, $default = null)
    {
    	return $this->_getDataSimple((string) $key, $default);
    }

    /**
     * Retrieves data from the object ($_data)
     * If $key is empty, return all object data
     *
     * @see _getData()
     * @param string $key
     * @param mixed $default
     * @param integer $index
     * @return mixed
     */
	public function getData($key = '', $default = null, $index = null)
    {
    	return $this->_getData($key, $default, $index);
    }

    /**
     * Overwrite data in the object for a single field. Key is not parsed.
     * Public proxy of {@link _setDataSimple()}
     * Data isn't sanitized so use this method only when data comes from trustable sources (e.g. DB)
     *
     *
     * @see _setData()
     * @param string $key
     * @param mixed $value
     * @param boolean $strict update only
     * @return e_model
     */
	public function set($key, $value = null, $strict = false)
    {
    	return $this->_setDataSimple($key, $value, $strict);
    }

    /**
     * Overwrite data in the object. Public proxy of {@link _setData()}
     * Data isn't sanitized so use this method only when data comes from trustable sources (e.g. DB)
     *
     * @see _setData()
     * @param string|array $key
     * @param mixed $value
     * @param boolean $strict update only
     * @return e_model
     */
	public function setData($key, $value = null, $strict = false)
    {
    	return $this->_setData($key, $value, $strict);
    }

    /**
     * Add data to the object.
     * Retains existing data in the object.
     * Public proxy of {@link _addData()}
     *
     * If $override is false, data will be updated only (check against existing data)
     *
     * @param string|array $key
     * @param mixed $value
     * @param boolean $override override existing data
     * @return e_model
     */
    public function addData($key, $value = null, $override = true)
    {
    	return $this->_addData($key, $value, $override);
    }

	/**
     * Unset single field from the object.
     * Public proxy of {@link _unsetDataSimple()}
     *
     * @param string $key
     * @return e_model
     */
    public function remove($key)
    {
    	return $this->_unsetDataSimple($key);
    }

	/**
     * Unset data from the object.
     * $key can be a string only. Array will be ignored.
     * '/' inside the key will be treated as array path
     * if $key is null entire object will be reset
     *
     * Public proxy of {@link _unsetData()}
     *
     * @param string|null $key
     * @return e_model
     */
    public function removeData($key = null)
    {
    	return $this->_unsetData($key);
    }

    /**
     * @param string $key
     * @return boolean
     */
    public function has($key)
    {
    	return $this->_hasData($key);
    }

    /**
     * @param string $key
     * @return boolean
     */
    public function hasData($key = '')
    {
    	return $this->_hasData($key);
    }

    /**
     * @param string $key
     * @return boolean
     */
    public function isData($key)
    {
    	return $this->_isData($key);
    }

    /**
     * @param boolean $new_state new object state if set
     * @return boolean
     */
    public function isModified($new_state = null)
    {
    	if(is_bool($new_state))
    	{
    		$this->data_has_changed = $new_state;
    	}
    	return $this->data_has_changed;
    }

	/**
	 * Retrieves data from the object
	 *
	 * If $key is empty will return all the data as an array
	 * Otherwise it will return value of the attribute specified by $key
	 * '/' inside the key will be treated as array path (x/y/z equals to [x][y][z]
	 *
	 * If $index is specified it will assume that attribute data is an array
	 * and retrieve corresponding member.
	 *
	 * NEW: '/' supported in keys now, just use double slashes '//' as key separator
	 * Examples:
	 * - 'key//some/key/with/slashes//more' -> [key][some/key/with/slashes][more]
	 * - '//some/key' -> [some/key] - starting with // means - don't parse!
	 * - '///some/key/' -> [/some/key/]
	 * - '//key//some/key/with/slashes//more' WRONG -> single key [key//some/key/with/slashes//more]
	 *
	 * @param string $key
	 * @param mixed $default
	 * @param null $index
	 * @param string $data_src
	 * @return mixed
	 */
    protected function _getData($key = '', $default = null, $index = null, $data_src = '_data')
    {
        if ('' === $key)
        {
            return $this->$data_src;
        }

        $simple = false;
        if(strpos($key, '//') === 0)
        {
        	$key = substr($key, 2);
        	$simple = true;
        }
        /*elseif($key[0] == '/')
        {
        	// just use it!
        	$simple = true;
        }*/
        else
        {
        	$simple = strpos($key, '/') === false;
        }

        // Fix - check if _data[path/to/value] really doesn't exist
        if (!$simple)
        {
        	//$key = trim($key, '/');
        	if(isset($this->_parsed_keys[$data_src.'/'.$key]))
        	{
        		return $this->_parsed_keys[$data_src.'/'.$key];
        	}
        	// new feature (double slash) - when searched key string is key//some/key/with/slashes//more
        	// -> search for 'key' => array('some/key/with/slashes', array('more' => value));
            $keyArr = explode(strpos($key, '//') ? '//' : '/', $key);
            $data = $this->$data_src;
            foreach ($keyArr as $i => $k)
            {
                if ('' === $k)
                {
                    return $default;
                }

                if (is_array($data))
                {
                    if (!isset($data[$k]))
                    {
                        return $default;
                    }
                    $data = $data[$k];
                }
                else
                {
                    return $default;
                }
            }
            $this->_parsed_keys[$data_src.'/'.$key] = $data;
            return $data;
        }

        //get $index
        if (isset($this->{$data_src}[$key]))
        {
            if (null === $index)
            {
                return $this->{$data_src}[$key];
            }

            $value = $this->{$data_src}[$key];
            if (is_array($value))
            {
                if (isset($value[$index]))
                {
                    return $value[$index];
                }
                return $default;
            }
            elseif (is_string($value))
            {
                $arr = explode("\n", $value);
                return (isset($arr[$index]) ? $arr[$index] : $default);
            }
            return $default;
        }
        return $default;
    }

	/**
	 * Get value from _data array without parsing the key
	 *
	 * @param string $key
	 * @param mixed $default
	 * @param string $data_src
	 * @return mixed
	 */
    protected function _getDataSimple($key, $default = null, $data_src = '_data')
    {
        return isset($this->{$data_src}[$key]) ? $this->{$data_src}[$key] : $default;
    }

    /**
     * Overwrite data in the object.
     *
     * $key can be string or array.
     * If $key is string, the attribute value will be overwritten by $value
     * '/' inside the key will be treated as array path
     *
     * If $key is an array and $strict is false, it will overwrite all the data in the object.
     *
     * If $strict is true and $data_src is '_data', data will be updated only (no new data will be added)
     *
     * NEW: '/' supported in keys now, just use double slashes '//' as key separator
     * Examples:
     * - 'key//some/key/with/slashes//more' -> [key][some/key/with/slashes][more]
     * - '//some/key' -> [some/key] - starting with // means - don't parse!
     * - '///some/key/' -> [/some/key/]
     * - '//key//some/key/with/slashes//more' WRONG -> single key [key//some/key/with/slashes//more]
     *
     *
     * @param string|array $key
     * @param mixed $value
     * @param boolean $strict
     * @param string $data_src
     * @return e_model
     */
    protected function _setData($key, $value = null, $strict = false, $data_src = '_data')
    {
        if(is_array($key))
        {
            if($strict)
	    	{
				foreach($key as $k => $v)
		        {
		        	$this->_setData($k, $v, true, $data_src);
		        }
		        return $this;
	    	}

            $this->$data_src = $key;
            return $this;
        }

        //multidimensional array support - strict _setData for values of type array
    	if($strict && !empty($value) && is_array($value))
       	{
			foreach($value as $k => $v)
			{
				// new - $k couldn't be a path - e.g. 'key' 'value/value1'
				// will result in 'key' => 'value/value1' and NOT 'key' => array('value' => value1)
			    $this->_setData($key.'//'.$k, $v, true, $data_src);
			}
			return $this;
       	}

        $simple = false;
        if(strpos($key, '//') === 0)
        {
        	// NEW - leading '//' means set 'some/key' without parsing it
        	// Example: '//some/key'; NOTE: '//some/key//more/depth' is NOT parsed
        	// if you wish to have array('some/key' => array('more/depth' => value))
        	// right syntax is 'some/key//more/depth'
        	$key = substr($key, 2);
        	$simple = true;
        }
        /*elseif($key[0] == '/')
        {
        	$simple = true;
        }*/
        else
        {
        	$simple = strpos($key, '/') === false;
        }

        //multidimensional array support - parse key
        if(!$simple)
        {
        	//$key = trim($key, '/');
        	//if strict - update only
	        if($strict && !$this->isData($key))
	        {
	        	return $this;
	        }

        	// new feature (double slash) - when parsing key: key//some/key/with/slashes//more
        	// -> result is 'key' => array('some/key/with/slashes', array('more' => value));
	        $keyArr = explode(strpos($key, '//') ? '//' : '/', $key);
        	//$keyArr = explode('/', $key);
        	$data = &$this->{$data_src};
            for ($i = 0, $l = count($keyArr); $i < $l; $i++)
            {

	            $k = $keyArr[$i];

	            if (!isset($data[$k]) || empty($data[$k])) // PHP7.1 fix. Reset to empty array() if $data[$k] is an empty string. Reason for empty string still unknown.
	            {
	                $data[$k] = array();
	            }

	            $data = &$data[$k];
	        }

	        //data has changed - optimized
	        if('_data' === $data_src && !$this->data_has_changed)
	        {
	        	$this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value);
	        }
	        $this->_parsed_keys[$data_src.'/'.$key] = $value;
	        $data = $value;
        }
        else
        {
			//if strict - update only
	        if($strict && !isset($this->_data[$key]))
	        {
	        	return $this;
	        }
        	if('_data' === $data_src && !$this->data_has_changed)
	        {
	        	$this->data_has_changed = (!isset($this->_data[$key]) || $this->{$data_src}[$key] != $value);
	        }
            $this->{$data_src}[$key] = $value;
        }

        return $this;
    }

    /**
     * Set data for the given source. More simple (and performance wise) version
     * of {@link _setData()}
     *
     * @param string $key
     * @param mixed $value
     * @param boolean $strict
     * @param string $data_src
     * @return e_model
     */
	protected function _setDataSimple($key, $value = null, $strict = false, $data_src = '_data')
    {
    	$key = $key.'';//smart toString
    	if(!$strict)
    	{
			//data has changed
	        if('_data' === $data_src && !$this->data_has_changed)
	        {
	        	$this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value);
	        }
	        $this->{$data_src}[$key] = $value;
    		return $this;
    	}

    	if($this->isData($key))
    	{
			if('_data' === $data_src && !$this->data_has_changed)
	        {
	        	$this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value);
	        }
	        $this->{$data_src}[$key] = $value;
    	}

    	return $this;
    }

    /**
     * Add data to the object.
     * Retains existing data in the object.
     *
     * If $override is false, only new (non-existent) data will be added
     *
     * @param string|array $key
     * @param mixed $value
     * @param boolean $override allow override of existing data
     * @param string $data_src data source
     * @return e_model
     */
    protected function _addData($key, $value = null, $override = true, $data_src = '_data')
    {
    	if(is_array($key))
    	{
			foreach($key as $k => $v)
			{
			    $this->_addData($k, $v, $override, $data_src);
			}
			return $this;
    	}

		if($override || !$this->_isData($key, $data_src))
       	{
       		if(is_array($value))
       		{
				if(is_array($key))
				{
					foreach($key as $k => $v)
					{
					    $this->_addData($key.'/'.$k, $v, $override, $data_src);
					}
				}
				return $this;
       		}
       		$this->_setData($key, $value, false, $data_src);
       	}
        return $this;
    }

    /**
     * Unset data from the object from the given source.
     * $key can be a string only. Array will be ignored.
     * '/' inside the key will be treated as array path
     * if $key is null entire object will be reset
     *
     * @param string|null $key
     * @param string $data_src data source
     * @return e_model
     */
    protected function _unsetData($key = null, $data_src = '_data')
    {
        if (null === $key)
        {
        	if('_data' === $data_src && !empty($this->_data))
        	{
        		$this->data_has_changed = true;
        	}
        	$this->{$data_src} = array();
            return $this;
        }

        $key = trim($key, '/');
        if(strpos($key,'/'))
        {
        	$keyArr = explode('/', $key);
        	$data = &$this->{$data_src};

        	$unskey = array_pop($keyArr);
            for ($i = 0, $l = count($keyArr); $i < $l; $i++)
            {
	            $k = $keyArr[$i];
	            if (!isset($data[$k]))
	            {
	                return $this; //not found
	            }
	            $data = &$data[$k];
	        }
	        if(is_array($data))
	        {
				if('_data' === $data_src && isset($data[$unskey]))
	        	{
	        		$this->data_has_changed = true;
	        	}
	        	unset($data[$unskey], $this->_parsed_keys[$data_src.'/'.$key]);
	        }
        }
        else
        {
       		if('_data' === $data_src && isset($this->{$data_src}[$key]))
        	{
        		$this->data_has_changed = true;
        	}
            unset($this->{$data_src}[$key]);
        }
        return $this;
    }

	/**
     * Unset single field from the object from the given source. Key is not parsed
     *
     * @param string $key
     * @param string $data_src data source
     * @return e_model
     */
    protected function _unsetDataSimple($key, $data_src = '_data')
    {
		if('_data' === $data_src && isset($this->{$data_src}[$key]))
       	{
       		$this->data_has_changed = true;
       	}
    	unset($this->{$data_src}[$key]);
    	return $this;
    }

    /**
     * If $key is empty, checks whether there's any data in the object
     * Otherwise checks if the specified key is empty/set.
     *
     * @param string $key
     * @param string $data_src data source
     * @return boolean
     */
    protected function _hasData($key = '', $data_src = '_data')
    {
        if (empty($key))
        {
            return !empty($this->$data_src);
        }
        $value = $this->_getData($key, null, null, $data_src);
        return !empty($value);
    }

    /**
     * Checks if the specified key is set
     *
     * @param string $key
     * @param string $data_src data source
     * @return boolean
     */
    protected function _isData($key, $data_src = '_data')
    {
        return (null !== $this->_getData($key, null, null, $data_src));
    }

	/**
	 * Add system message of type Information
	 *
	 * @param string $message
	 * @param boolean $session [optional]
	 * @return e_model
	 */
	public function addMessageInfo($message, $session = false)
	{
		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_INFO, $session);
		return $this;
	}

	/**
	 * Add system message of type Success
	 *
	 * @param string $message
	 * @param boolean $session [optional]
	 * @return e_model
	 */
	public function addMessageSuccess($message, $session = false)
	{
		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_SUCCESS, $session);
		return $this;
	}

	/**
	 * Add system message of type Warning
	 *
	 * @param string $message
	 * @param boolean $session [optional]
	 * @return e_model
	 */
	public function addMessageWarning($message, $session = false)
	{
		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_WARNING, $session);
		return $this;
	}

	/**
	 * Add system message of type Error
	 *
	 * @param string $message
	 * @param boolean $session [optional]
	 * @param array $logData [optional] array('TABLE'=>'', 'ERROR'=>'') etc.
	 * @return e_model
	 */
	public function addMessageError($message, $session = false, $logData = array())
	{
		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_ERROR, $session);

		if(!empty($logData))
		{
			e107::getLog()->addArray($logData);
		}
		else
		{
			e107::getLog()->addError($message,false);
		}

		e107::getLog()->save('ADMINUI_04', E_LOG_WARNING);

		return $this;
	}

	/**
	 * Add system message of type Information
	 *
	 * @param string $message
	 * @param boolean $session [optional]
	 * @return e_model
	 */
	public function addMessageDebug($message, $session = false)
	{
		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_DEBUG, $session);
		return $this;
	}

    /**
     * Render System messages (if any)
     *
     * @param boolean $session store messages to session
     * @param boolean $reset reset errors
     * @return string
     */
    public function renderMessages($session = false, $reset = true)
    {
    	return e107::getMessage()->render($this->_message_stack, $session, $reset);
    }

    /**
     * Move model System messages (if any) to the default eMessage stack
     *
     * @param boolean $session store messages to session
     * @return e_model
     */
    public function setMessages($session = false)
    {
    	e107::getMessage()->moveStack($this->_message_stack, 'default', false, $session);
		return $this;
    }

    /**
     * Reset model System messages
     *
     * @param boolean|string $type E_MESSAGE_INFO | E_MESSAGE_SUCCESS | E_MESSAGE_WARNING | E_MESSAGE_WARNING | E_MESSAGE_DEBUG | false (all)
     * @param boolean $session reset also session messages
     * @return e_model
     */
    public function resetMessages($type = false, $session = false)
    {
        e107::getMessage()->reset($type, $this->_message_stack, $session);
		return $this;
    }

	/**
	 * Set model message stack
	 * @param string $stack_name
	 * @return e_model
	 */
    public function setMessageStackName($stack_name)
    {
    	$this->_message_stack = $stack_name;
		return $this;
    }

	/**
	 * Get model message stack name
	 * @return string
	 */
    public function getMessageStackName()
    {
		return $this->_message_stack;
    }

    /**
     * User defined model validation
     * Awaiting for child class implementation
     *
     */
    public function verify()
    {
    }

    /**
     * Model validation
     * @see e_model_admin
     */
    public function validate()
    {
    }

    /**
     * Generic load data from DB
	 * @param mixed $id
     * @param boolean $force
     * @return e_model
     */
	public function load($id = null, $force = false)
	{


		if(!$force && $this->getId())
		{
			return $this;
		}

		if($force)
		{
			$this->setData(array())
				->_clearCacheData();
		}
		if($id) $id = e107::getParser()->toDB($id);
		if(!$id && !$this->getParam('db_query'))
		{
			return $this;
		}

		$cached = $this->_getCacheData();
		if($cached !== false)
		{
			$this->setData($cached);
			return $this;
		}

		$sql = e107::getDb();
		$qry = str_replace('{ID}', $id, $this->getParam('db_query'));
		if($qry)
		{
			$res = $sql->gen($qry, $this->getParam('db_debug') ? true : false);
		}
		else
		{
			if(!is_numeric($id)) $id = "'{$id}'";

			$res = $sql->select(
				$this->getModelTable(),
				$this->getParam('db_fields', '*'),
				$this->getFieldIdName().'='.$id.' '.trim($this->getParam('db_where', '')),
				'default',
				($this->getParam('db_debug') ? true : false)
			);
		}


		if($res)
		{
			$this->setData($sql->fetch());
		}

		if($sql->getLastErrorNumber())
		{
			$this->addMessageDebug('SQL error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText());
			$this->addMessageDebug($sql->getLastQuery());
		}
		else
		{

			$this->_setCacheData();
		}

		return $this;
	}

	/**
	 * Retrieve system cache (if any)
	 * @return array|false
	 */
	protected function _getCacheData()
	{
		if(!$this->isCacheEnabled())
		{
			return false;
		}

		$cached = e107::getCache()->retrieve_sys($this->getCacheString(true), false, $this->_cache_force);
		if(false !== $cached)
		{
			return e107::unserialize($cached);
		}

		return false;
	}

	/**
	 * Set system cache if enabled for the model
	 * @return e_model
	 */
	protected function _setCacheData()
	{
		if(!$this->isCacheEnabled())
		{
			return $this;
		}
		e107::getCache()->set_sys($this->getCacheString(true), $this->toString(false), $this->_cache_force, false);
		return $this;
	}

	/**
	 * Clrear system cache if enabled for the model
	 * @return e_model
	 */
	protected function _clearCacheData()
	{
		if(!$this->isCacheEnabled(false))
		{
			return $this;
		}
		e107::getCache()->clear_sys($this->getCacheString(true), false);
		return $this;
	}

	/**
	 * Clrear system cache (public proxy) if enabled for the model
	 * @return e_model
	 */
	public function clearCache()
	{
		return $this->_clearCacheData();
	}

	/**
	 * Check if cache is enabled for the current model
	 * @param boolean $checkId check if there is model ID
	 * @return boolean
	 */
	public function isCacheEnabled($checkId = true)
	{
		return (null !== $this->getCacheString() && (!$checkId || $this->getId()));
	}

	/**
	 * Get model cache string
	 * @param boolean $replace try to add current model ID (replace destination is {ID})
	 * @return string
	 */
	public function getCacheString($replace = false)
	{
		return ($replace ? str_replace('{ID}', $this->getId(), $this->_cache_string) : $this->_cache_string);
	}

	/**
	 * Set model cache string
	 * @param string $str
	 * @return e_model
	 */
	public function setCacheString($str)
	{
		$this->_cache_string = $str;
		return $this;
	}

    /**
     * Save data to DB
     * Awaiting for child class implementation
     * @see e_model_admin
     */
    public function save()
    {
    }

    /**
     * Delete DB record
     * Awaiting for child class implementation
     * @see e_model_admin
     */
    public function delete($ids, $destroy = true, $session_messages = false)
    {
    }

    /**
     * Create new DB recorrd
     * Awaiting for child class implementation
     * @see e_model_admin
     */
    public function create()
    {
    }

    /**
     * Insert data to DB
     * Awaiting for child class implementation
     * @see e_model_admin
     */
    protected function dbInsert()
    {
    }

    /**
     * Update DB data
     * Awaiting for child class implementation
     * @see e_model_admin
     */
    protected function dbUpdate($force = false, $session_messages = false)
    {
    }

    /**
     * Replace DB record
     * Awaiting for child class implementation
     * @see e_model_admin
     */
    protected function dbReplace()
    {
    }

    /**
     * Delete DB data
     * Awaiting for child class implementation
     * @see e_model_admin
     */
    protected function dbDelete()
    {
    }

	/**
	 * Set parameter array
	 * Core implemented:
	 * - db_query: string db query to be passed to load() ($sql->gen())
	 * - db_query
	 * - db_fields
	 * - db_where
	 * - db_debug
	 * - model_class: e_tree_model class/subclasses - string class name for creating nodes inside default load() method
	 * - clearModelCache: e_tree_model class/subclasses - clear cache per node after successful DB operation
	 * - noCacheStringModify: e_tree_model class/subclasses - do not add additional md5 sum to tree cache string
	 * @param array $params
	 * @return e_model
	 */
	public function setParams(array $params)
	{
		parent::setParams($params);
		return $this;
	}



	/**
	 * Render model data, all 'sc_*' methods will be recongnized
	 * as shortcodes.
	 *
	 * @param string $template
	 * @param boolean $parsesc parse external shortcodes, default is true
	 * @param e_vars $eVars simple parser data
	 * @return string parsed template
	 */
	public function toHTML($template, $parsesc = true, $eVars = null)
	{
		return e107::getParser()->parseTemplate($template, $parsesc, $this, $eVars);
	}

	/**
	 * Export a Model configuration
	 * @return string
	 */
	public function toXML()
	{
		$ret = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
		$ret .= "<e107Export type=\"model\" version=\"1.0\" timestamp=\"".time()."\" >\n";

		$ret .= "\t<data>\n";
		// TODO - handle multi dimensional arrays (already possible - field1/field2?), method toXMLValue($value, $type)
		foreach ($this->getDataFields() as $field => $type)
		{
			$ret .= "\t\t<field name=\"{$field}\" type=\"{$type}\">";
			$ret .= $type == 'str' || $type == 'string' ? "<![CDATA[".$this->getData($field)."]]>" : $this->getData($field);
			$ret .= "</field>\n";
		}
		$ret .= "\t</data>\n";

		$ret .= "</e107Export>";
		return $ret;
	}

	/**
	 * Try to convert string to a number
	 * Shoud fix locale related troubles
	 *
	 * @param string $value
	 * @return integer|float
	 */
	// moved to e_parse
	// public function toNumber($value)
	// {
	// 	$larr = localeconv();
	// 	$search = array(
	// 		$larr['decimal_point'],
	// 		$larr['mon_decimal_point'],
	// 		$larr['thousands_sep'],
	// 		$larr['mon_thousands_sep'],
	// 		$larr['currency_symbol'],
	// 		$larr['int_curr_symbol']
	// 	);
	// 	$replace = array('.', '.', '', '', '', '');

	// 	return str_replace($search, $replace, $value);
	// }

	/**
	 * Convert object data to a string
	 *
	 * @param boolean $AddSlashes
	 * @param string $key optional, if set method will return corresponding value as a string
	 * @return string
	 */
	public function toString($AddSlashes = true, $key = null)
	{
		if (null !== $key)
		{
			$value = $this->getData($key);
			if(is_array($value))
			{
				return e107::serialize($value, $AddSlashes);
			}
			return (string) $value;
		}
		return (string) e107::serialize($this->toArray(), $AddSlashes);
	}

	/**
	 * @return void
	 */
	public function destroy()
	{
		$this->_data = array();
		$this->_params = array();
		$this->_data_fields = array();
		$this->_parsed_keys = array();
		$this->_db_table = $this->_field_id = '';
		$this->data_has_changed = false;
	}

	/**
	 * Disable Magic setter
	 */
	public function __set($key, $value)
	{
	}

	/**
	 * Disable Magic getter
	 */
	public function __get($key)
	{
	}

	/**
	 * Disable
	 */
	public function __isset($key)
	{
	}

	/**
	 * Disable
	 */
	public function __unset($key)
	{
	}
}

/**
 * Base e107 Front Model class interface
 *
 * Some important points:
 * - model data should be always in toDB() format:
 * 		- retrieved direct from DB
 * 		- set & sanitized via setPostedData()->mergePostedData()
 * 		- manually sanitized before passed to model setter (set(), setData(), add(), addData(), etc.) methods
 * - $_data_fields property is important, it tells to sanitize() method how to sanitize posted data
 * - if $_data_fields is missing, sanitize() will call internally e107::getParser()->toDB() on the data
 * - sanitize() is triggered by default on mergePostedData() and mergeData() methods
 * - mergePostedData() and mergeData() methods will filter posted/passed data against (in this order):
 * 		- getValidator()->getValidData() if true is passed as validate parameter (currently disabled, gather feedback)
 * 		- $_data_fields if true is passed as sanitize parameter
 * - toSqlQuery() needs $_data_fields and $_field_id to work proper, $_FIELD_TYPES is optional but recommended (faster SQL queries)
 * - result array from toSqlQuery() call will be filtered against $_data_fields
 * - in almost every case $_FIELD_TYPES shouldn't contain 'escape' and 'todb' - dont't forget you are going to pass already sanitized data (see above)
 * - most probably $_FIELD_TYPES will go in the future, $_data_fields alone could do the job
 * - default db related methods (save(), dbUpdate(), etc.) need $_db_table
 *
 * @package e107
 * @category e107_handlers
 * @version $Id$
 * @author SecretR
 * @copyright Copyright (C) 2008-2010 e107 Inc.
 */
class e_front_model extends e_model
{
    /**
    * Posted data
    * Back-end related
    *
    * @var array
    */
    protected $_posted_data = array();


    /**
     * DB format array - see db::_getTypes() and db::_getFieldValue() (mysql_class.php)
     * for example
     *
     * This can/should be overwritten by extending the class
     *
     * @var array
     */
    protected $_FIELD_TYPES = array();

    /**
     * Validation structure - see {@link e_validator::$_required_rules} for
     * more information about the array format.
     * Used in {@link validate()} method.
     * TODO - check_rules (see e_validator::$_optional_rules)
     * This can/should be overwritten by extending the class.
     *
     * @var array
     */
    protected $_validation_rules = array();

    protected $_optional_rules = array();

	/**
	 * @var integer Last SQL error number
	 */
	protected $_db_errno = 0;

	/**
	 * @var string Last SQL error message
	 */
	protected $_db_errmsg = '';

	/**
	 * @var string Last SQL query
	 */
	protected $_db_qry = '';

    /**
     * Validator object
     *
     * @var e_validator
     */
    protected $_validator = null;

    /**
     * @return array
     */
    public function getValidationRules()
    {
    	return $this->_validation_rules;
    }

	/**
	 * Set object validation rules if $_validation_rules array is empty
	 *
	 * @param array $vrules
	 * @param bool $force
	 * @return e_front_model
	 */
    public function setValidationRules(array $vrules, $force = false)
    {
    	if($force || empty($this->_validation_rules))
    	{
    		$this->_validation_rules = $vrules;
    	}
    	return $this;
    }

    /**
     * @return array
     */
    public function getOptionalRules()
    {
    	return $this->_optional_rules;
    }

    /**
     * @param array $rules
     * @return e_front_model
     */
    public function setOptionalRules(array $rules)
    {
    	$this->_optional_rules = $rules;
    	return $this;
    }

    /**
     * Set object validation rules if $_validation_rules array is empty
     *
     * @param string $field
     * @param array $rule
     * @param boolean $required
     * @return e_front_model
     */
    public function setValidationRule($field, $rule, $required = true)
    {
    	$pname = $required ? '_validation_rules' : '_optional_rules';
    	$rules = &$this->$pname;
    	$rules[$field] = $rule;

    	return $this;
    }

    /**
     * Predefined data fields types, passed to DB handler
     * @return array
     */
    public function getDbTypes()
    {
    	return ($this->_FIELD_TYPES ? $this->_FIELD_TYPES : $this->getDataFields());
    }

    /**
     * Predefined data fields types, passed to DB handler
     *
     * @param array $field_types
     * @return e_front_model
     */
    public function setDbTypes($field_types)
    {
    	$this->_FIELD_TYPES = $field_types;
		return $this;
    }

    /**
     * Auto field type definitions
     * Disabled for now, it should auto-create _data_types
     * @param boolean $force
     * @return boolean
     */
//	public function setFieldTypeDefs($force = false)
//	{
//		if($force || !$this->getFieldTypes())
//		{
//			$ret = e107::getDb()->getFieldDefs($this->getModelTable());
//			if($ret)
//			{
//				foreach ($ret as $k => $v)
//				{
//					if('todb' == $v)
//					{
//						$ret[$k] = 'string';
//					}
//				}
//				$this->setFieldTypes($ret);
//				return true;
//			}
//		}
//		return false;
//	}

    /**
     * Retrieves data from the object ($_posted_data) without
     * key parsing (performance wise, prefered when possible)
     *
     * @see _getDataSimple()
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
	public function getPosted($key, $default = null)
    {
    	return $this->_getDataSimple((string) $key, $default, '_posted_data');
    }

    /**
     * Retrieves data from the object ($_posted_data)
     * If $key is empty, return all object posted data
     * @see _getData()
     * @param string $key
     * @param mixed $default
     * @param integer $index
     * @return mixed
     */
	public function getPostedData($key = '', $default = null, $index = null)
    {
    	return $this->_getData($key, $default, $index, '_posted_data');
    }

    /**
     * Search for requested data from available sources in this order:
     * - posted data
     * - default object data
     * - passed default value
     *
     * Use this method inside forms
     *
     * @param string $key
     * @param string $default
     * @param integer $index
     * @return string
     */
    public function getIfPosted($key, $default = '', $index = null)
    {
    	$d = $this->getDataFields();
		
		if(!empty($d[$key]) && ($d[$key] === 'array' || $d[$key] === 'json'))
		{
			return e107::unserialize($this->getData((string) $key, $default, $index));	
		}   
		
    	$posted = $this->getPostedData((string) $key, null, $index);
		if(null !== $posted)
		{
			// FIXED - double post_toFom() and toDB(post_toForm()) problems
			// setPosted|setPostedData|addPostedData methods are storing RAW data now
			return e107::getParser()->post_toForm($posted);
		}
		return e107::getParser()->toForm($this->getData((string) $key, $default, $index));
    }

    /**
     * Overwrite posted data in the object for a single field. Key is not parsed.
     * Public proxy of {@link _setDataSimple()}
     * Use this method to store data from non-trustable sources (e.g. _POST) - it doesn't overwrite
     * the original object data
     *
     * @param string $key
     * @param mixed $value
     * @param boolean $strict update only
     * @return e_front_model
     */
    public function setPosted($key, $value, $strict = false)
    {
        return $this->_setDataSimple($key, $value, $strict, '_posted_data');
    }

    /**
     * Overwrite posted data in the object. Key is parsed (multidmensional array support).
     * Public proxy of {@link _setData()}
     * Use this method to store data from non-trustable sources (e.g. _POST) - it doesn't overwrite
     * the original object data
     *
     * @param string|array $key
     * @param mixed $value
     * @param boolean $strict update only
     * @return e_front_model
     */
    public function setPostedData($key, $value = null, $strict = false)
    {
        return $this->_setData($key, $value, $strict, '_posted_data');
    }

    /**
     * Add data to the object.
     * Retains existing data in the object.
     * Public proxy of {@link _addData()}
     *
     * If $override is false, data will be updated only (check against existing data)
     *
     * @param string|array $key
     * @param mixed $value
     * @param boolean $override override existing data
     * @return e_front_model
     */
    public function addPostedData($key, $value = null, $override = true)
    {
    	return $this->_addData($key, $value, $override, '_posted_data');
    }

	/**
     * Unset single posted data field from the object.
     * Public proxy of {@link _unsetDataSimple()}
     *
     * @param string $key
     * @return e_front_model
     */
    public function removePosted($key)
    {
    	return $this->_unsetDataSimple($key, '_posted_data');
    }

	/**
     * Unset posted data from the object.
     * $key can be a string only. Array will be ignored.
     * '/' inside the key will be treated as array path
     * if $key is null entire object will be reset
     *
     * Public proxy of {@link _unsetData()}
     *
     * @param string|null $key
     * @return e_front_model
     */
    public function removePostedData($key = null)
    {
    	return $this->_unsetData($key, '_posted_data');
    }

    /**
     * Check if given key exists and non-empty in the posted data array
     * @param string $key
     * @return boolean
     */
    public function hasPosted($key)
    {
    	return $this->_hasData($key, '_posted_data');
    }

	/**
	 * Check if posted data is empty
	 * @return boolean
	 */
    public function hasPostedData()
    {
    	return $this->_hasData('', '_posted_data');
    }

    /**
     * Check if given key exists in the posted data array
     *
     * @param string $key
     * @return boolean
     */
    public function isPosted($key)
    {
    	return (isset($this->_posted_data[$key]));
    }

    /**
     * Check if given key exists in the posted data array ($key us parsed)
     *
     * @param string $key
     * @return boolean
     */
    public function isPostedData($key)
    {
    	return $this->_isData($key, '_posted_data');
    }

    /**
     * Compares posted data vs object data
     *
     * @param string $field
     * @param boolean $strict compare variable type as well
     * @return boolean
     */
    public function dataHasChangedFor($field, $strict = false)
    {
        $newData = $this->getData($field);
        $postedData = $this->getPostedData($field);
        return ($strict ? $newData !== $postedData : $newData != $postedData);
    }

    /**
     * @return boolean
     */
    public function dataHasChanged()
    {
        return $this->data_has_changed;
    }

    /**
     * Merge posted data with the object data
     * Should be used on edit/update/create record (back-end)
     * Retrieved for copy Posted data will be removed (no matter if copy is successfull or not)
     *
     * If $strict is true, only existing object data will be copied (update)
     * If $validate is true, data will be copied only after successful validation
     *
     * @param boolean $strict
     * @param boolean $sanitize sanitize posted data before move it to the object data
     * @param boolean $validate perform validation check
     * @return e_front_model
     */
    public function mergePostedData($strict = true, $sanitize = true, $validate = true)
    {
    	if(!$this->hasPostedData() || ($validate && !$this->validate()))
    	{
    		return $this;
    	}


		$oldData = $this->getData();
//		$this->addMessageDebug("OLDD".print_a($oldData,true));


		$data = $this->getPostedData();

		$valid_data = $validate ? $this->getValidator()->getValidData() : array();

		if($sanitize)
		{
			// search for db_field types
			if($this->getDataFields())
			{
				$data = $this->sanitize($data);
			}
			else //no db field types, use toDB()
			{
				$data = e107::getParser()->toDB($data);
			}
		}

	//	$newData = $this->getPostedData();
		e107::getLog()->addArray($data,$oldData);
	//	$this->addMessageDebug("NEWD".print_a($data,true));

		$tp = e107::getParser();
    	foreach ($data as $field => $dt)
    	{
    		// get values form validated array when possible
    		// we need it because of advanced validation methods e.g. 'compare'
    		// FIX - security issue, toDb required
    		if(isset($valid_data[$field])) $dt = $tp->toDB($valid_data[$field]);

    		$this->setData($field, $dt, $strict)
    			->removePostedData($field);
    	}




    	return $this;
    }

	/**
	 * Merge passed data array with the object data
	 * Should be used on edit/update/create record (back-end)
	 *
	 * If $strict is true, only existing object data will be copied (update)
	 * If $validate is true, data will be copied only after successful validation
	 *
	 * @param array $src_data
	 * @param bool $strict
	 * @param boolean $sanitize
	 * @param boolean $validate perform validation check
	 * @return e_front_model
	 */
    public function mergeData(array $src_data, $strict = true, $sanitize = true, $validate = true)
    {
    	//FIXME
    	if(!$src_data || ($validate && !$this->validate($src_data)))
    	{
    		return $this;
    	}

		/* Wrong?
		// retrieve only valid data
		if($validate)
		{
			$src_data = $this->getValidator()->getValidData();
		}*/

		if($sanitize)
		{
			// search for db_field types
			if($this->getDataFields())
			{
				$src_data = $this->sanitize($src_data);
			}
			else //no db field types, use toDB()
			{
				$src_data = e107::getParser()->toDB($src_data);
			}
		}



		foreach ($src_data as $key => $value)
		{
			$this->setData($key, $value, $strict);
		}



    	return $this;
    }

    /**
     * Validate posted data:
     * 1. validate posted data against object validation rules
     * 2. add validation errors to the object if any
     * 3. return true for valid and false for non-valid data
     *
     * @param array $data optional - data for validation, defaults to posted data
     * @return boolean
     */
    public function validate($data = null)
    {
    	if(!$this->getValidationRules())
    	{
    		return true;
    	}
		if(null === $data)
		{
			$data = $this->getPostedData();
		}

		// New param to control validate process - useful when part of the data is going to be updated
		// Use it with cautious!!!
		$availableOnly = false;
		if($this->getParam('validateAvailable'))
		{
			$availableOnly = true;
			$this->setParam('validateAvailable', null); // reset it
		}

		return $this->getValidator()->validate($data, $availableOnly);
    }

    /**
     * User defined model validation
     * Awaiting for child class implementation
     *
     */
    public function verify()
    {
    }

	/**
	 * @return e_validator
	 */
	public function getValidator()
	{
		if(null === $this->_validator)
		{
			$this->_validator = e107::getObject('e_validator');
			$this->_validator->setRules($this->getValidationRules())
				->setOptionalRules($this->getOptionalRules())
				->setMessageStack($this->_message_stack.'_validator');
			//TODO - optional check rules
		}
		return $this->_validator;
	}

	/**
	 * Add custom validation message.
	 * $field_type and $error_code will be inserted via $tp->lanVars()
	 * in the $message string
	 * Example:
	 * <code>
	 * $model->addValidationError('Custom error message [#%d] for %s', 'My Field', 1000);
	 * //produces 'Custom error message [#1000] for My Field'
	 * </code>
	 *
	 * @param string $message
	 * @param string $field_title [optional]
	 * @param integer $error_code [optional]
	 * @return object
	 */
	public function addValidationError($message, $field_title = '', $error_code = 0)
	{
		$this->getValidator()->addValidateMessage($field_title, $error_code, $message)->setIsValidData(false);
		return $this;
	}

    /**
     * Render validation errors (if any)
     *
     * @param boolean $session store messages to session
     * @param boolean $reset reset errors
     * @return string
     */
    public function renderValidationErrors($session = false, $reset = true)
    {
		return $this->getValidator()->renderValidateMessages($session, $reset);
    }

    /**
     * Render System messages (if any)
     *
     * @param boolean $validation render validation messages as well
     * @param boolean $session store messages to session
     * @param boolean $reset reset errors
     * @return string
     */
    public function renderMessages($validation = true, $session = false, $reset = true)
    {
    	if($validation)
		{
			e107::getMessage()->moveStack($this->_message_stack.'_validator', $this->_message_stack, false, $session);
		}
		return parent::renderMessages($session, $reset);
    }

    /**
     * Move model System messages (if any) to the default eMessage stack
     *
     * @param boolean $session store messages to session
     * @param boolean $validation move validation messages as well
     * @return e_front_model
     */
    public function setMessages($session = false, $validation = true)
    {
    	if($validation)
		{
			e107::getMessage()->moveStack($this->_message_stack.'_validator', 'default', false, $session);
		}
    	parent::setMessages($session);
		return $this;
    }

    /**
     * Reset model System messages
     *
     * @param boolean|string $type E_MESSAGE_INFO | E_MESSAGE_SUCCESS | E_MESSAGE_WARNING | E_MESSAGE_WARNING | E_MESSAGE_DEBUG | false (all)
     * @param boolean $session reset session messages
     * @param boolean $validation reset validation messages as well
     * @return e_front_model
     */
    public function resetMessages($type = false, $session = false, $validation = false)
    {
        if($validation)
		{
			e107::getMessage()->reset($type, $this->_message_stack.'_validator', $session);
		}
    	parent::resetMessages($type, $session);
		return $this;
    }

    /**
     * @return boolean
     */
    public function hasValidationError()
    {
    	return $this->getValidator()->isValid();
    }

    /**
     * @return boolean
     */
    public function hasSqlError()
    {
    	return !empty($this->_db_errno);
    }

    /**
     * @return integer last mysql error number
     */
    public function getSqlErrorNumber()
    {
    	return $this->_db_errno;
    }

    /**
     * @return string last mysql error message
     */
    public function getSqlError()
    {
    	return $this->_db_errmsg;
    }

    /**
     * @return string last mysql error message
     */
    public function getSqlQuery()
    {
    	return $this->_db_qry;
    }

    /**
     * @return boolean
     */
    public function hasError()
    {
    	return ($this->hasValidationError() || $this->hasSqlError());
    }

    /**
     * Generic load data from DB
     * @param boolean $force
     * @return e_front_model
     */
	public function load($id=null, $force = false)
	{
		parent::load($id, $force);

		$sql = e107::getDb();
		$this->_db_errno = $sql->getLastErrorNumber();
		$this->_db_errmsg = $sql->getLastErrorText();
		$this->_db_qry = $sql->getLastQuery();

		if($this->_db_errno)
		{
			$data = array(
				'TABLE'     => $this->getModelTable(),
				'error_no'  => $this->_db_errno,
				'error_msg' => $this->_db_errmsg,
				'qry'       => $this->_db_qry,
				'url'       => e_REQUEST_URI,
			);


			$this->addMessageError('SQL Select Error', false, $data); //TODO - Lan
			// already done by the parent
			//$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText());
		}


		return $this;
	}


	/**
	 * Build query array to be used with db methods (db_Update, db_Insert, db_Replace)
	 *
	 * @param string $force [optional] force action - possible values are create|update|replace
	 * @return array db query
	 */
	public function toSqlQuery($force = '')
	{
		$qry = array();

		if($force)
		{
			$action = $force;
		}
		else
		{
			$action = $this->getId() ? 'update' : 'create';
		}

		$qry['_FIELD_TYPES'] = $this->_FIELD_TYPES; //DB field types are optional

		// support for tables with no auto-increment PK
		$id = $this->getId();
		$qry['data'][$this->getFieldIdName()] = $id;

		//XXX This check is done in _setModel() of admin-ui.  NULL below will break MySQL strict.
		// Allow admin config to specify the best data type. 
		/*
		if($action == 'create' && !$id) $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'NULL';
		elseif(is_numeric($id)) $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'integer';
		else $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'string';
		*/

		foreach ($this->_data_fields as $key => $type)
		{

			if(!isset($qry['_FIELD_TYPES'][$key]))
			{
				$qry['_FIELD_TYPES'][$key] = $type; //_FIELD_TYPES much more optional now...
			}

			if($qry['_FIELD_TYPES'][$key] == 'set') //new 'set' type, could be moved in mysql handler now
			{
				$qry['_FIELD_TYPES'][$key] = 'str';
				if(is_array($this->getData($key)))	$this->setData($key, implode(',', $this->getData($key)));
			}
			$qry['data'][$key] = $this->getData($key);

		}

		switch($action)
		{
			case 'create':
				//$qry['data'][$this->getFieldIdName()] = NULL;
			break;
			case 'replace':
				$qry['_REPLACE'] = true;
			break;

			case 'update':
				unset($qry['data'][$this->getFieldIdName()]);
				if(is_numeric($id)) $id = intval($id);
				else $id = "'".e107::getParser()->toDB($id)."'";
				$qry['WHERE'] = $this->getFieldIdName().'='.$id;
			break;
		}

		if(E107_DEBUG_LEVEL == E107_DBG_SQLQUERIES)
		{
			$this->addMessageDebug('SQL Qry: '.print_a($qry,true), null);
		}
		return $qry;
	}

	/**
	 * @param $key
	 * @return false|mixed
	 */
	public function isValidFieldKey($key)
	{
		if(isset($this->_data_fields[$key]))
		{
			return $this->_data_fields[$key];
		}

		// Check key against a multi/dimensional/field name.
		// FIXME make this more accurate - commonly used field names could conflict. @see e_front_modelTest::testSanitize()
		foreach($this->_data_fields as $k=>$var)
		{
			if(strpos($k,'/') !== false && strpos($k, $key) !== false)
			{
				return $this->_data_fields[$k];
			}
		}

		return false;
	}



	/**
	 * Sanitize value based on its db field type ($_data_fields),
	 * method will return null only if db field rule is not found.
	 * If $value is null, it'll be retrieved from object posted data
	 * If $key is an array, $value is omitted.
	 *
	 * NOTE: If $key is not found in object's _data_fields array, it is removed
	 *
	 * @param mixed $key string key name or array data to be sanitized
	 * @param mixed $value
	 * @return mixed sanitized $value or null on failure
	 */
	public function sanitize($key, $value = null)
	{
		if(is_array($key))
		{
			$expectedFields = e107::getParser()->fromFlatArray($this->_data_fields);
			return $this->sanitizeRecursive($key, $expectedFields);
		}

		if(!isset($this->_data_fields[$key]))
		{
			return null;
		}
		$type =  $this->_data_fields[$key];
		if(null === $value)
		{
			$value = $this->getPostedData($key);
		}

		return $this->sanitizeValue($type, $value, $key);

	}

	/**
	 * @param array $data
	 * @param array $fields
	 * @return array
	 */
	private function sanitizeRecursive($data, $fields)
	{
		foreach ($data as $fieldKey => $value)
		{
			if (!array_key_exists($fieldKey, $fields))
			{
				unset($data[$fieldKey]);
				continue;
			}
			$expectedType = $fields[$fieldKey];
			if (is_array($value) && is_array($expectedType))
			{
				$data[$fieldKey] = $this->sanitizeRecursive($value, $fields[$fieldKey]);
			}
			else
			{
				$data[$fieldKey] = $this->sanitizeValue($expectedType, $value, $fieldKey);
			}
		}
		return $data;
	}

	/**
	 * @return void
	 */
	public function destroy()
	{
		parent::destroy();
		$this->_validator = null;
		$this->_validation_rules = array();
		$this->_db_errno = null;
		$this->_posted_data = array();
		$this->data_has_changed = array();
		$this->_FIELD_TYPES = array();
	}


    /**
     * Update DB data
     *
     * @param boolean $force force query even if $data_has_changed is false
     * @param boolean $session_messages to use or not session to store system messages
     */
    protected function dbUpdate($force = false, $session_messages = false)
    {
        $this->_db_errno = 0;
		$this->_db_errmsg = '';
		$this->_db_qry = '';

	//	 $this->getData();
	//	 $this->getPostedData();


		if($this->hasError()) return false;

		if(!$this->data_has_changed && $force === false)
		{
			$this->addMessageInfo(LAN_NO_CHANGE);
			return 0;
		}

		$sql = e107::getDb();
		$qry = $this->toSqlQuery('update');
		$table = $this->getModelTable();

		$res = $sql->update($table, $qry, $this->getParam('db_debug', false));
        $this->_db_qry = $sql->getLastQuery();
		if(!$res)
		{
			$this->_db_errno = $sql->getLastErrorNumber();
			$this->_db_errmsg = $sql->getLastErrorText();

			if($this->_db_errno)
			{
				$data = array(
					'TABLE'     => $table,
					'error_no' => $this->_db_errno,
					'error_msg' => $this->_db_errmsg,
					'qry'       => $this->_db_qry,
					'url'       => e_REQUEST_URI,
				);

				$this->addMessageError('SQL Update Error', $session_messages, $data); //TODO - Lan
				$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText());
				return false;
			}

			if($force === false)
			{
				$this->addMessageInfo(LAN_NO_CHANGE);
			}
			else
			{
				$this->addMessageDebug(LAN_NO_CHANGE);
			}


			return 0;
		}
		$this->clearCache()->addMessageSuccess(LAN_UPDATED. " #".$this->getId());

		e107::getLog()->addSuccess('TABLE: '.$table, false);
		e107::getLog()->addSuccess('WHERE: '.$qry['WHERE'], false);
		e107::getLog()->save('ADMINUI_02');


		return $res;
    }

    /**
     * Save data to DB
     *
     * @param boolen $from_post
     * @return boolean|integer
     */
    public function save($from_post = true, $force = false, $session_messages = false)
    {
    	if(!$this->getFieldIdName())
		{
			return false;
		}

		if($from_post)
		{
			//no strict copy, validate & sanitize
			$this->mergePostedData(false, true, true);
		}

		if($this->getId())
		{
			return $this->dbUpdate($force, $session_messages);
		}

		return false;
    }

    /**
     * Update record
     * @see save()
     * @param boolen $from_post
     * @return boolean|integer
     *//*
    public function update($from_post = true, $force = false, $session_messages = false)
    {
    	if(!$this->getFieldIdName())
		{
			return false;
		}

		if($from_post)
		{
			//no strict copy, validate & sanitize
			$this->mergePostedData(false, true, true);
		}

		return $this->dbUpdate($force, $session_messages);
    }*/

	/**
	 * Exactly what it says - your debug helper
	 * @param bool $return
	 * @param boolean $undo
	 * @return void
	 */
	public function saveDebug($return = false, $undo = true)
	{
		$ret = array();

		$ret['validation_rules'] = $this->getValidationRules();
		$ret['optional_validation_rules'] = $this->getOptionalRules();
		$ret['model_base_ismodfied'] = $this->isModified();
		$ret['model_base_data'] = $this->getData();
		$ret['posted_data'] = $this->getPostedData();

		$this->mergePostedData(false, true, true);

		$ret['model_modified_data'] = $this->getData();
		$ret['model_modified_ismodfied'] = $this->isModified();
		$ret['validator_valid_data'] = $this->getValidator()->getValidData();

		// undo
		if($undo)
		{
			$this->setData($ret['model_base_data'])
				->isModified($ret['model_base_ismodfied']);
			$this->setPostedData($ret['posted_data']);
		}
		if($return) return $ret;

		var_dump($ret);
	}

	/**
	 * @param $type
	 * @param $value
	 * @param $key
	 * @return array|bool|float|int|mixed|string|null
	 */
	private function sanitizeValue($type, $value, $key)
	{

		$ret = null; // default
		$tp = e107::getParser();

		switch($type)
		{
			case 'int':
			case 'integer':
				//return intval($this->toNumber($value));
				$ret = (int) $tp->toNumber($value);
				break;

			case 'safestr':
				$ret = $tp->filter($value);
				break;

			case 'str':
			case 'string':
			case 'array':
				$type = $this->getFieldInputType($key);
				$ret = $tp->toDB($value, false, false, 'model', array('type' => $type, 'field' => $key));
				break;

			case 'json':
				if(!empty($value))
				{
					$ret = e107::serialize($value, 'json');
				}
				break;

			case 'code':
				$ret = $tp->toDB($value, false, false, 'pReFs');
				break;

			case 'float':
				// return $this->toNumber($value);
				$ret = $tp->toNumber($value);
				break;

			case 'bool':
			case 'boolean':
				$ret = ($value ? true : false);
				break;

			case 'model':
				$ret = $value->mergePostedData(false, true, true);
				break;

			case 'null':
				$ret = ($value ? $tp->toDB($value) : null);
				break;
		}

		return $ret;
	}
}

//FIXME - move e_model_admin to e_model_admin.php

/**
 * Base e107 Admin Model class
 *
 * @package e107
 * @category e107_handlers
 * @version $Id$
 * @author SecretR
 * @copyright Copyright (C) 2008-2010 e107 Inc.
 */
class e_admin_model extends e_front_model
{
    /**
     * Save data to DB
     *
     * @param boolen $from_post
     */
    public function save($from_post = true, $force = false, $session_messages = false)
    {
    	if(!$this->getFieldIdName())
		{
			return false;
		}

		if($from_post)
		{
			//no strict copy, validate & sanitize
			$this->mergePostedData(false, true, true);
		}

		if($this->getId() && $this->getPostedData('etrigger_submit') !='create') // Additional Check to allow primary ID to be manually set when auto-increment PID is not used. @see userclass2.php
		{
			return $this->dbUpdate($force, $session_messages);
		}

		return $this->dbInsert($session_messages);
    }

    /**
     * Insert record
     *
     * @param boolen $from_post
	 * @param boolean $session_messages
	 * @return integer inserted ID or false on error
     */
    public function insert($from_post = true, $session_messages = false)
    {
		if($from_post)
		{
			//no strict copy, validate & sanitize
			$this->mergePostedData(false, true, true);
		}

		return $this->dbInsert($session_messages);
    }

	/**
	 * @param $ids
	 * @param $destroy
	 * @param $session_messages
	 * @return false|int
	 */
	public function delete($ids, $destroy = true, $session_messages = false)
	{
		$ret = $this->dbDelete();
		if($ret)
		{
			if($destroy)
			{
				$this->setMessages($session_messages)->destroy();
			}
		}
		return $ret;
	}

    /**
     * Insert data to DB
     * @param boolean $session_messages to use or not session to store system messages
     * @return integer
     */
    protected function dbInsert($session_messages = false)
    {
        $this->_db_errno = 0;
        $this->_db_errmsg = '';
		$this->_db_qry = '';
		if($this->hasError()/* || (!$this->data_has_changed && !$force)*/) // not appropriate here!
		{
			return false;
		}
		$sql = e107::getDb();
		$sqlQry = $this->toSqlQuery('create');
		$table = $this->getModelTable();

		$res = $sql->insert($table, $sqlQry, $this->getParam('db_debug', false));
        $this->_db_qry = $sql->getLastQuery();
		if(!$res)
		{
			$this->_db_errno = $sql->getLastErrorNumber();
			$this->_db_errmsg = $sql->getLastErrorText();

			$logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'QRY'=>print_r($sqlQry,true)) : false;

			$this->addMessageError('SQL Insert Error', $session_messages, $logData); //TODO - Lan
			$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$this->_db_errmsg);
			$this->addMessageDebug('SQL QRY Error '.print_a($sqlQry,true));

			return false;
		}

	    e107::getLog()->addSuccess('TABLE: '.$table, false);
		e107::getLog()->save('ADMINUI_01');
	//	e107::getAdminLog()->clear()->addSuccess($table,false)->addArray($sqlQry)->save('ADMINUI_01');

		// Set the reutrned ID
		$this->setId($res);
		$this->clearCache()->addMessageSuccess(LAN_CREATED. " #".$this->getId());

		return $res;
    }

    /**
     * Replace data in DB
     *
     * @param boolean $force force query even if $data_has_changed is false
     * @param boolean $session_messages to use or not session to store system messages
     */
    protected function dbReplace($force = false, $session_messages = false)
    {
    	$this->_db_errno = 0;
    	$this->_db_errmsg = '';
		$this->_db_qry = '';

		if($this->hasError()) return false;
		if(!$this->data_has_changed && !$force)
		{
			return 0;
		}
		$sql = e107::getDb();
		$table = $this->getModelTable();
		$res = $sql->insert($table, $this->toSqlQuery('replace'));
        $this->_db_qry = $sql->getLastQuery();
		if(!$res)
		{
			$this->_db_errno = $sql->getLastErrorNumber();
			$this->_db_errmsg = $sql->getLastErrorText();

			if($this->_db_errno)
			{
				$logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'QRY'=> print_r($this->_db_qry,true)) : false;

				$this->addMessageError('SQL Replace Error', $session_messages, $logData); //TODO - Lan
				$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText());
			}
		}
		else
		{
			$this->clearCache();
		}
		return $res;
    }

	/**
	 * Delete DB data
	 *
	 * @param boolean $session_messages to use or not session to store system messages
	 * @return false|int
	 */
    protected function dbDelete($session_messages = false)
    {
    	$this->_db_errno = 0;
		$this->_db_errmsg = '';
		$this->_db_qry = '';

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

		if(!$this->getId())
		{
			$this->addMessageError('Record not found', $session_messages); //TODO - Lan
			return 0;
		}
		$sql = e107::getDb();
		$id = $this->getId();
		if(is_numeric($id)) $id = intval($id);
		else  $id = "'".e107::getParser()->toDB($id)."'";
		$table  = $this->getModelTable();
		$where = $this->getFieldIdName().'='.$id;
		$res = $sql->delete($table, $where);
        $this->_db_qry = $sql->getLastQuery();

		if(!$res)
		{
			$this->_db_errno = $sql->getLastErrorNumber();
			$this->_db_errmsg = $sql->getLastErrorText();

			if($this->_db_errno)
			{
				$logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'WHERE'=>$where) : false;

				$this->addMessageError('SQL Delete Error', $session_messages, $logData); //TODO - Lan
				$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText());
			}
		}
    	else
		{
			if($table != 'admin_log')
			{
				$logData = array('TABLE'=>$table, 'WHERE'=>$where);
				e107::getLog()->addSuccess($table,false);
				e107::getLog()->addArray($logData)->save('ADMINUI_03');
			}

			$this->clearCache();
		}
		return $res;
    }
}

/**
 * Model collection handler
 */
class e_tree_model extends e_front_model
{
	/**
	 * Current model DB table, used in all db calls
	 * This can/should be overwritten by extending the class
	 *
	 * @var string
	 */
	protected $_db_table;

	/**
	 * All records (no limit) cache
	 *
	 * @var string
	 */
	protected $_total = false;

	/**
	 * Constructor
	 *
	 */
	function __construct($tree_data = array())
	{
		if($tree_data)
		{
			$this->setTree($tree_data);
		}
	}

	/**
	 * @return bool|string
	 */
	public function getTotal()
	{
		return $this->_total;
	}

	/**
	 * @param $num
	 * @return $this
	 */
	public function setTotal($num)
	{
		$this->_total = $num;
		return $this;
	}

	/**
	 * Set table name
	 * @param string $table
	 * @return e_tree_model
	 */
	public function setModelTable($table)
	{
		$this->_db_table = $table;
		return $this;
	}

	/**
	 * Get table name
	 * @return string
	 */
	public function getModelTable()
	{
		return $this->_db_table;
	}

	/**
	 * Get array of models
	 * @return array
	 */
	function getTree()
	{
		return $this->get('__tree', array());
	}

	/**
	 * Set array of models
	 * @return e_tree_model
	 */
	function setTree($tree_data, $force = false)
	{
		if($force || !$this->isTree())
		{
			$this->set('__tree', $tree_data);
		}

		return $this;
	}

	/**
	 * Unset all current data
	 * @return e_tree_model
	 */
	function unsetTree()
	{
		$this->remove('__tree');
		return $this;
	}

	/**
	 * @param $checkId
	 * @return bool
	 */
	public function isCacheEnabled($checkId = true)
	{
		return (null !== $this->getCacheString());
	}

	/**
	 * @param $replace
	 * @return string|null
	 */
	public function getCacheString($replace = false)
	{
		return $this->_cache_string;
	}

	/**
	 * @param $str
	 * @return e_tree_model
	 */
	public function setCacheString($str = null)
	{
		if(isset($str))
			return parent::setCacheString($str);

		if($this->isCacheEnabled() && !$this->getParam('noCacheStringModify'))
		{
			$str = !$this->getParam('db_query')
				?
					$this->getModelTable()
					.$this->getParam('nocount')
					.$this->getParam('db_where')
					.$this->getParam('db_order')
					.$this->getParam('db_limit')
				:
					$this->getParam('db_query');

			return $this->setCacheString($this->getCacheString().'_'.md5($str));
		}

		return parent::setCacheString($str);
	}

	/**
	 * @return $this|e_tree_model
	 */
	protected function _setCacheData()
	{
		if(!$this->isCacheEnabled())
		{
			return $this;
		}

		e107::getCache()->set_sys(
			$this->getCacheString(true),
			$this->toString(false, null, $this->getParam('nocount') ? false : true),
			$this->_cache_force,
			false
		);
		return $this;
	}

	/**
	 * @param $array
	 * @return void
	 */
	protected function _loadFromArray($array)
	{
		if(isset($array['total']))
		{
			$this->setTotal((integer) $array['total']);
			unset($array['total']);
		}
		$class_name = $this->getParam('model_class', 'e_model');
		$tree = array();
		foreach ($array as $id => $data)
		{
			$tree[$id] = new $class_name($data);
			$this->_onLoad($tree[$id]);
		}

		$this->setTree($tree, true);
	}

	/**
	 * Additional on load logic to be set from subclasses
	 *
	 * @param e_model $node
	 * @return e_tree_model
	 */
	protected function _onLoad($node)
	{
		return $this;
	}

	/**
	 * Default load method
	 *
	 * @return e_tree_model
	 */
	public function loadBatch($force = false)
	{
		if ($force)
		{
			$this->unsetTree()
				->_clearCacheData();

			$this->_total = false;
		}

		// XXX What would break if changed to the most proper isTree()?
		elseif($this->isTree()) //!$this->isEmpty()
		{
			return $this;
		}

		$this->setCacheString();
		$cached = $this->_getCacheData();
		if($cached !== false)
		{
			$this->_loadFromArray($cached);
			return $this;
		}

		// auto-load all
		if(!$this->getParam('db_query') && $this->getModelTable())
		{
			$this->setParam('db_query', 'SELECT'.(!$this->getParam('nocount') ? ' SQL_CALC_FOUND_ROWS' : '')
				.($this->getParam('db_cols') ? ' '.$this->getParam('db_cols') : ' *').' FROM #'.$this->getModelTable()
				.($this->getParam('db_joins') ? ' '.$this->getParam('db_joins') : '')
				.($this->getParam('db_where') ? ' WHERE '.$this->getParam('db_where') : '')
				.($this->getParam('db_order') ? ' ORDER BY '.$this->getParam('db_order') : '')
				.($this->getParam('db_limit') ? ' LIMIT '.$this->getParam('db_limit') : '')
			);
		}

		$class_name = $this->getParam('model_class', 'e_model');
		if($this->getParam('db_query') && $class_name && class_exists($class_name))
		{
			$sql = e107::getDb($this->getParam('model_class', 'e_model'));
			$this->_total = $sql->total_results = false;

			if($rows = $this->getRows($sql))
			{
				foreach($rows as $tmp)
				{
					$tmp = new $class_name($tmp);
					if($this->getParam('model_message_stack'))
					{
						$tmp->setMessageStackName($this->getParam('model_message_stack'));
					}
					$this->_onLoad($tmp)->setNode($tmp->get($this->getFieldIdName()), $tmp);
				}
				unset($tmp);

				$this->countResults($sql);
			}

			if($sql->getLastErrorNumber())
			{

				$data = array(
					'TABLE'     => $this->getModelTable(),
					'error_no' => $sql->getLastErrorNumber(),
					'error_msg' => $sql->getLastErrorText(),
					'qry'       => $sql->getLastQuery(),
					'url'       => e_REQUEST_URI,
				);


				$this->addMessageError('Application Error - DB query failed.', false, $data) // TODO LAN
					->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText())
					->addMessageDebug($sql->getLastQuery());
			}
			else
			{
				$this->_setCacheData();
			}

		}
		return $this;
	}

	/**
	 * @param $sql
	 * @return array|false
	 */
	protected function getRows($sql)
	{
		// Tree (Parent-Child Relationship)
		if ($this->getParam('sort_parent') && $this->getParam('sort_field'))
		{
			return $this->getRowsTree($sql);
		}
		// Flat List
		return $this->getRowsList($sql);
	}

	/**
	 * @param $sql
	 * @return false
	 */
	protected function getRowsList($sql)
	{
		$success = $sql->gen($this->getParam('db_query'), $this->getParam('db_debug') ? true : false);
		if (!$success) return false;

		return $sql->rows();
	}

	/**
	 * @param $sql
	 * @return array|false
	 */
	protected function getRowsTree($sql)
	{
		// Workaround: Parse and modify db_query param for simulated pagination
		$this->prepareSimulatedPagination();
		// Workaround: Parse and modify db_query param for simulated custom ordering
		$this->prepareSimulatedCustomOrdering();

		$success = $sql->gen($this->getParam('db_query'), $this->getParam('db_debug') ? true : false);
		if (!$success) return false;

		$rows_tree = self::arrayToTree($sql->rows(),
			$this->getParam('primary_field'),
			$this->getParam('sort_parent'));
		$rows = self::flattenTree($rows_tree,
			$this->getParam('sort_field'),
			$this->getParam('sort_order'));

		// Simulated pagination
		$rows = array_splice($rows,
			(int) $this->getParam('db_limit_offset'),
			($this->getParam('db_limit_count') ? $this->getParam('db_limit_count') : count($rows))
		);

		return $rows;
	}

	/**
	 * Converts a relational array with a parent field and a sort order field to a tree
	 * @param array $rows Relational array with a parent field and a sort order field
	 * @param string $primary_field The field name of the primary key (matches children to parents)
	 * @param string $sort_parent The field name whose value is the parent ID
	 * @return array Multidimensional array with child nodes under the "_children" key
	 */
	protected static function arrayToTree($rows, $primary_field, $sort_parent)
	{
		$nodes = array();
		$root = array($primary_field => 0);
		$nodes[] = &$root;

		while(!empty($nodes))
		{
			self::moveRowsToTreeNodes($nodes, $rows, $primary_field, $sort_parent);
		}

		return array(0 => $root);
	}

	/**
	 * Put rows with parent matching the ID of the first node into the next node's children
	 * @param array &$nodes Current queue of nodes, the first of which may have children added to it
	 * @param array &rows The remaining rows that have yet to be converted into children of nodes
	 * @param string $primary_field The field name of the primary key (matches children to parents)
	 * @param string $sort_parent The field name whose value is the parent ID
	 * @returns null
	 */
	protected static function moveRowsToTreeNodes(&$nodes, &$rows, $primary_field, $sort_parent)
	{
		$node = &$nodes[0];
		array_shift($nodes);
		$nodeID = (int) $node[$primary_field];
		foreach($rows as $key => $row)
		{
			$rowParentID = (int) $row[$sort_parent];

			// Note: This optimization only works if the SQL query executed was ordered by the sort parent.
			if($rowParentID > $nodeID) break;

			$node['_children'][] = &$row;
			unset($rows[$key]);
			$nodes[] = &$row;
			unset($row);
		}
	}

	/**
	 * Flattens a tree into a depth-first array, sorting each node by a field's values
	 * @param array $tree Tree with child nodes under the "_children" key
	 * @param mixed $sort_field The field name (string) or field names (array) whose value
	 *                          is or values are the sort order in the current tree node
	 * @param int $sort_order Desired sorting direction: 1 if ascending, -1 if descending
	 * @param int $depth The depth that this level of recursion is entering
	 * @return array One-dimensional array in depth-first order with depth indicated by the "_depth" key
	 */
	protected static function flattenTree($tree, $sort_field = null, $sort_order = 1, $depth = 0)
	{
		$flat = array();

		foreach($tree as $item)
		{
			$children = isset($item['_children']) ? $item['_children'] : null;
			unset($item['_children']);
			$item['_depth'] = $depth;
			if($depth > 0)
				$flat[] = $item;
			if(is_array($children))
			{
				uasort($children, function($node1, $node2) use ($sort_field, $sort_order)
				{
					return self::multiFieldCmp($node1, $node2, $sort_field, $sort_order);
				});
				$flat = array_merge($flat, self::flattenTree($children, $sort_field, $sort_order, $depth+1));
			}
		}

		return $flat;
	}

	/**
	 * Naturally compares two associative arrays given multiple sort keys and a reverse order flag
	 * @param array $row1 Associative array to compare to $row2
	 * @param array $row2 Associative array to compare to $row1
	 * @param mixed $sort_field Key (string) or keys (array) to compare
	 *                          the values of in both $row1 and $row2
	 * @param int $sort_order -1 to reverse the sorting order or 1 to keep the order as ascending
	 * @return int -1 if $row1 is less than $row2
	 *             0 if $row1 is equal to $row2
	 *             1 if $row1 is greater than $row2
	 */
	protected static function multiFieldCmp($row1, $row2, $sort_field, $sort_order = 1)
	{
		if (!is_array($sort_field))
			$sort_field = [$sort_field];
		$field = array_shift($sort_field);

		$cmp = strnatcmp((string) $row1[$field], (string) $row2[$field]);
		if ($sort_order === -1 || $sort_order === 1) $cmp *= $sort_order;
		if ($cmp === 0 && count($sort_field) >= 1)
			return self::multiFieldCmp($row1, $row2, $sort_field, $sort_order);
		return $cmp;
	}

	/**
	 * Resiliently counts the results from the last SQL query in the given resource
	 *
	 * Sets the count in $this->_total
	 *
	 * @param e_db_pdo $sql SQL resource that executed a query
	 * @return int Number of results from the latest query
	 */
	protected function countResults($sql)
	{
		$total = $sql->foundRows();
		$this->_total = is_int($total) ? $total : false; //requires SQL_CALC_FOUND_ROWS in query - see db handler

		if(false === $this->_total && $this->getModelTable() && !$this->getParam('nocount'))
		{
			//SQL_CALC_FOUND_ROWS not found in the query, do one more query
		//	$this->_total = e107::getDb()->db_Count($this->getModelTable()); // fails with specific listQry

			// Calculates correct total when using filters and search. //XXX Optimize.
			$countQry = preg_replace('/(LIMIT ([\d,\s])*)$/', "", $this->getParam('db_query'));

			$this->_total = e107::getDb()->gen($countQry);

		}
		return $this->_total;
	}

	/**
	 * Workaround: Parse and modify query to prepare for simulation of tree pagination
	 *
	 * This is a hack to maintain compatibility of pagination of tree
	 * models without SQL LIMITs
	 *
	 * Implemented out of necessity under
	 * https://github.com/e107inc/e107/issues/3015
	 *
	 * @returns null
	 */
	protected function prepareSimulatedPagination()
	{
		$db_query = $this->getParam('db_query');
		$db_query = preg_replace_callback("/LIMIT ([\d]+)[ ]*(?:,|OFFSET)?[ ]*([\d]*)/i", function($matches)
		{
			// Count only
			if (empty($matches[2]))
			{
				$this->setParam('db_limit_count', $matches[1]);
			}
			// Offset and count
			else
			{
				$this->setParam('db_limit_offset', $matches[1]);
				$this->setParam('db_limit_count', $matches[2]);
			}

			return "";
		}, $db_query);
		$this->setParam('db_query', $db_query);
	}

	/**
	 * Workaround: Parse and modify query to prepare for simulation of custom ordering
	 *
	 * XXX: Not compliant with all forms of ORDER BY clauses
	 * XXX: Does not support quoted identifiers (`identifier`)
	 * XXX: Does not support mixed sort orders (identifier1 ASC, identifier2 DESC)
	 *
	 * This is a hack to enable custom ordering of tree models when
	 * flattening the tree.
	 *
	 * Implemented out of necessity under
	 * https://github.com/e107inc/e107/issues/3029
	 *
	 * @returns null
	 */
	protected function prepareSimulatedCustomOrdering()
	{
		$db_query = $this->getParam('db_query');
		$db_query = preg_replace_callback('/ORDER BY (?:.+\.)*[\.]*([A-Za-z0-9$_,]+)[ ]*(ASC|DESC)*/i', function($matches)
			{
				if (!empty($matches[1]))
				{
					$current_sort_field = $this->getParam('sort_field');
					if (!empty($current_sort_field))
					{
						$matches[1] = $current_sort_field.",".$matches[1];
					}
					$this->setParam('sort_field', array_map('trim', explode(',', $matches[1])));
				}
				if (!empty($matches[2]))
					$this->setParam('sort_order',
						(0 === strcasecmp($matches[2], 'DESC') ? -1 : 1)
					);

				return "";
			}, $db_query)
			// Optimization goes with e_tree_model::moveRowsToTreeNodes()
			. " ORDER BY " . $this->getParam('sort_parent') . "," . $this->getParam('primary_field');
		$this->setParam('db_query', $db_query);
	}

	/**
	 * Get single model instance from the collection
	 * @param integer $node_id
	 * @return e_model
	 */
	function getNode($node_id)
	{
		return $this->getData('__tree/'.$node_id);
	}

	/**
	 * Add or remove (when $node is null) model to the collection
	 *
	 * @param integer $node_id
	 * @param e_model $node
	 * @return e_tree_model
	 */
	function setNode($node_id, $node)
	{
		if(null === $node)
		{
			$this->removeData('__tree/'.$node_id);
			return $this;
		}

		$this->setData('__tree/'.$node_id, $node);
		return $this;
	}

	/**
	 * Check if model with passed id exists in the collection
	 *
	 * @param integer $node_id
	 * @return boolean
	 */
	public function isNode($node_id)
	{
		return $this->isData('__tree/'.$node_id);
	}

	/**
	 * Check if model with passed id exists in the collection and is not empty
	 *
	 * @param integer $node_id
	 * @return boolean
	 */
	public function hasNode($node_id)
	{
		return $this->hasData('__tree/'.$node_id);
	}

	/**
	 * Check if collection is empty
	 *
	 * @return boolean
	 */
	function isEmpty()
	{
		return (!$this->has('__tree'));
	}

	/**
	 * Check if collection is loaded (not null)
	 *
	 * @return boolean
	 */
	function isTree()
	{
		return $this->is('__tree');
	}

	/**
	 * Same as isEmpty(), but with opposite boolean logic
	 *
	 * @return boolean
	 */
	function hasTree()
	{
		return $this->has('__tree');
	}

	/**
	 * Render model data, all 'sc_*' methods will be recongnized
	 * as shortcodes.
	 *
	 * @param string $template
	 * @param boolean $parsesc parse external shortcodes, default is true
	 * @param e_vars $eVars simple parser data
	 * @return string parsed template
	 */
	public function toHTML($template, $parsesc = true, $eVars = null)
	{
		$ret = '';
		$i = 1;
		foreach ($this->getTree() as $model)
		{
			if($eVars) $eVars->treeCounter = $i;
			$ret .= $model->toHTML($template, $parsesc, $eVars);
			$i++;
		}
		return $ret;
	}

	/**
	 * @return string
	 */
	public function toXML()
	{
		return '';
		// UNDER CONSTRUCTION
	}

	/**
	 * Convert model object to array
	 * @param boolean $total include total results property
	 * @return array object data
	 */
	public function toArray($total = false)
	{
		$ret = array();
		foreach ($this->getTree() as $id => $model)
		{
			$ret[$id] = $model->toArray();
		}
		if($total) $ret['total'] = $this->getTotal();

		return $ret;
	}

	/**
	 * Convert object data to a string
	 *
	 * @param boolean $AddSlashes
	 * @param string $node_id optional, if set method will return corresponding value as a string
	 * @param boolean $total include total results property
	 * @return string
	 */
	public function toString($AddSlashes = true, $node_id = null, $total = false)
	{
		if (null !== $node_id && $this->isNode($node_id))
		{
			return $this->getNode($node_id)->toString($AddSlashes);
		}
		return (string) e107::getArrayStorage()->serialize($this->toArray($total), $AddSlashes);
	}

	/**
	 * @param $from_post
	 * @param $force
	 * @param $session_messages
	 * @return void
	 */
	public function update($from_post = true, $force = false, $session_messages = false)
	{
	}

	/**
	 * @param $ids
	 * @param $destroy
	 * @param $session_messages
	 * @return void
	 */
	public function delete($ids, $destroy = true, $session_messages = false)
	{
	}
}


/**
 *
 */
class e_front_tree_model extends e_tree_model
{
	/**
	 * @var integer Last SQL error number
	 */
	protected $_db_errno = 0;

	/**
	 * @var string Last SQL error message
	 */
	protected $_db_errmsg = '';

	/**
	 * @var string Last SQL query
	 */
	protected $_db_qry = '';

    /**
     * @return boolean
     */
    public function hasSqlError()
    {
    	return !empty($this->_db_errno);
    }

    /**
     * @return integer last mysql error number
     */
    public function getSqlErrorNumber()
    {
    	return $this->_db_errno;
    }

    /**
     * @return string last mysql error message
     */
    public function getSqlError()
    {
    	return $this->_db_errmsg;
    }

    /**
     * @return string last mysql error message
     */
    public function getSqlQuery()
    {
    	return $this->_db_qry;
    }

    /**
     * @return boolean
     */
    public function hasError()
    {
    	return $this->hasSqlError();
    }

	/**
	 * Batch update tree records/nodes
	 * @param string $field field name
	 * @param string $value
	 * @param string|array $ids numerical array or string comma separated ids
	 * @param mixed $syncvalue value to be used for model data synchronization (db value could be something like '1-field_name'), null - no sync
	 * @param boolean $sanitize [optional] default true
	 * @param boolean $session_messages [optional] default false
	 * @return integer updated count or false on error
	 */
	public function batchUpdate($field, $value, $ids, $syncvalue = null, $sanitize = true, $session_messages = false)
	{
		$tp = e107::getParser();
		$sql = e107::getDb();
		if(empty($ids))
		{
			return 0;
		}
		if(!is_array($ids))
		{
			$ids = explode(',', $ids);
		}

		if(true === $syncvalue)
		{
			$syncvalue = $value;
		}

		if($sanitize)
		{
			$ids = array_map(array($tp, 'toDB'), $ids);
			$field = $tp->toDB($field);
			$value = "'".$tp->toDB($value)."'";
		}
		$idstr = implode(', ', $ids);

		$table = $this->getModelTable();

		$res = $sql->update($table, "{$field}={$value} WHERE ".$this->getFieldIdName().' IN ('.$idstr.')', $this->getParam('db_debug', false));
		$this->_db_errno = $sql->getLastErrorNumber();
		$this->_db_errmsg = $sql->getLastErrorText();
		$this->_db_qry = $sql->getLastQuery();

		if(!$res)
		{
			if($sql->getLastErrorNumber())
			{
				$data = array(
					'TABLE'     => $table ,
					'error_no' => $this->_db_errno,
					'error_msg' => $this->_db_errmsg,
					'qry'       => $this->_db_qry,
					'url'       => e_REQUEST_URI,
				);


				$this->addMessageError(LAN_UPDATED_FAILED, $session_messages, $data);
				$this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText());
			}
			else
			{
				$this->addMessageInfo(LAN_NO_CHANGE, $session_messages);
			}
		}
		else
		{
			$this->clearCache();
		}

		$modelCacheCheck = $this->getParam('clearModelCache');
		if(null === $syncvalue && !$modelCacheCheck) return $res;

		foreach ($ids as $id)
		{
			$node = $this->getNode($id);
			if(!$node) continue;

			if(null !== $syncvalue)
			{
				$node->set($field, $syncvalue)
					->setMessages($session_messages);
			}
			if($modelCacheCheck) $this->clearCache();
		}
		return $res;
	}
}


/**
 *
 */
class e_admin_tree_model extends e_front_tree_model
{


	/**
	 * Batch Delete records
	 * @param mixed $ids
	 * @param boolean $destroy [optional] destroy object instance after db delete
	 * @param boolean $session_messages [optional]
	 * @return integer deleted records number or false on DB error
	 */
	public function delete($ids, $destroy = true, $session_messages = false)
	{
		if(!$ids) return 0;

		if(!is_array($ids))
		{
			$ids = explode(',', $ids);
		}

		$tp = e107::getParser();
		$ids = array_map(array($tp, 'toDB'), $ids);
		$idstr = implode(', ', $ids);

		$sql = e107::getDb();
		$table = $this->getModelTable();
		$sqlQry = $this->getFieldIdName().' IN (\''.$idstr.'\')';

		$res = $sql->delete($table, $sqlQry);

		$this->_db_errno = $sql->getLastErrorNumber();
		$this->_db_errmsg = $sql->getLastErrorText();
		$this->_db_qry = $sql->getLastQuery();

		$modelCacheCheck = $this->getParam('clearModelCache');

		if(!$res)
		{
			if($sql->getLastErrorNumber())
			{
				$data = array(
					'TABLE'     => $table,
					'error_no' => $this->_db_errno,
					'error_msg' => $this->_db_errmsg,
					'qry'       => $this->_db_qry,
					'url'       => e_REQUEST_URI,
				);


				$this->addMessageError('SQL Delete Error: ' . $sql->getLastQuery(), $session_messages, $data); //TODO - Lan
				$this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText());
			}
		}
		elseif($destroy || $modelCacheCheck)
		{
			foreach ($ids as $id)
			{
				if($this->hasNode($id))
				{
					$this->getNode($id)->clearCache()->setMessages($session_messages);
					if($destroy)
					{
						call_user_func(array($this->getNode(trim($id)), 'destroy')); // first call model destroy method if any
						$this->setNode($id, null);
					}
				}
			}
		}

		if($table != 'admin_log')
		{
			$logData = array('TABLE'=>$table, 'WHERE'=>$sqlQry);
			e107::getLog()->addArray($logData)->save('ADMINUI_03');
		}
		return $res;
	}

	/**
	 * Batch Copy Table Rows.
	 */
	public function copy($ids, $session_messages = false)
	{
		if(empty($ids[0]))
		{
			$this->addMessageError('No IDs provided', $session_messages); //TODO - Lan
			$this->addMessageDebug(print_a(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),true),$session_messages); //TODO - Lan
			return false;
		}


		$tp = e107::getParser();
		$ids = array_map(array($tp, 'toDB'), $ids);
		$idstr = implode(', ', $ids);

		$sql = e107::getDb();
		$res = $sql->db_CopyRow($this->getModelTable(), "*", $this->getFieldIdName().' IN ('.$idstr.')');
		if(false !== $res)
		{
			$this->addMessageSuccess('Copied #'.$idstr);
		}
		else
		{
			if($sql->getLastErrorNumber())
			{
				$this->addMessageError('SQL Copy Error', $session_messages); //TODO - Lan
				$this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText());
				$this->addMessageDebug('$SQL Query'.print_a($sql->getLastQuery(),true));
			}
		}
		$this->_db_errno = $sql->getLastErrorNumber();
		$this->_db_errmsg = $sql->getLastErrorText();
		$this->_db_qry = $sql->getLastQuery();
		return $res;
	}


	/**
	 * Get urls/url data for given nodes
	 */
    public function url($ids, $options = array(), $extended = false)
    {
    	$ret = array();
    	foreach ($ids as $id)
    	{
    		if(!$this->hasNode($id)) continue;

			$model = $this->getNode($id);
			if($this->getUrl()) $model->setUrl($this->getUrl()); // copy url config data if available
			$ret[$id] = $model->url(null, $options, $extended);
		}
		return $ret;
    }


	/**
	 * Export Selected Data
	 * @param $ids
	 * @return null
	 */
	public function export($ids)
    {
        $ids = e107::getParser()->filter($ids,'int');

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

        $idstr = implode(', ', $ids);

	    $table      = array($this->getModelTable());

	    $filename   = "e107Export_" .$this->getModelTable()."_". date("YmdHi").".xml";
	    $query      = $this->getFieldIdName().' IN ('.$idstr.') '; //  ORDER BY '.$this->getParam('db_order') ;

		e107::getXml()->e107Export(null,$table,null,null, array('file'=>$filename,'query'=>$query));

		return null;

    }
}
