e107inc/e107

View on GitHub
e107_handlers/model_class.php

Summary

Maintainability
C
1 day
Test Coverage
F
43%
<?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;

    }
}