1<?php
2/*
3 * e107 website system
4 *
5 * Copyright (C) 2008-2010 e107 Inc (e107.org)
6 * Released under the terms and conditions of the
7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
8 *
9 * e107 Base Model
10 *
11 * $Id$
12 * $Author$
13*/
14
15if (!defined('e107_INIT')) { exit; }
16
17/**
18 * Base e107 Object class
19 *
20 * @package e107
21 * @category e107_handlers
22 * @version 1.0
23 * @author SecretR
24 * @copyright Copyright (C) 2010, e107 Inc.
25 */
26class e_object
27{
28    /**
29     * Object data
30     *
31     * @var array
32     */
33    protected $_data = array();
34
35	/**
36	 * Model parameters passed mostly from external sources
37	 *
38	 * @var array
39	 */
40	protected $_params = array();
41
42
43    /**
44    * Name of object id field
45    * Required for {@link getId()()} method
46    *
47    * @var string
48    */
49    protected $_field_id;
50
51    /**
52     * Constructor - set data on initialization
53     *
54     * @param array $data
55     */
56	function __construct($data = array())
57	{
58		if(is_array($data)) $this->setData($data);
59	}
60
61    /**
62     * Set name of object's field id
63     *
64     * @see getId()
65     *
66     * @param   string $name
67     * @return object e_object
68     */
69    public function setFieldIdName($name)
70    {
71        $this->_field_id = $name;
72        return $this;
73    }
74
75    /**
76     * Retrieve name of object's field id
77     *
78     * @see getId()
79     *
80     * @param   string $name
81     * @return  string
82     */
83    public function getFieldIdName()
84    {
85        return $this->_field_id;
86    }
87
88    /**
89     * Retrieve object primary id field value
90     *
91     * @return mixed
92     */
93    public function getId()
94    {
95
96        if ($this->getFieldIdName())
97        {
98            return $this->get($this->getFieldIdName(), 0); // default of NULL will break MySQL strict in most cases.
99        }
100        return $this->get('id', 0);
101    }
102
103    /**
104     * Set object primary id field value
105     *
106     * @return e_object
107     */
108    public function setId($id)
109    {
110        if ($this->getFieldIdName())
111        {
112            return $this->set($this->getFieldIdName(), $id);
113        }
114        return $this;
115    }
116
117    /**
118     * Retrieves data from the object ($_data) without
119     * key parsing (performance wise, prefered when possible)
120     *
121     * @param string $key
122     * @param mixed $default
123     * @return mixed
124     */
125	public function get($key, $default = null)
126    {
127    	return (isset($this->_data[$key]) ? $this->_data[$key] : $default);
128    }
129
130    /**
131     * Get object data
132     * @return array
133     */
134	public function getData()
135    {
136    	return $this->_data;
137    }
138
139    /**
140     * Overwrite data in the object for a single field.
141     *
142     * @param string $key
143     * @param mixed $value
144     * @return e_object
145     */
146	public function set($key, $value)
147    {
148    	$this->_data[$key] = $value;
149    	return $this;
150    }
151
152    /**
153     * Set object data
154     * @return e_object
155     */
156	public function setData($data)
157    {
158    	$this->_data = $data;
159    	return $this;
160    }
161
162    /**
163     * Update object data
164     * @return e_object
165     */
166	public function addData($data)
167    {
168    	foreach($data as $key => $val)
169		{
170			$this->set($key, $val);
171		}
172    	return $this;
173    }
174
175    /**
176     * Remove object data key
177     *
178     * @param string $key
179     * @return e_object
180     */
181	public function remove($key)
182    {
183    	unset($this->_data[$key]);
184    	return $this;
185    }
186
187    /**
188     * Reset  object data key
189     *
190     * @return e_object
191     */
192	public function removeData()
193    {
194    	$this->_data = array();
195    	return $this;
196    }
197
198    /**
199     * Check if key is set
200     * @param string $key
201     * @return boolean
202     */
203    public function is($key)
204    {
205    	return (isset($this->_data[$key]));
206    }
207
208    /**
209     * Check if key is set and not empty
210     * @param string $key
211     * @return boolean
212     */
213    public function has($key)
214    {
215    	return (isset($this->_data[$key]) && !empty($this->_data[$key]));
216    }
217
218    /**
219     * Check if object has data
220     * @return boolean
221     */
222    public function hasData()
223    {
224    	return !empty($this->_data);
225    }
226
227	/**
228	 * Set parameter array
229	 * @param array $params
230	 * @return e_object
231	 */
232	public function setParams(array $params)
233	{
234		$this->_params = $params;
235		return $this;
236	}
237
238	/**
239	 * Update parameter array
240	 * @param array $params
241	 * @return object e_object
242	 */
243	public function updateParams(array $params)
244	{
245		foreach ($params as $k => $v)
246		{
247			$this->setParam($k, $v);
248		}
249
250		return $this;
251	}
252
253	/**
254	 * Get parameter array
255	 *
256	 * @return array parameters
257	 */
258	public function getParams()
259	{
260		return $this->_params;
261	}
262
263	/**
264	 * Set parameter
265	 *
266	 * @param string $key
267	 * @param mixed $value
268	 * @return e_tree_model
269	 */
270	public function setParam($key, $value)
271	{
272		if(null === $value)
273		{
274			unset($this->_params[$key]);
275		}
276		else $this->_params[$key] = $value;
277
278		return $this;
279	}
280
281	/**
282	 * Get parameter
283	 *
284	 * @param string $key
285	 * @param mixed $default
286	 */
287	public function getParam($key, $default = null)
288	{
289		return (isset($this->_params[$key]) ? $this->_params[$key] : $default);
290	}
291
292	/**
293	 * Convert object data to simple shortcodes (e_vars object)
294	 * @return string
295	 */
296	public function toSc()
297	{
298		return new e_vars($this->_data);
299	}
300
301	/**
302	 * Convert object data to array
303	 * @return string
304	 */
305	public function toJson()
306	{
307		return json_encode($this->_data);
308	}
309
310	/**
311	 * Convert object to array
312	 * @return array object data
313	 */
314	public function toArray()
315	{
316		return $this->_data;
317	}
318
319	/**
320	 * Magic method - convert object data to an array
321	 *
322	 * @return array
323	 */
324	public function __toArray()
325	{
326		return $this->toArray();
327	}
328
329	/**
330	 * Convert object data to a string
331	 *
332	 * @param boolean $AddSlashes
333	 * @return string
334	 */
335	public function toString($AddSlashes = false)
336	{
337		return (string) e107::getArrayStorage()->WriteArray($this->toArray(), $AddSlashes);
338	}
339
340	/**
341	 * Magic method - convert object data to a string
342	 * NOTE: before PHP 5.2.0 the __toString method was only
343	 * called when it was directly combined with echo() or print()
344	 *
345	 * NOTE: PHP 5.3+ is throwing parse error if __toString has optional arguments.
346	 *
347	 * @return string
348	 */
349	public function __toString()
350	{
351		return $this->toString(false);
352	}
353
354	/**
355	 * Magic setter
356	 * Triggered on e.g. <code><?php $e_object->myKey = 'someValue'; </code>
357	 *
358	 * @param string $key
359	 * @param mixed $value
360	 */
361	public function __set($key, $value)
362	{
363		// Unset workaround - PHP < 5.1.0
364		if(null === $value) $this->remove($key);
365		else $this->set($key, $value);
366	}
367
368	/**
369	 * Magic getter
370	 * Triggered on e.g. <code><?php print($e_object->myKey); </code>
371	 * @param string $key
372	 * @return mixed value or null if key not found
373	 */
374	public function __get($key)
375	{
376		if($this->is($key))
377		{
378			return $this->get($key);
379		}
380
381		return null;
382	}
383
384	/**
385	 * Magic method to check if given data key is set.
386	 * Triggered on <code><?php isset($e_object->myKey); </code>
387	 * NOTE: works on PHP 5.1.0+
388	 *
389	 * @param string $key
390	 * @return boolean
391	 */
392	public function __isset($key)
393	{
394		return $this->is($key);
395	}
396
397	/**
398	 * Magic method to unset given data key.
399	 * Triggered on <code><?php unset($e_object->myKey); </code>
400	 * NOTE: works on PHP 5.1.0+
401	 *
402	 * @param string $key
403	 */
404	public function __unset($key)
405	{
406		$this->remove($key);
407	}
408}
409
410
411/**
412 * Data object for e_parse::simpleParse()
413 * NEW - not inherits core e_object
414 * Moved from e_parse_class.php
415 * Could go in separate file in the future, together with e_object class
416 */
417class e_vars extends e_object
418{
419	/**
420	 * Get data array
421	 *
422	 * @return array
423	 */
424	public function getVars()
425	{
426		return $this->getData();
427	}
428
429	/**
430	 * Set array data
431	 *
432	 * @param array $array
433	 * @return e_vars
434	 */
435	public function setVars(array $array)
436	{
437		$this->setData($array);
438		return $this;
439	}
440
441	/**
442	 * Add array data to the object (merge with existing)
443	 *
444	 * @param array $array
445	 * @return e_vars
446	 */
447	public function addVars(array $array)
448	{
449		$this->addData($array);
450	}
451
452	/**
453	 * Reset object data
454	 *
455	 * @return e_vars
456	 */
457	public function emptyVars()
458	{
459		$this->removeData();
460		return $this;
461	}
462
463	/**
464	 * Check if there is data available
465	 *
466	 * @return boolean
467	 */
468	public function isEmpty()
469	{
470		return (!$this->hasData());
471	}
472
473	/**
474	 * Check if given data key is set
475	 * @param string $key
476	 * @return boolean
477	 */
478	public function isVar($key)
479	{
480		return $this->is($key);
481	}
482
483	/**
484	 * No need of object conversion, optional cloning
485	 * @param boolean $clone return current object clone
486	 * @return e_vars
487	 */
488	public function toSc($clone = false)
489	{
490		if($clone) return clone $this;
491		return $this;
492	}
493}
494
495/**
496 * Base e107 Model class
497 *
498 * @package e107
499 * @category e107_handlers
500 * @version 1.0
501 * @author SecretR
502 * @copyright Copyright (C) 2010, e107 Inc.
503 */
504class e_model extends e_object
505{
506    /**
507     * Data structure (types) array, required for {@link e_front_model::sanitize()} method,
508     * it also serves as a map (find data) for building DB queries,
509     * copy/sanitize posted data to object data, etc.
510     *
511     * This can/should be overwritten by extending the class
512     *
513     * @var array
514     */
515    protected $_data_fields = array();
516
517
518
519	/**
520	 * Current model field types eg. text, bbarea, dropdown etc.
521	 *
522	 *
523	 * @var string
524	 */
525	protected $_field_input_types = array();
526
527
528	/**
529	 * Current model DB table, used in all db calls
530	 *
531	 * This can/should be overwritten/set by extending the class
532	 *
533	 * @var string
534	 */
535	protected $_db_table;
536
537    /**
538     * Current url Profile data
539	 * Example: array('route'=>'page/view/index', 'vars' => array('id' => 'page_id', 'sef' => 'page_sef'), 'name' => 'page_title', 'description' => '');
540     * @var string
541     */
542    protected $_url = array();
543
544
545    /**
546     * Current Featurebox Profile data
547	 * Example: array('title' => 'page_title', 'text' => '');
548     * @var string
549     */
550    protected $_featurebox = array();
551
552    /**
553     * Runtime cache of parsed from {@link _getData()} keys
554     *
555     * @var array
556     */
557    protected $_parsed_keys = array();
558
559    /**
560     * Avoid DB calls if data is not changed
561     *
562     * @see _setData()
563     * @var boolean
564     */
565    protected $data_has_changed = false;
566
567	/**
568	 * Namespace to be used for model related system messages in {@link eMessage} handler
569	 *
570	 * @var string
571	 */
572	protected $_message_stack = 'default';
573
574	/**
575	 * Cache string to be used from _get/set/clearCacheData() methods
576	 *
577	 * @var string
578	 */
579	protected $_cache_string = null;
580
581	/**
582	 * Force Cache even if system cahche is disabled
583	 * Default is false
584	 *
585	 * @var boolean
586	 */
587	protected $_cache_force = false;
588
589
590	/**
591	 * Optional DB table - used for auto-load data from the DB
592	 * @param string $table
593	 * @return e_model
594	 */
595	public function getModelTable()
596	{
597		return $this->_db_table;
598	}
599
600	/**
601	 * Set model DB table
602	 * @param string $table
603	 * @return e_model
604	 */
605	public function setModelTable($table)
606	{
607		$this->_db_table = $table;
608		return $this;
609	}
610
611
612    /**
613     * Set model Url Profile
614     * @param string $table
615     * @return e_model
616     */
617    public function setUrl($url)
618    {
619    	if(!is_array($url)) $url = array('route' => $url);
620        $this->_url = $url;
621        return $this;
622    }
623
624    /**
625     * Get url profile
626     * @return array
627     */
628    public function getUrl()
629    {
630        return $this->_url;
631    }
632
633    /**
634     * Set model Featurebox  Profile
635     * @param string $table
636     * @return e_model
637     */
638    public function setFeaturebox($fb)
639    {
640    //	if(!is_array($url)) $url = array('route' => $url);
641        $this->_featurebox = $fb;
642        return $this;
643    }
644
645    /**
646     * Get Featurebox profile
647     * @return array
648     */
649    public function getFeaturebox()
650    {
651        return $this->_featurebox;
652    }
653
654
655    /**
656     * Generic URL assembling method
657	 * @param array $options [optional] see eRouter::assemble() for $options structure
658	 * @param boolean $extended [optional] if true, method will return an array containing url, title and description of the url
659     * @return mixed URL string or extended array data
660     */
661    public function url($ids, $options = array(), $extended = false)
662    {
663        $urldata = $this->getUrl();
664		if(empty($urldata) || !vartrue($urldata['route'])) return ($extended ? array() : null);
665
666		$eurl = e107::getUrl();
667
668		if(empty($options)) $options = array();
669		elseif(!is_array($options)) parse_str($options, $options);
670
671		$vars = $this->toArray();
672		if(!isset($options['allow']) || empty($options['allow']))
673		{
674			if(vartrue($urldata['vars']) && is_array($urldata['vars']))
675			{
676				$vars = array();
677				foreach ($urldata['vars'] as $var => $field)
678				{
679					if($field === true) $field = $var;
680					$vars[$var] = $this->get($field);
681				}
682			}
683		}
684
685		$method = isset($options['sc']) ? 'sc' : 'create';
686
687		$url = e107::getUrl()->$method($urldata['route'], $vars, $options);
688
689		if(!$extended)
690		{
691			return $url;
692		}
693
694		return array(
695			'url' => $url,
696			'name' => vartrue($urldata['name']) ? $this->get($urldata['name']) : '',
697			'description' => vartrue($urldata['description']) ? $this->get($urldata['description']) : '',
698		);
699    }
700
701
702     /**
703     * Generic Featurebox assembling method
704     * @return mixed URL string or extended array data
705     */
706    public function featurebox($options = array(), $extended = false)
707    {
708
709
710    }
711
712
713
714
715
716
717
718
719
720
721
722
723
724    /**
725     * Get data fields array
726     * @return array
727     */
728    public function getDataFields()
729    {
730    	return $this->_data_fields;
731    }
732
733
734	/**
735	 * @param $key
736	 * @return bool
737	 */
738	public function getFieldInputType($key)
739    {
740        if(isset($this->_field_input_types[$key]))
741        {
742            return $this->_field_input_types[$key];
743        }
744
745        return false;
746    }
747
748    /**
749     * Set Predefined data fields in format key => type
750     * @return object e_model
751     */
752    public function setDataFields($data_fields)
753    {
754    	$this->_data_fields = $data_fields;
755		return $this;
756    }
757
758	/**
759     * Set Predefined data fields in format key => type
760     * @return e_model
761     */
762    public function setFieldInputTypes($fields)
763    {
764    	$this->_field_input_types = $fields;
765		return $this;
766    }
767
768    /**
769     * Set Predefined data field
770     * @return e_model
771     */
772    public function setDataField($field, $type)
773    {
774    	$this->_data_fields[$field] = $type;
775		return $this;
776    }
777
778    /**
779     * Retrieves data from the object ($_data) without
780     * key parsing (performance wise, prefered when possible)
781     *
782     * @see _getDataSimple()
783     * @param string $key
784     * @param mixed $default
785     * @return mixed
786     */
787	public function get($key, $default = null)
788    {
789    	return $this->_getDataSimple((string) $key, $default);
790    }
791
792    /**
793     * Retrieves data from the object ($_data)
794     * If $key is empty, return all object data
795     *
796     * @see _getData()
797     * @param string $key
798     * @param mixed $default
799     * @param integer $index
800     * @return mixed
801     */
802	public function getData($key = '', $default = null, $index = null)
803    {
804    	return $this->_getData($key, $default, $index);
805    }
806
807    /**
808     * Overwrite data in the object for a single field. Key is not parsed.
809     * Public proxy of {@link _setDataSimple()}
810     * Data isn't sanitized so use this method only when data comes from trustable sources (e.g. DB)
811     *
812     *
813     * @see _setData()
814     * @param string $key
815     * @param mixed $value
816     * @param boolean $strict update only
817     * @return e_model
818     */
819	public function set($key, $value = null, $strict = false)
820    {
821    	return $this->_setDataSimple($key, $value, $strict);
822    }
823
824    /**
825     * Overwrite data in the object. Public proxy of {@link _setData()}
826     * Data isn't sanitized so use this method only when data comes from trustable sources (e.g. DB)
827     *
828     * @see _setData()
829     * @param string|array $key
830     * @param mixed $value
831     * @param boolean $strict update only
832     * @return e_model
833     */
834	public function setData($key, $value = null, $strict = false)
835    {
836    	return $this->_setData($key, $value, $strict);
837    }
838
839    /**
840     * Add data to the object.
841     * Retains existing data in the object.
842     * Public proxy of {@link _addData()}
843     *
844     * If $override is false, data will be updated only (check against existing data)
845     *
846     * @param string|array $key
847     * @param mixed $value
848     * @param boolean $override override existing data
849     * @return e_model
850     */
851    public function addData($key, $value = null, $override = true)
852    {
853    	return $this->_addData($key, $value, $override);
854    }
855
856	/**
857     * Unset single field from the object.
858     * Public proxy of {@link _unsetDataSimple()}
859     *
860     * @param string $key
861     * @return e_model
862     */
863    public function remove($key)
864    {
865    	return $this->_unsetDataSimple($key);
866    }
867
868	/**
869     * Unset data from the object.
870     * $key can be a string only. Array will be ignored.
871     * '/' inside the key will be treated as array path
872     * if $key is null entire object will be reset
873     *
874     * Public proxy of {@link _unsetData()}
875     *
876     * @param string|null $key
877     * @return e_model
878     */
879    public function removeData($key = null)
880    {
881    	return $this->_unsetData($key);
882    }
883
884    /**
885     * @param string $key
886     * @return boolean
887     */
888    public function has($key)
889    {
890    	return $this->_hasData($key);
891    }
892
893    /**
894     * @param string $key
895     * @return boolean
896     */
897    public function hasData($key = '')
898    {
899    	return $this->_hasData($key);
900    }
901
902    /**
903     * @param string $key
904     * @return boolean
905     */
906    public function isData($key)
907    {
908    	return $this->_isData($key);
909    }
910
911    /**
912     * @param boolean $new_state new object state if set
913     * @return boolean
914     */
915    public function isModified($new_state = null)
916    {
917    	if(is_bool($new_state))
918    	{
919    		$this->data_has_changed = $new_state;
920    	}
921    	return $this->data_has_changed;
922    }
923
924    /**
925     * Retrieves data from the object
926     *
927     * If $key is empty will return all the data as an array
928     * Otherwise it will return value of the attribute specified by $key
929     * '/' inside the key will be treated as array path (x/y/z equals to [x][y][z]
930     *
931     * If $index is specified it will assume that attribute data is an array
932     * and retrieve corresponding member.
933     *
934     * NEW: '/' supported in keys now, just use double slashes '//' as key separator
935     * Examples:
936     * - 'key//some/key/with/slashes//more' -> [key][some/key/with/slashes][more]
937     * - '//some/key' -> [some/key] - starting with // means - don't parse!
938     * - '///some/key/' -> [/some/key/]
939     * - '//key//some/key/with/slashes//more' WRONG -> single key [key//some/key/with/slashes//more]
940     *
941     * @param string $key
942     * @param mixed $default
943     * @param integer $index
944     * @param boolean $posted data source
945     * @return mixed
946     */
947    protected function _getData($key = '', $default = null, $index = null, $data_src = '_data')
948    {
949        if ('' === $key)
950        {
951            return $this->$data_src;
952        }
953
954        $simple = false;
955        if(strpos($key, '//') === 0)
956        {
957        	$key = substr($key, 2);
958        	$simple = true;
959        }
960        /*elseif($key[0] == '/')
961        {
962        	// just use it!
963        	$simple = true;
964        }*/
965        else
966        {
967        	$simple = strpos($key, '/') === false;
968        }
969
970        // Fix - check if _data[path/to/value] really doesn't exist
971        if (!$simple)
972        {
973        	//$key = trim($key, '/');
974        	if(isset($this->_parsed_keys[$data_src.'/'.$key]))
975        	{
976        		return $this->_parsed_keys[$data_src.'/'.$key];
977        	}
978        	// new feature (double slash) - when searched key string is key//some/key/with/slashes//more
979        	// -> search for 'key' => array('some/key/with/slashes', array('more' => value));
980            $keyArr = explode(strpos($key, '//') ? '//' : '/', $key);
981            $data = $this->$data_src;
982            foreach ($keyArr as $i => $k)
983            {
984                if ('' === $k)
985                {
986                    return $default;
987                }
988
989                if (is_array($data))
990                {
991                    if (!isset($data[$k]))
992                    {
993                        return $default;
994                    }
995                    $data = $data[$k];
996                }
997                else
998                {
999                    return $default;
1000                }
1001            }
1002            $this->_parsed_keys[$data_src.'/'.$key] = $data;
1003            return $data;
1004        }
1005
1006        //get $index
1007        if (isset($this->{$data_src}[$key]))
1008        {
1009            if (null === $index)
1010            {
1011                return $this->{$data_src}[$key];
1012            }
1013
1014            $value = $this->{$data_src}[$key];
1015            if (is_array($value))
1016            {
1017                if (isset($value[$index]))
1018                {
1019                    return $value[$index];
1020                }
1021                return $default;
1022            }
1023            elseif (is_string($value))
1024            {
1025                $arr = explode("\n", $value);
1026                return (isset($arr[$index]) ? $arr[$index] : $default);
1027            }
1028            return $default;
1029        }
1030        return $default;
1031    }
1032
1033    /**
1034     * Get value from _data array without parsing the key
1035     *
1036     * @param string $key
1037     * @param mixed $default
1038     * @param string $posted data source
1039     * @return mixed
1040     */
1041    protected function _getDataSimple($key, $default = null, $data_src = '_data')
1042    {
1043        return isset($this->{$data_src}[$key]) ? $this->{$data_src}[$key] : $default;
1044    }
1045
1046    /**
1047     * Overwrite data in the object.
1048     *
1049     * $key can be string or array.
1050     * If $key is string, the attribute value will be overwritten by $value
1051     * '/' inside the key will be treated as array path
1052     *
1053     * If $key is an array and $strict is false, it will overwrite all the data in the object.
1054     *
1055     * If $strict is true and $data_src is '_data', data will be updated only (no new data will be added)
1056     *
1057     * NEW: '/' supported in keys now, just use double slashes '//' as key separator
1058     * Examples:
1059     * - 'key//some/key/with/slashes//more' -> [key][some/key/with/slashes][more]
1060     * - '//some/key' -> [some/key] - starting with // means - don't parse!
1061     * - '///some/key/' -> [/some/key/]
1062     * - '//key//some/key/with/slashes//more' WRONG -> single key [key//some/key/with/slashes//more]
1063     *
1064     *
1065     * @param string|array $key
1066     * @param mixed $value
1067     * @param boolean $strict
1068     * @param string $data_src
1069     * @return e_model
1070     */
1071    protected function _setData($key, $value = null, $strict = false, $data_src = '_data')
1072    {
1073        if(is_array($key))
1074        {
1075            if($strict)
1076	    	{
1077				foreach($key as $k => $v)
1078		        {
1079		        	$this->_setData($k, $v, true, $data_src);
1080		        }
1081		        return $this;
1082	    	}
1083
1084            $this->$data_src = $key;
1085            return $this;
1086        }
1087
1088        //multidimensional array support - strict _setData for values of type array
1089    	if($strict && !empty($value) && is_array($value))
1090       	{
1091			foreach($value as $k => $v)
1092			{
1093				// new - $k couldn't be a path - e.g. 'key' 'value/value1'
1094				// will result in 'key' => 'value/value1' and NOT 'key' => array('value' => value1)
1095			    $this->_setData($key.'//'.$k, $v, true, $data_src);
1096			}
1097			return $this;
1098       	}
1099
1100        $simple = false;
1101        if(strpos($key, '//') === 0)
1102        {
1103        	// NEW - leading '//' means set 'some/key' without parsing it
1104        	// Example: '//some/key'; NOTE: '//some/key//more/depth' is NOT parsed
1105        	// if you wish to have array('some/key' => array('more/depth' => value))
1106        	// right syntax is 'some/key//more/depth'
1107        	$key = substr($key, 2);
1108        	$simple = true;
1109        }
1110        /*elseif($key[0] == '/')
1111        {
1112        	$simple = true;
1113        }*/
1114        else
1115        {
1116        	$simple = strpos($key, '/') === false;
1117        }
1118
1119        //multidimensional array support - parse key
1120        if(!$simple)
1121        {
1122        	//$key = trim($key, '/');
1123        	//if strict - update only
1124	        if($strict && !$this->isData($key))
1125	        {
1126	        	return $this;
1127	        }
1128
1129        	// new feature (double slash) - when parsing key: key//some/key/with/slashes//more
1130        	// -> result is 'key' => array('some/key/with/slashes', array('more' => value));
1131	        $keyArr = explode(strpos($key, '//') ? '//' : '/', $key);
1132        	//$keyArr = explode('/', $key);
1133        	$data = &$this->{$data_src};
1134            for ($i = 0, $l = count($keyArr); $i < $l; $i++)
1135            {
1136
1137	            $k = $keyArr[$i];
1138
1139	            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.
1140	            {
1141	                $data[$k] = array();
1142	            }
1143
1144	            $data = &$data[$k];
1145	        }
1146
1147	        //data has changed - optimized
1148	        if('_data' === $data_src && !$this->data_has_changed)
1149	        {
1150	        	$this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value);
1151	        }
1152	        $this->_parsed_keys[$data_src.'/'.$key] = $value;
1153	        $data = $value;
1154        }
1155        else
1156        {
1157			//if strict - update only
1158	        if($strict && !isset($this->_data[$key]))
1159	        {
1160	        	return $this;
1161	        }
1162        	if('_data' === $data_src && !$this->data_has_changed)
1163	        {
1164	        	$this->data_has_changed = (!isset($this->_data[$key]) || $this->{$data_src}[$key] != $value);
1165	        }
1166            $this->{$data_src}[$key] = $value;
1167        }
1168
1169        return $this;
1170    }
1171
1172    /**
1173     * Set data for the given source. More simple (and performance wise) version
1174     * of {@link _setData()}
1175     *
1176     * @param string $key
1177     * @param mixed $value
1178     * @param boolean $strict
1179     * @param string $data_src
1180     * @return e_model
1181     */
1182	protected function _setDataSimple($key, $value = null, $strict = false, $data_src = '_data')
1183    {
1184    	$key = $key.'';//smart toString
1185    	if(!$strict)
1186    	{
1187			//data has changed
1188	        if('_data' === $data_src && !$this->data_has_changed)
1189	        {
1190	        	$this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value);
1191	        }
1192	        $this->{$data_src}[$key] = $value;
1193    		return $this;
1194    	}
1195
1196    	if($this->isData($key))
1197    	{
1198			if('_data' === $data_src && !$this->data_has_changed)
1199	        {
1200	        	$this->data_has_changed = (!isset($this->_data[$key]) || $this->_data[$key] != $value);
1201	        }
1202	        $this->{$data_src}[$key] = $value;
1203    	}
1204
1205    	return $this;
1206    }
1207
1208    /**
1209     * Add data to the object.
1210     * Retains existing data in the object.
1211     *
1212     * If $override is false, only new (non-existent) data will be added
1213     *
1214     * @param string|array $key
1215     * @param mixed $value
1216     * @param boolean $override allow override of existing data
1217     * @param string $data_src data source
1218     * @return e_model
1219     */
1220    protected function _addData($key, $value = null, $override = true, $data_src = '_data')
1221    {
1222    	if(is_array($key))
1223    	{
1224			foreach($key as $k => $v)
1225			{
1226			    $this->_addData($k, $v, $override, $data_src);
1227			}
1228			return $this;
1229    	}
1230
1231		if($override || !$this->_isData($key, $data_src))
1232       	{
1233       		if(is_array($value))
1234       		{
1235				if(is_array($key))
1236				{
1237					foreach($key as $k => $v)
1238					{
1239					    $this->_addData($key.'/'.$k, $v, $override, $data_src);
1240					}
1241				}
1242				return $this;
1243       		}
1244       		$this->_setData($key, $value, false, $data_src);
1245       	}
1246        return $this;
1247    }
1248
1249    /**
1250     * Unset data from the object from the given source.
1251     * $key can be a string only. Array will be ignored.
1252     * '/' inside the key will be treated as array path
1253     * if $key is null entire object will be reset
1254     *
1255     * @param string|null $key
1256     * @param string $data_src data source
1257     * @return e_model
1258     */
1259    protected function _unsetData($key = null, $data_src = '_data')
1260    {
1261        if (null === $key)
1262        {
1263        	if('_data' === $data_src && !empty($this->_data))
1264        	{
1265        		$this->data_has_changed = true;
1266        	}
1267        	$this->{$data_src} = array();
1268            return $this;
1269        }
1270
1271        $key = trim($key, '/');
1272        if(strpos($key,'/'))
1273        {
1274        	$keyArr = explode('/', $key);
1275        	$data = &$this->{$data_src};
1276
1277        	$unskey = array_pop($keyArr);
1278            for ($i = 0, $l = count($keyArr); $i < $l; $i++)
1279            {
1280	            $k = $keyArr[$i];
1281	            if (!isset($data[$k]))
1282	            {
1283	                return $this; //not found
1284	            }
1285	            $data = &$data[$k];
1286	        }
1287	        if(is_array($data))
1288	        {
1289				if('_data' === $data_src && isset($data[$unskey]))
1290	        	{
1291	        		$this->data_has_changed = true;
1292	        	}
1293	        	unset($data[$unskey], $this->_parsed_keys[$data_src.'/'.$key]);
1294	        }
1295        }
1296        else
1297        {
1298       		if('_data' === $data_src && isset($this->{$data_src}[$key]))
1299        	{
1300        		$this->data_has_changed = true;
1301        	}
1302            unset($this->{$data_src}[$key]);
1303        }
1304        return $this;
1305    }
1306
1307	/**
1308     * Unset single field from the object from the given source. Key is not parsed
1309     *
1310     * @param string $key
1311     * @param string $data_src data source
1312     * @return e_model
1313     */
1314    protected function _unsetDataSimple($key, $data_src = '_data')
1315    {
1316		if('_data' === $data_src && isset($this->{$data_src}[$key]))
1317       	{
1318       		$this->data_has_changed = true;
1319       	}
1320    	unset($this->{$data_src}[$key]);
1321    	return $this;
1322    }
1323
1324    /**
1325     * If $key is empty, checks whether there's any data in the object
1326     * Otherwise checks if the specified key is empty/set.
1327     *
1328     * @param string $key
1329     * @param string $data_src data source
1330     * @return boolean
1331     */
1332    protected function _hasData($key = '', $data_src = '_data')
1333    {
1334        if (empty($key))
1335        {
1336            return !empty($this->$data_src);
1337        }
1338        $value = $this->_getData($key, null, null, $data_src);
1339        return !empty($value);
1340    }
1341
1342    /**
1343     * Checks if the specified key is set
1344     *
1345     * @param string $key
1346     * @param string $data_src data source
1347     * @return boolean
1348     */
1349    protected function _isData($key, $data_src = '_data')
1350    {
1351        return (null !== $this->_getData($key, null, null, $data_src));
1352    }
1353
1354	/**
1355	 * Add system message of type Information
1356	 *
1357	 * @param string $message
1358	 * @param boolean $session [optional]
1359	 * @return e_model
1360	 */
1361	public function addMessageInfo($message, $session = false)
1362	{
1363		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_INFO, $session);
1364		return $this;
1365	}
1366
1367	/**
1368	 * Add system message of type Success
1369	 *
1370	 * @param string $message
1371	 * @param boolean $session [optional]
1372	 * @return e_model
1373	 */
1374	public function addMessageSuccess($message, $session = false)
1375	{
1376		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_SUCCESS, $session);
1377		return $this;
1378	}
1379
1380	/**
1381	 * Add system message of type Warning
1382	 *
1383	 * @param string $message
1384	 * @param boolean $session [optional]
1385	 * @return e_model
1386	 */
1387	public function addMessageWarning($message, $session = false)
1388	{
1389		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_WARNING, $session);
1390		return $this;
1391	}
1392
1393	/**
1394	 * Add system message of type Error
1395	 *
1396	 * @param string $message
1397	 * @param boolean $session [optional]
1398	 * @param array $logData [optional] array('TABLE'=>'', 'ERROR'=>'') etc.
1399	 * @return e_model
1400	 */
1401	public function addMessageError($message, $session = false, $logData = array())
1402	{
1403		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_ERROR, $session);
1404
1405		if(!empty($logData))
1406		{
1407			e107::getAdminLog()->addArray($logData);
1408		}
1409		else
1410		{
1411			e107::getAdminLog()->addError($message,false);
1412		}
1413
1414		e107::getAdminLog()->save('ADMINUI_04', E_LOG_WARNING);
1415
1416		return $this;
1417	}
1418
1419	/**
1420	 * Add system message of type Information
1421	 *
1422	 * @param string $message
1423	 * @param boolean $session [optional]
1424	 * @return e_model
1425	 */
1426	public function addMessageDebug($message, $session = false)
1427	{
1428		e107::getMessage()->addStack($message, $this->_message_stack, E_MESSAGE_DEBUG, $session);
1429		return $this;
1430	}
1431
1432    /**
1433     * Render System messages (if any)
1434     *
1435     * @param boolean $session store messages to session
1436     * @param boolean $reset reset errors
1437     * @return string
1438     */
1439    public function renderMessages($session = false, $reset = true)
1440    {
1441    	return e107::getMessage()->render($this->_message_stack, $session, $reset);
1442    }
1443
1444    /**
1445     * Move model System messages (if any) to the default eMessage stack
1446     *
1447     * @param boolean $session store messages to session
1448     * @return e_model
1449     */
1450    public function setMessages($session = false)
1451    {
1452    	e107::getMessage()->moveStack($this->_message_stack, 'default', false, $session);
1453		return $this;
1454    }
1455
1456    /**
1457     * Reset model System messages
1458     *
1459     * @param boolean|string $type E_MESSAGE_INFO | E_MESSAGE_SUCCESS | E_MESSAGE_WARNING | E_MESSAGE_WARNING | E_MESSAGE_DEBUG | false (all)
1460     * @param boolean $session reset also session messages
1461     * @return e_model
1462     */
1463    public function resetMessages($type = false, $session = false)
1464    {
1465        e107::getMessage()->reset($type, $this->_message_stack, $session);
1466		return $this;
1467    }
1468
1469	/**
1470	 * Set model message stack
1471	 * @param string $stack_name
1472	 * @return e_model
1473	 */
1474    public function setMessageStackName($stack_name)
1475    {
1476    	$this->_message_stack = $stack_name;
1477		return $this;
1478    }
1479
1480	/**
1481	 * Get model message stack name
1482	 * @return string
1483	 */
1484    public function getMessageStackName()
1485    {
1486		return $this->_message_stack;
1487    }
1488
1489    /**
1490     * User defined model validation
1491     * Awaiting for child class implementation
1492     *
1493     */
1494    public function verify()
1495    {
1496    }
1497
1498    /**
1499     * Model validation
1500     * @see e_model_admin
1501     */
1502    public function validate()
1503    {
1504    }
1505
1506    /**
1507     * Generic load data from DB
1508	 * @param mixed $id
1509     * @param boolean $force
1510     * @return e_model
1511     */
1512	public function load($id = null, $force = false)
1513	{
1514
1515
1516		if(!$force && $this->getId())
1517		{
1518			return $this;
1519		}
1520
1521		if($force)
1522		{
1523			$this->setData(array())
1524				->_clearCacheData();
1525		}
1526		if($id) $id = e107::getParser()->toDB($id);
1527		if(!$id && !$this->getParam('db_query'))
1528		{
1529			return $this;
1530		}
1531
1532		$cached = $this->_getCacheData();
1533		if($cached !== false)
1534		{
1535			$this->setData($cached);
1536			return $this;
1537		}
1538
1539		$sql = e107::getDb();
1540		$qry = str_replace('{ID}', $id, $this->getParam('db_query'));
1541		if($qry)
1542		{
1543			$res = $sql->gen($qry, $this->getParam('db_debug') ? true : false);
1544		}
1545		else
1546		{
1547			if(!is_numeric($id)) $id = "'{$id}'";
1548
1549			$res = $sql->select(
1550				$this->getModelTable(),
1551				$this->getParam('db_fields', '*'),
1552				$this->getFieldIdName().'='.$id.' '.trim($this->getParam('db_where', '')),
1553				'default',
1554				($this->getParam('db_debug') ? true : false)
1555			);
1556		}
1557
1558
1559		if($res)
1560		{
1561			$this->setData($sql->fetch());
1562		}
1563
1564		if($sql->getLastErrorNumber())
1565		{
1566			$this->addMessageDebug('SQL error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText());
1567			$this->addMessageDebug($sql->getLastQuery());
1568		}
1569		else
1570		{
1571
1572			$this->_setCacheData();
1573		}
1574
1575		return $this;
1576	}
1577
1578	/**
1579	 * Retrieve system cache (if any)
1580	 * @return array|false
1581	 */
1582	protected function _getCacheData()
1583	{
1584		if(!$this->isCacheEnabled())
1585		{
1586			return false;
1587		}
1588
1589		$cached = e107::getCache()->retrieve_sys($this->getCacheString(true), false, $this->_cache_force);
1590		if(false !== $cached)
1591		{
1592			return e107::unserialize($cached);
1593		}
1594
1595		return false;
1596	}
1597
1598	/**
1599	 * Set system cache if enabled for the model
1600	 * @return e_model
1601	 */
1602	protected function _setCacheData()
1603	{
1604		if(!$this->isCacheEnabled())
1605		{
1606			return $this;
1607		}
1608		e107::getCache()->set_sys($this->getCacheString(true), $this->toString(false), $this->_cache_force, false);
1609		return $this;
1610	}
1611
1612	/**
1613	 * Clrear system cache if enabled for the model
1614	 * @return e_model
1615	 */
1616	protected function _clearCacheData()
1617	{
1618		if(!$this->isCacheEnabled(false))
1619		{
1620			return $this;
1621		}
1622		e107::getCache()->clear_sys($this->getCacheString(true), false);
1623		return $this;
1624	}
1625
1626	/**
1627	 * Clrear system cache (public proxy) if enabled for the model
1628	 * @return e_model
1629	 */
1630	public function clearCache()
1631	{
1632		return $this->_clearCacheData();
1633	}
1634
1635	/**
1636	 * Check if cache is enabled for the current model
1637	 * @param boolean $checkId check if there is model ID
1638	 * @return boolean
1639	 */
1640	public function isCacheEnabled($checkId = true)
1641	{
1642		return (null !== $this->getCacheString() && (!$checkId || $this->getId()));
1643	}
1644
1645	/**
1646	 * Get model cache string
1647	 * @param boolean $replace try to add current model ID (replace destination is {ID})
1648	 * @return string
1649	 */
1650	public function getCacheString($replace = false)
1651	{
1652		return ($replace ? str_replace('{ID}', $this->getId(), $this->_cache_string) : $this->_cache_string);
1653	}
1654
1655	/**
1656	 * Set model cache string
1657	 * @param string $str
1658	 * @return e_model
1659	 */
1660	public function setCacheString($str)
1661	{
1662		$this->_cache_string = $str;
1663		return $this;
1664	}
1665
1666    /**
1667     * Save data to DB
1668     * Awaiting for child class implementation
1669     * @see e_model_admin
1670     */
1671    public function save()
1672    {
1673    }
1674
1675    /**
1676     * Delete DB record
1677     * Awaiting for child class implementation
1678     * @see e_model_admin
1679     */
1680    public function delete($ids, $destroy = true, $session_messages = false)
1681    {
1682    }
1683
1684    /**
1685     * Create new DB recorrd
1686     * Awaiting for child class implementation
1687     * @see e_model_admin
1688     */
1689    public function create()
1690    {
1691    }
1692
1693    /**
1694     * Insert data to DB
1695     * Awaiting for child class implementation
1696     * @see e_model_admin
1697     */
1698    protected function dbInsert()
1699    {
1700    }
1701
1702    /**
1703     * Update DB data
1704     * Awaiting for child class implementation
1705     * @see e_model_admin
1706     */
1707    protected function dbUpdate($force = false, $session_messages = false)
1708    {
1709    }
1710
1711    /**
1712     * Replace DB record
1713     * Awaiting for child class implementation
1714     * @see e_model_admin
1715     */
1716    protected function dbReplace()
1717    {
1718    }
1719
1720    /**
1721     * Delete DB data
1722     * Awaiting for child class implementation
1723     * @see e_model_admin
1724     */
1725    protected function dbDelete()
1726    {
1727    }
1728
1729	/**
1730	 * Set parameter array
1731	 * Core implemented:
1732	 * - db_query: string db query to be passed to load() ($sql->gen())
1733	 * - db_query
1734	 * - db_fields
1735	 * - db_where
1736	 * - db_debug
1737	 * - model_class: e_tree_model class/subclasses - string class name for creating nodes inside default load() method
1738	 * - clearModelCache: e_tree_model class/subclasses - clear cache per node after successful DB operation
1739	 * - noCacheStringModify: e_tree_model class/subclasses - do not add additional md5 sum to tree cache string
1740	 * @param array $params
1741	 * @return e_model|e_tree_model
1742	 */
1743	public function setParams(array $params)
1744	{
1745		parent::setParams($params);
1746		return $this;
1747	}
1748
1749
1750
1751	/**
1752	 * Render model data, all 'sc_*' methods will be recongnized
1753	 * as shortcodes.
1754	 *
1755	 * @param string $template
1756	 * @param boolean $parsesc parse external shortcodes, default is true
1757	 * @param e_vars $eVars simple parser data
1758	 * @return string parsed template
1759	 */
1760	public function toHTML($template, $parsesc = true, $eVars = null)
1761	{
1762		return e107::getParser()->parseTemplate($template, $parsesc, $this, $eVars);
1763	}
1764
1765	/**
1766	 * Export a Model configuration
1767	 * @return string
1768	 */
1769	public function toXML()
1770	{
1771		$ret = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
1772		$ret .= "<e107Export type=\"model\" version=\"1.0\" timestamp=\"".time()."\" >\n";
1773
1774		$ret .= "\t<data>\n";
1775		// TODO - handle multi dimensional arrays (already possible - field1/field2?), method toXMLValue($value, $type)
1776		foreach ($this->getDataFields() as $field => $type)
1777		{
1778			$ret .= "\t\t<field name=\"{$field}\" type=\"{$type}\">";
1779			$ret .= $type == 'str' || $type == 'string' ? "<![CDATA[".$this->getData($field)."]]>" : $this->getData($field);
1780			$ret .= "</field>\n";
1781		}
1782		$ret .= "\t</data>\n";
1783
1784		$ret .= "</e107Export>";
1785		return $ret;
1786	}
1787
1788	/**
1789	 * Try to convert string to a number
1790	 * Shoud fix locale related troubles
1791	 *
1792	 * @param string $value
1793	 * @return integer|float
1794	 */
1795	// moved to e_parse
1796	// public function toNumber($value)
1797	// {
1798	// 	$larr = localeconv();
1799	// 	$search = array(
1800	// 		$larr['decimal_point'],
1801	// 		$larr['mon_decimal_point'],
1802	// 		$larr['thousands_sep'],
1803	// 		$larr['mon_thousands_sep'],
1804	// 		$larr['currency_symbol'],
1805	// 		$larr['int_curr_symbol']
1806	// 	);
1807	// 	$replace = array('.', '.', '', '', '', '');
1808
1809	// 	return str_replace($search, $replace, $value);
1810	// }
1811
1812	/**
1813	 * Convert object data to a string
1814	 *
1815	 * @param boolean $AddSlashes
1816	 * @param string $key optional, if set method will return corresponding value as a string
1817	 * @return string
1818	 */
1819	public function toString($AddSlashes = true, $key = null)
1820	{
1821		if (null !== $key)
1822		{
1823			$value = $this->getData($key);
1824			if(is_array($value))
1825			{
1826				return e107::getArrayStorage()->WriteArray($value, $AddSlashes);
1827			}
1828			return (string) $value;
1829		}
1830		return (string) e107::getArrayStorage()->WriteArray($this->toArray(), $AddSlashes);
1831	}
1832
1833	public function destroy()
1834	{
1835		$this->_data = array();
1836		$this->_params = array();
1837		$this->_data_fields = array();
1838		$this->_parsed_keys = array();
1839		$this->_db_table = $this->_field_id = '';
1840		$this->data_has_changed = false;
1841	}
1842
1843	/**
1844	 * Disable Magic setter
1845	 */
1846	public function __set($key, $value)
1847	{
1848	}
1849
1850	/**
1851	 * Disable Magic getter
1852	 */
1853	public function __get($key)
1854	{
1855	}
1856
1857	/**
1858	 * Disable
1859	 */
1860	public function __isset($key)
1861	{
1862	}
1863
1864	/**
1865	 * Disable
1866	 */
1867	public function __unset($key)
1868	{
1869	}
1870}
1871
1872/**
1873 * Base e107 Front Model class interface
1874 *
1875 * Some important points:
1876 * - model data should be always in toDB() format:
1877 * 		- retrieved direct from DB
1878 * 		- set & sanitized via setPostedData()->mergePostedData()
1879 * 		- manually sanitized before passed to model setter (set(), setData(), add(), addData(), etc.) methods
1880 * - $_data_fields property is important, it tells to sanitize() method how to sanitize posted data
1881 * - if $_data_fields is missing, sanitize() will call internally e107::getParser()->toDB() on the data
1882 * - sanitize() is triggered by default on mergePostedData() and mergeData() methods
1883 * - mergePostedData() and mergeData() methods will filter posted/passed data against (in this order):
1884 * 		- getValidator()->getValidData() if true is passed as validate parameter (currently disabled, gather feedback)
1885 * 		- $_data_fields if true is passed as sanitize parameter
1886 * - toSqlQuery() needs $_data_fields and $_field_id to work proper, $_FIELD_TYPES is optional but recommended (faster SQL queries)
1887 * - result array from toSqlQuery() call will be filtered against $_data_fields
1888 * - 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)
1889 * - most probably $_FIELD_TYPES will go in the future, $_data_fields alone could do the job
1890 * - default db related methods (save(), dbUpdate(), etc.) need $_db_table
1891 *
1892 * @package e107
1893 * @category e107_handlers
1894 * @version $Id$
1895 * @author SecretR
1896 * @copyright Copyright (C) 2008-2010 e107 Inc.
1897 */
1898class e_front_model extends e_model
1899{
1900    /**
1901    * Posted data
1902    * Back-end related
1903    *
1904    * @var array
1905    */
1906    protected $_posted_data = array();
1907
1908
1909    /**
1910     * DB format array - see db::_getTypes() and db::_getFieldValue() (mysql_class.php)
1911     * for example
1912     *
1913     * This can/should be overwritten by extending the class
1914     *
1915     * @var array
1916     */
1917    protected $_FIELD_TYPES = array();
1918
1919    /**
1920     * Validation structure - see {@link e_validator::$_required_rules} for
1921     * more information about the array format.
1922     * Used in {@link validate()} method.
1923     * TODO - check_rules (see e_validator::$_optional_rules)
1924     * This can/should be overwritten by extending the class.
1925     *
1926     * @var array
1927     */
1928    protected $_validation_rules = array();
1929
1930    protected $_optional_rules = array();
1931
1932	/**
1933	 * @var integer Last SQL error number
1934	 */
1935	protected $_db_errno = 0;
1936
1937	/**
1938	 * @var string Last SQL error message
1939	 */
1940	protected $_db_errmsg = '';
1941
1942	/**
1943	 * @var string Last SQL query
1944	 */
1945	protected $_db_qry = '';
1946
1947    /**
1948     * Validator object
1949     *
1950     * @var e_validator
1951     */
1952    protected $_validator = null;
1953
1954    /**
1955     * @return array
1956     */
1957    public function getValidationRules()
1958    {
1959    	return $this->_validation_rules;
1960    }
1961
1962    /**
1963     * Set object validation rules if $_validation_rules array is empty
1964     *
1965     * @param array $vrules
1966     * @return e_front_model
1967     */
1968    public function setValidationRules(array $vrules, $force = false)
1969    {
1970    	if($force || empty($this->_validation_rules))
1971    	{
1972    		$this->_validation_rules = $vrules;
1973    	}
1974    	return $this;
1975    }
1976
1977    /**
1978     * @return array
1979     */
1980    public function getOptionalRules()
1981    {
1982    	return $this->_optional_rules;
1983    }
1984
1985    /**
1986     * @param array $rules
1987     * @return e_front_model
1988     */
1989    public function setOptionalRules(array $rules)
1990    {
1991    	$this->_optional_rules = $rules;
1992    	return $this;
1993    }
1994
1995    /**
1996     * Set object validation rules if $_validation_rules array is empty
1997     *
1998     * @param string $field
1999     * @param array $rule
2000     * @param boolean $required
2001     * @return e_front_model
2002     */
2003    public function setValidationRule($field, $rule, $required = true)
2004    {
2005    	$pname = $required ? '_validation_rules' : '_optional_rules';
2006    	$rules = &$this->$pname;
2007    	$rules[$field] = $rule;
2008
2009    	return $this;
2010    }
2011
2012    /**
2013     * Predefined data fields types, passed to DB handler
2014     * @return array
2015     */
2016    public function getDbTypes()
2017    {
2018    	return ($this->_FIELD_TYPES ? $this->_FIELD_TYPES : $this->getDataFields());
2019    }
2020
2021    /**
2022     * Predefined data fields types, passed to DB handler
2023     *
2024     * @param array $field_types
2025     * @return e_front_model
2026     */
2027    public function setDbTypes($field_types)
2028    {
2029    	$this->_FIELD_TYPES = $field_types;
2030		return $this;
2031    }
2032
2033    /**
2034     * Auto field type definitions
2035     * Disabled for now, it should auto-create _data_types
2036     * @param boolean $force
2037     * @return boolean
2038     */
2039//	public function setFieldTypeDefs($force = false)
2040//	{
2041//		if($force || !$this->getFieldTypes())
2042//		{
2043//			$ret = e107::getDb()->getFieldDefs($this->getModelTable());
2044//			if($ret)
2045//			{
2046//				foreach ($ret as $k => $v)
2047//				{
2048//					if('todb' == $v)
2049//					{
2050//						$ret[$k] = 'string';
2051//					}
2052//				}
2053//				$this->setFieldTypes($ret);
2054//				return true;
2055//			}
2056//		}
2057//		return false;
2058//	}
2059
2060    /**
2061     * Retrieves data from the object ($_posted_data) without
2062     * key parsing (performance wise, prefered when possible)
2063     *
2064     * @see _getDataSimple()
2065     * @param string $key
2066     * @param mixed $default
2067     * @return mixed
2068     */
2069	public function getPosted($key, $default = null)
2070    {
2071    	return $this->_getDataSimple((string) $key, $default, '_posted_data');
2072    }
2073
2074    /**
2075     * Retrieves data from the object ($_posted_data)
2076     * If $key is empty, return all object posted data
2077     * @see _getData()
2078     * @param string $key
2079     * @param mixed $default
2080     * @param integer $index
2081     * @return mixed
2082     */
2083	public function getPostedData($key = '', $default = null, $index = null)
2084    {
2085    	return $this->_getData($key, $default, $index, '_posted_data');
2086    }
2087
2088    /**
2089     * Search for requested data from available sources in this order:
2090     * - posted data
2091     * - default object data
2092     * - passed default value
2093     *
2094     * Use this method inside forms
2095     *
2096     * @param string $key
2097     * @param string $default
2098     * @param integer $index
2099     * @return string
2100     */
2101    public function getIfPosted($key, $default = '', $index = null)
2102    {
2103    	$d = $this->getDataFields();
2104
2105		if(!empty($d[$key]) && ($d[$key] == 'array'))
2106		{
2107			return e107::unserialize($this->getData((string) $key, $default, $index));
2108		}
2109
2110    	$posted = $this->getPostedData((string) $key, null, $index);
2111		if(null !== $posted)
2112		{
2113			// FIXED - double post_toFom() and toDB(post_toForm()) problems
2114			// setPosted|setPostedData|addPostedData methods are storing RAW data now
2115			return e107::getParser()->post_toForm($posted);
2116		}
2117		return e107::getParser()->toForm($this->getData((string) $key, $default, $index));
2118    }
2119
2120    /**
2121     * Overwrite posted data in the object for a single field. Key is not parsed.
2122     * Public proxy of {@link _setDataSimple()}
2123     * Use this method to store data from non-trustable sources (e.g. _POST) - it doesn't overwrite
2124     * the original object data
2125     *
2126     * @param string $key
2127     * @param mixed $value
2128     * @param boolean $strict update only
2129     * @return e_front_model
2130     */
2131    public function setPosted($key, $value, $strict = false)
2132    {
2133        return $this->_setDataSimple($key, $value, $strict, '_posted_data');
2134    }
2135
2136    /**
2137     * Overwrite posted data in the object. Key is parsed (multidmensional array support).
2138     * Public proxy of {@link _setData()}
2139     * Use this method to store data from non-trustable sources (e.g. _POST) - it doesn't overwrite
2140     * the original object data
2141     *
2142     * @param string|array $key
2143     * @param mixed $value
2144     * @param boolean $strict update only
2145     * @return e_front_model
2146     */
2147    public function setPostedData($key, $value = null, $strict = false)
2148    {
2149        return $this->_setData($key, $value, $strict, '_posted_data');
2150    }
2151
2152    /**
2153     * Add data to the object.
2154     * Retains existing data in the object.
2155     * Public proxy of {@link _addData()}
2156     *
2157     * If $override is false, data will be updated only (check against existing data)
2158     *
2159     * @param string|array $key
2160     * @param mixed $value
2161     * @param boolean $override override existing data
2162     * @return e_front_model
2163     */
2164    public function addPostedData($key, $value = null, $override = true)
2165    {
2166    	return $this->_addData($key, $value, $override, '_posted_data');
2167    }
2168
2169	/**
2170     * Unset single posted data field from the object.
2171     * Public proxy of {@link _unsetDataSimple()}
2172     *
2173     * @param string $key
2174     * @return e_front_model
2175     */
2176    public function removePosted($key)
2177    {
2178    	return $this->_unsetDataSimple($key, '_posted_data');
2179    }
2180
2181	/**
2182     * Unset posted data from the object.
2183     * $key can be a string only. Array will be ignored.
2184     * '/' inside the key will be treated as array path
2185     * if $key is null entire object will be reset
2186     *
2187     * Public proxy of {@link _unsetData()}
2188     *
2189     * @param string|null $key
2190     * @return e_front_model
2191     */
2192    public function removePostedData($key = null)
2193    {
2194    	return $this->_unsetData($key, '_posted_data');
2195    }
2196
2197    /**
2198     * Check if given key exists and non-empty in the posted data array
2199     * @param string $key
2200     * @return boolean
2201     */
2202    public function hasPosted($key)
2203    {
2204    	return $this->_hasData($key, '_posted_data');
2205    }
2206
2207	/**
2208	 * Check if posted data is empty
2209	 * @return boolean
2210	 */
2211    public function hasPostedData()
2212    {
2213    	return $this->_hasData('', '_posted_data');
2214    }
2215
2216    /**
2217     * Check if given key exists in the posted data array
2218     *
2219     * @param string $key
2220     * @return boolean
2221     */
2222    public function isPosted($key)
2223    {
2224    	return (isset($this->_posted_data[$key]));
2225    }
2226
2227    /**
2228     * Check if given key exists in the posted data array ($key us parsed)
2229     *
2230     * @param string $key
2231     * @return boolean
2232     */
2233    public function isPostedData($key)
2234    {
2235    	return $this->_isData($key, '_posted_data');
2236    }
2237
2238    /**
2239     * Compares posted data vs object data
2240     *
2241     * @param string $field
2242     * @param boolean $strict compare variable type as well
2243     * @return boolean
2244     */
2245    public function dataHasChangedFor($field, $strict = false)
2246    {
2247        $newData = $this->getData($field);
2248        $postedData = $this->getPostedData($field);
2249        return ($strict ? $newData !== $postedData : $newData != $postedData);
2250    }
2251
2252    /**
2253     * @return boolean
2254     */
2255    public function dataHasChanged()
2256    {
2257        return $this->data_has_changed;
2258    }
2259
2260    /**
2261     * Merge posted data with the object data
2262     * Should be used on edit/update/create record (back-end)
2263     * Retrieved for copy Posted data will be removed (no matter if copy is successfull or not)
2264     *
2265     * If $strict is true, only existing object data will be copied (update)
2266     * If $validate is true, data will be copied only after successful validation
2267     *
2268     * @param boolean $strict
2269     * @param boolean $sanitize sanitize posted data before move it to the object data
2270     * @param boolean $validate perform validation check
2271     * @return e_front_model
2272     */
2273    public function mergePostedData($strict = true, $sanitize = true, $validate = true)
2274    {
2275    	if(!$this->hasPostedData() || ($validate && !$this->validate()))
2276    	{
2277    		return $this;
2278    	}
2279
2280
2281		$oldData = $this->getData();
2282//		$this->addMessageDebug("OLDD".print_a($oldData,true));
2283
2284
2285		$data = $this->getPostedData();
2286
2287		$valid_data = $validate ? $this->getValidator()->getValidData() : array();
2288
2289		if($sanitize)
2290		{
2291			// search for db_field types
2292			if($this->getDataFields())
2293			{
2294				$data = $this->sanitize($data);
2295			}
2296			else //no db field types, use toDB()
2297			{
2298				$data = e107::getParser()->toDB($data);
2299			}
2300		}
2301
2302	//	$newData = $this->getPostedData();
2303		e107::getAdminLog()->addArray($data,$oldData);
2304	//	$this->addMessageDebug("NEWD".print_a($data,true));
2305
2306		$tp = e107::getParser();
2307    	foreach ($data as $field => $dt)
2308    	{
2309    		// get values form validated array when possible
2310    		// we need it because of advanced validation methods e.g. 'compare'
2311    		// FIX - security issue, toDb required
2312    		if(isset($valid_data[$field])) $dt = $tp->toDb($valid_data[$field]);
2313
2314    		$this->setData($field, $dt, $strict)
2315    			->removePostedData($field);
2316    	}
2317
2318
2319
2320
2321    	return $this;
2322    }
2323
2324    /**
2325     * Merge passed data array with the object data
2326     * Should be used on edit/update/create record (back-end)
2327     *
2328     * If $strict is true, only existing object data will be copied (update)
2329     * If $validate is true, data will be copied only after successful validation
2330     *
2331     * @param array $src_data
2332     * @param boolean $sanitize
2333     * @param boolean $validate perform validation check
2334     * @return e_front_model
2335     */
2336    public function mergeData(array $src_data, $strict = true, $sanitize = true, $validate = true)
2337    {
2338    	//FIXME
2339    	if(!$src_data || ($validate && !$this->validate($src_data)))
2340    	{
2341    		return $this;
2342    	}
2343
2344		/* Wrong?
2345		// retrieve only valid data
2346		if($validate)
2347		{
2348			$src_data = $this->getValidator()->getValidData();
2349		}*/
2350
2351		if($sanitize)
2352		{
2353			// search for db_field types
2354			if($this->getDataFields())
2355			{
2356				$src_data = $this->sanitize($src_data);
2357			}
2358			else //no db field types, use toDB()
2359			{
2360				$src_data = e107::getParser()->toDB($src_data);
2361			}
2362		}
2363
2364
2365
2366		foreach ($src_data as $key => $value)
2367		{
2368			$this->setData($key, $value, $strict);
2369		}
2370
2371
2372
2373    	return $this;
2374    }
2375
2376    /**
2377     * Validate posted data:
2378     * 1. validate posted data against object validation rules
2379     * 2. add validation errors to the object if any
2380     * 3. return true for valid and false for non-valid data
2381     *
2382     * @param array $data optional - data for validation, defaults to posted data
2383     * @return boolean
2384     */
2385    public function validate($data = null)
2386    {
2387    	if(!$this->getValidationRules())
2388    	{
2389    		return true;
2390    	}
2391		if(null === $data)
2392		{
2393			$data = $this->getPostedData();
2394		}
2395
2396		// New param to control validate process - useful when part of the data is going to be updated
2397		// Use it with cautious!!!
2398		$availableOnly = false;
2399		if($this->getParam('validateAvailable'))
2400		{
2401			$availableOnly = true;
2402			$this->setParam('validateAvailable', null); // reset it
2403		}
2404
2405		return $this->getValidator()->validate($data, $availableOnly);
2406    }
2407
2408    /**
2409     * User defined model validation
2410     * Awaiting for child class implementation
2411     *
2412     */
2413    public function verify()
2414    {
2415    }
2416
2417	/**
2418	 * @return e_validator
2419	 */
2420	public function getValidator()
2421	{
2422		if(null === $this->_validator)
2423		{
2424			$this->_validator = e107::getObject('e_validator');
2425			$this->_validator->setRules($this->getValidationRules())
2426				->setOptionalRules($this->getOptionalRules())
2427				->setMessageStack($this->_message_stack.'_validator');
2428			//TODO - optional check rules
2429		}
2430		return $this->_validator;
2431	}
2432
2433	/**
2434	 * Add custom validation message.
2435	 * $field_type and $error_code will be inserted via $tp->lanVars()
2436	 * in the $message string
2437	 * Example:
2438	 * <code>
2439	 * $model->addValidationError('Custom error message [#%d] for %s', 'My Field', 1000);
2440	 * //produces 'Custom error message [#1000] for My Field'
2441	 * </code>
2442	 *
2443	 * @param string $message
2444	 * @param string $field_title [optional]
2445	 * @param integer $error_code [optional]
2446	 * @return object
2447	 */
2448	public function addValidationError($message, $field_title = '', $error_code = 0)
2449	{
2450		$this->getValidator()->addValidateMessage($field_title, $error_code, $message)->setIsValidData(false);
2451		return $this;
2452	}
2453
2454    /**
2455     * Render validation errors (if any)
2456     *
2457     * @param boolean $session store messages to session
2458     * @param boolean $reset reset errors
2459     * @return string
2460     */
2461    public function renderValidationErrors($session = false, $reset = true)
2462    {
2463		return $this->getValidator()->renderValidateMessages($session, $reset);
2464    }
2465
2466    /**
2467     * Render System messages (if any)
2468     *
2469     * @param boolean $validation render validation messages as well
2470     * @param boolean $session store messages to session
2471     * @param boolean $reset reset errors
2472     * @return string
2473     */
2474    public function renderMessages($validation = true, $session = false, $reset = true)
2475    {
2476    	if($validation)
2477		{
2478			e107::getMessage()->moveStack($this->_message_stack.'_validator', $this->_message_stack, false, $session);
2479		}
2480		return parent::renderMessages($session, $reset);
2481    }
2482
2483    /**
2484     * Move model System messages (if any) to the default eMessage stack
2485     *
2486     * @param boolean $session store messages to session
2487     * @param boolean $validation move validation messages as well
2488     * @return e_front_model
2489     */
2490    public function setMessages($session = false, $validation = true)
2491    {
2492    	if($validation)
2493		{
2494			e107::getMessage()->moveStack($this->_message_stack.'_validator', 'default', false, $session);
2495		}
2496    	parent::setMessages($session);
2497		return $this;
2498    }
2499
2500    /**
2501     * Reset model System messages
2502     *
2503     * @param boolean|string $type E_MESSAGE_INFO | E_MESSAGE_SUCCESS | E_MESSAGE_WARNING | E_MESSAGE_WARNING | E_MESSAGE_DEBUG | false (all)
2504     * @param boolean $session reset session messages
2505     * @param boolean $validation reset validation messages as well
2506     * @return e_front_model
2507     */
2508    public function resetMessages($type = false, $session = false, $validation = false)
2509    {
2510        if($validation)
2511		{
2512			e107::getMessage()->reset($type, $this->_message_stack.'_validator', $session);
2513		}
2514    	parent::resetMessages($type, $session);
2515		return $this;
2516    }
2517
2518    /**
2519     * @return boolean
2520     */
2521    public function hasValidationError()
2522    {
2523    	return $this->getValidator()->isValid();
2524    }
2525
2526    /**
2527     * @return boolean
2528     */
2529    public function hasSqlError()
2530    {
2531    	return !empty($this->_db_errno);
2532    }
2533
2534    /**
2535     * @return integer last mysql error number
2536     */
2537    public function getSqlErrorNumber()
2538    {
2539    	return $this->_db_errno;
2540    }
2541
2542    /**
2543     * @return string last mysql error message
2544     */
2545    public function getSqlError()
2546    {
2547    	return $this->_db_errmsg;
2548    }
2549
2550    /**
2551     * @return string last mysql error message
2552     */
2553    public function getSqlQuery()
2554    {
2555    	return $this->_db_qry;
2556    }
2557
2558    /**
2559     * @return boolean
2560     */
2561    public function hasError()
2562    {
2563    	return ($this->hasValidationError() || $this->hasSqlError());
2564    }
2565
2566    /**
2567     * Generic load data from DB
2568     * @param boolean $force
2569     * @return e_front_model
2570     */
2571	public function load($id=null, $force = false)
2572	{
2573		parent::load($id, $force);
2574
2575		$sql = e107::getDb();
2576		$this->_db_errno = $sql->getLastErrorNumber();
2577		$this->_db_errmsg = $sql->getLastErrorText();
2578		$this->_db_qry = $sql->getLastQuery();
2579
2580		if($this->_db_errno)
2581		{
2582			$data = array(
2583				'TABLE'     => $this->getModelTable(),
2584				'error_no'  => $this->_db_errno,
2585				'error_msg' => $this->_db_errmsg,
2586				'qry'       => $this->_db_qry,
2587				'url'       => e_REQUEST_URI,
2588			);
2589
2590
2591			$this->addMessageError('SQL Select Error', false, $data); //TODO - Lan
2592			// already done by the parent
2593			//$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText());
2594		}
2595
2596
2597		return $this;
2598	}
2599
2600
2601	/**
2602	 * Build query array to be used with db methods (db_Update, db_Insert, db_Replace)
2603	 *
2604	 * @param string $force [optional] force action - possible values are create|update|replace
2605	 * @return array db query
2606	 */
2607	public function toSqlQuery($force = '')
2608	{
2609		$qry = array();
2610
2611		if($force)
2612		{
2613			$action = $force;
2614		}
2615		else
2616		{
2617			$action = $this->getId() ? 'update' : 'create';
2618		}
2619
2620		$qry['_FIELD_TYPES'] = $this->_FIELD_TYPES; //DB field types are optional
2621
2622		// support for tables with no auto-increment PK
2623		$id = $this->getId();
2624		$qry['data'][$this->getFieldIdName()] = $id;
2625
2626		//XXX This check is done in _setModel() of admin-ui.  NULL below will break MySQL strict.
2627		// Allow admin config to specify the best data type.
2628		/*
2629		if($action == 'create' && !$id) $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'NULL';
2630		elseif(is_numeric($id)) $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'integer';
2631		else $qry['_FIELD_TYPES'][$this->getFieldIdName()] = 'string';
2632		*/
2633
2634		foreach ($this->_data_fields as $key => $type)
2635		{
2636
2637			if(!isset($qry['_FIELD_TYPES'][$key]))
2638			{
2639				$qry['_FIELD_TYPES'][$key] = $type; //_FIELD_TYPES much more optional now...
2640			}
2641
2642			if($qry['_FIELD_TYPES'][$key] == 'set') //new 'set' type, could be moved in mysql handler now
2643			{
2644				$qry['_FIELD_TYPES'][$key] = 'str';
2645				if(is_array($this->getData($key)))	$this->setData($key, implode(',', $this->getData($key)));
2646			}
2647			$qry['data'][$key] = $this->getData($key);
2648
2649		}
2650
2651		switch($action)
2652		{
2653			case 'create':
2654				//$qry['data'][$this->getFieldIdName()] = NULL;
2655			break;
2656			case 'replace':
2657				$qry['_REPLACE'] = true;
2658			break;
2659
2660			case 'update':
2661				unset($qry['data'][$this->getFieldIdName()]);
2662				if(is_numeric($id)) $id = intval($id);
2663				else $id = "'".e107::getParser()->toDB($id)."'";
2664				$qry['WHERE'] = $this->getFieldIdName().'='.$id;
2665			break;
2666		}
2667
2668		if(E107_DEBUG_LEVEL == E107_DBG_SQLQUERIES)
2669		{
2670			$this->addMessageDebug('SQL Qry: '.print_a($qry,true), null);
2671		}
2672		return $qry;
2673	}
2674
2675	/**
2676	 * Sanitize value based on its db field type ($_data_fields),
2677	 * method will return null only if db field rule is not found.
2678	 * If $value is null, it'll be retrieved from object posted data
2679	 * If $key is an array, $value is omitted.
2680	 *
2681	 * NOTE: If $key is not found in object's _data_fields array, null is returned
2682	 *
2683	 * @param mixed $key string key name or array data to be sanitized
2684	 * @param mixed $value
2685	 * @return mixed sanitized $value or null on failure
2686	 */
2687	public function sanitize($key, $value = null)
2688	{
2689		$tp = e107::getParser();
2690		if(is_array($key))
2691		{
2692			$ret = array();
2693			foreach ($key as $k=>$v)
2694			{
2695	            if(isset($this->_data_fields[$k]))
2696	            {
2697	               $ret[$k] = $this->sanitize($k, $v);
2698	            }
2699			}
2700			return $ret;
2701		}
2702
2703		if(!isset($this->_data_fields[$key]))
2704		{
2705			return null;
2706		}
2707		$type =  $this->_data_fields[$key];
2708		if(null === $value)
2709		{
2710			$value = $this->getPostedData($key);
2711		}
2712
2713
2714		switch ($type)
2715		{
2716			case 'int':
2717			case 'integer':
2718				//return intval($this->toNumber($value));
2719				return intval($tp->toNumber($value));
2720			break;
2721
2722			case 'safestr':
2723				return $tp->filter($value);
2724			break;
2725
2726			case 'str':
2727			case 'string':
2728			case 'array':
2729				$type = $this->getFieldInputType($key);
2730				return $tp->toDB($value, false, false, 'model', array('type'=>$type, 'field'=>$key));
2731			break;
2732
2733			case 'json':
2734				if(empty($value))
2735				{
2736					return null;
2737				}
2738				return e107::serialize($value,'json');
2739			break;
2740
2741			case 'code':
2742				return $tp->toDB($value, false, false, 'pReFs');
2743			break;
2744
2745			case 'float':
2746				// return $this->toNumber($value);
2747				return $tp->toNumber($value);
2748			break;
2749
2750			case 'bool':
2751			case 'boolean':
2752				return ($value ? true : false);
2753			break;
2754
2755			case 'model':
2756				return $value->mergePostedData(false, true, true);
2757			break;
2758
2759			case 'null':
2760				return ($value ? $tp->toDB($value) : null);
2761			break;
2762	  	}
2763
2764		return null;
2765	}
2766
2767
2768
2769	public function destroy()
2770	{
2771		parent::destroy();
2772		$this->_validator = null;
2773		$this->_validation_rules = array();
2774		$this->_db_errno = null;
2775		$this->_posted_data = array();
2776		$this->data_has_changed = array();
2777		$this->_FIELD_TYPES = array();
2778	}
2779
2780
2781    /**
2782     * Update DB data
2783     *
2784     * @param boolean $force force query even if $data_has_changed is false
2785     * @param boolean $session_messages to use or not session to store system messages
2786     */
2787    protected function dbUpdate($force = false, $session_messages = false)
2788    {
2789        $this->_db_errno = 0;
2790		$this->_db_errmsg = '';
2791		$this->_db_qry = '';
2792
2793	//	 $this->getData();
2794	//	 $this->getPostedData();
2795
2796
2797		if($this->hasError()) return false;
2798
2799		if(!$this->data_has_changed && $force === false)
2800		{
2801			$this->addMessageInfo(LAN_NO_CHANGE);
2802			return 0;
2803		}
2804
2805		$sql = e107::getDb();
2806		$qry = $this->toSqlQuery('update');
2807		$table = $this->getModelTable();
2808
2809		$res = $sql->update($table, $qry, $this->getParam('db_debug', false));
2810        $this->_db_qry = $sql->getLastQuery();
2811		if(!$res)
2812		{
2813			$this->_db_errno = $sql->getLastErrorNumber();
2814			$this->_db_errmsg = $sql->getLastErrorText();
2815
2816			if($this->_db_errno)
2817			{
2818				$data = array(
2819					'TABLE'     => $table,
2820					'error_no' => $this->_db_errno,
2821					'error_msg' => $this->_db_errmsg,
2822					'qry'       => $this->_db_qry,
2823					'url'       => e_REQUEST_URI,
2824				);
2825
2826				$this->addMessageError('SQL Update Error', $session_messages, $data); //TODO - Lan
2827				$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText());
2828				return false;
2829			}
2830
2831			if($force === false)
2832			{
2833				$this->addMessageInfo(LAN_NO_CHANGE);
2834			}
2835			else
2836			{
2837				$this->addMessageDebug(LAN_NO_CHANGE);
2838			}
2839
2840
2841			return 0;
2842		}
2843		$this->clearCache()->addMessageSuccess(LAN_UPDATED. " #".$this->getId());
2844
2845		e107::getAdminLog()->addSuccess('TABLE: '.$table, false);
2846		e107::getAdminLog()->addSuccess('WHERE: '.$qry['WHERE'], false);
2847		e107::getAdminLog()->save('ADMINUI_02');
2848
2849
2850		return $res;
2851    }
2852
2853    /**
2854     * Save data to DB
2855     *
2856     * @param boolen $from_post
2857     * @return boolean|integer
2858     */
2859    public function save($from_post = true, $force = false, $session_messages = false)
2860    {
2861    	if(!$this->getFieldIdName())
2862		{
2863			return false;
2864		}
2865
2866		if($from_post)
2867		{
2868			//no strict copy, validate & sanitize
2869			$this->mergePostedData(false, true, true);
2870		}
2871
2872		if($this->getId())
2873		{
2874			return $this->dbUpdate($force, $session_messages);
2875		}
2876
2877		return false;
2878    }
2879
2880    /**
2881     * Update record
2882     * @see save()
2883     * @param boolen $from_post
2884     * @return boolean|integer
2885     *//*
2886    public function update($from_post = true, $force = false, $session_messages = false)
2887    {
2888    	if(!$this->getFieldIdName())
2889		{
2890			return false;
2891		}
2892
2893		if($from_post)
2894		{
2895			//no strict copy, validate & sanitize
2896			$this->mergePostedData(false, true, true);
2897		}
2898
2899		return $this->dbUpdate($force, $session_messages);
2900    }*/
2901
2902    /**
2903     * Exactly what it says - your debug helper
2904     * @param boolean $retrun
2905     * @param boolean $undo
2906     * @return void
2907     */
2908	public function saveDebug($return = false, $undo = true)
2909	{
2910		$ret = array();
2911
2912		$ret['validation_rules'] = $this->getValidationRules();
2913		$ret['optional_validation_rules'] = $this->getOptionalRules();
2914		$ret['model_base_ismodfied'] = $this->isModified();
2915		$ret['model_base_data'] = $this->getData();
2916		$ret['posted_data'] = $this->getPostedData();
2917
2918		$this->mergePostedData(false, true, true);
2919
2920		$ret['model_modified_data'] = $this->getData();
2921		$ret['model_modified_ismodfied'] = $this->isModified();
2922		$ret['validator_valid_data'] = $this->getValidator()->getValidData();
2923
2924		// undo
2925		if($undo)
2926		{
2927			$this->setData($ret['model_base_data'])
2928				->isModified($ret['model_base_ismodfied'])
2929				->setPostedData($ret['posted_data']);
2930		}
2931		if($return) return $ret;
2932
2933		print_a($ret);
2934	}
2935}
2936
2937//FIXME - move e_model_admin to e_model_admin.php
2938
2939/**
2940 * Base e107 Admin Model class
2941 *
2942 * @package e107
2943 * @category e107_handlers
2944 * @version $Id$
2945 * @author SecretR
2946 * @copyright Copyright (C) 2008-2010 e107 Inc.
2947 */
2948class e_admin_model extends e_front_model
2949{
2950    /**
2951     * Save data to DB
2952     *
2953     * @param boolen $from_post
2954     */
2955    public function save($from_post = true, $force = false, $session_messages = false)
2956    {
2957    	if(!$this->getFieldIdName())
2958		{
2959			return false;
2960		}
2961
2962		if($from_post)
2963		{
2964			//no strict copy, validate & sanitize
2965			$this->mergePostedData(false, true, true);
2966		}
2967
2968		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
2969		{
2970			return $this->dbUpdate($force, $session_messages);
2971		}
2972
2973		return $this->dbInsert($session_messages);
2974    }
2975
2976    /**
2977     * Insert record
2978     *
2979     * @param boolen $from_post
2980	 * @param boolean $session_messages
2981	 * @return integer inserted ID or false on error
2982     */
2983    public function insert($from_post = true, $session_messages = false)
2984    {
2985		if($from_post)
2986		{
2987			//no strict copy, validate & sanitize
2988			$this->mergePostedData(false, true, true);
2989		}
2990
2991		return $this->dbInsert($session_messages);
2992    }
2993
2994	public function delete($ids, $destroy = true, $session_messages = false)
2995	{
2996		$ret = $this->dbDelete();
2997		if($ret)
2998		{
2999			if($destroy)
3000			{
3001				$this->setMessages($session_messages)->destroy();
3002			}
3003		}
3004		return $ret;
3005	}
3006
3007    /**
3008     * Insert data to DB
3009     * @param boolean $session_messages to use or not session to store system messages
3010     * @return integer
3011     */
3012    protected function dbInsert($session_messages = false)
3013    {
3014        $this->_db_errno = 0;
3015        $this->_db_errmsg = '';
3016		$this->_db_qry = '';
3017		if($this->hasError()/* || (!$this->data_has_changed && !$force)*/) // not appropriate here!
3018		{
3019			return false;
3020		}
3021		$sql = e107::getDb();
3022		$sqlQry = $this->toSqlQuery('create');
3023		$table = $this->getModelTable();
3024
3025		$res = $sql->insert($table, $sqlQry, $this->getParam('db_debug', false));
3026        $this->_db_qry = $sql->getLastQuery();
3027		if(!$res)
3028		{
3029			$this->_db_errno = $sql->getLastErrorNumber();
3030			$this->_db_errmsg = $sql->getLastErrorText();
3031
3032			$logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'QRY'=>print_r($sqlQry,true)) : false;
3033
3034			$this->addMessageError('SQL Insert Error', $session_messages, $logData); //TODO - Lan
3035			$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$this->_db_errmsg);
3036			$this->addMessageDebug('SQL QRY Error '.print_a($sqlQry,true));
3037
3038			return false;
3039		}
3040
3041	    e107::getAdminLog()->addSuccess('TABLE: '.$table, false);
3042		e107::getAdminLog()->save('ADMINUI_01');
3043	//	e107::getAdminLog()->clear()->addSuccess($table,false)->addArray($sqlQry)->save('ADMINUI_01');
3044
3045		// Set the reutrned ID
3046		$this->setId($res);
3047		$this->clearCache()->addMessageSuccess(LAN_CREATED. " #".$this->getId());
3048
3049		return $res;
3050    }
3051
3052    /**
3053     * Replace data in DB
3054     *
3055     * @param boolean $force force query even if $data_has_changed is false
3056     * @param boolean $session_messages to use or not session to store system messages
3057     */
3058    protected function dbReplace($force = false, $session_messages = false)
3059    {
3060    	$this->_db_errno = 0;
3061    	$this->_db_errmsg = '';
3062		$this->_db_qry = '';
3063
3064		if($this->hasError()) return false;
3065		if(!$this->data_has_changed && !$force)
3066		{
3067			return 0;
3068		}
3069		$sql = e107::getDb();
3070		$table = $this->getModelTable();
3071		$res = $sql->db_Insert($table, $this->toSqlQuery('replace'));
3072        $this->_db_qry = $sql->getLastQuery();
3073		if(!$res)
3074		{
3075			$this->_db_errno = $sql->getLastErrorNumber();
3076			$this->_db_errmsg = $sql->getLastErrorText();
3077
3078			if($this->_db_errno)
3079			{
3080				$logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'QRY'=> print_r($this->_db_qry,true)) : false;
3081
3082				$this->addMessageError('SQL Replace Error', $session_messages, $logData); //TODO - Lan
3083				$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText());
3084			}
3085		}
3086		else
3087		{
3088			$this->clearCache();
3089		}
3090		return $res;
3091    }
3092
3093    /**
3094     * Delete DB data
3095     *
3096     * @param boolean $force force query even if $data_has_changed is false
3097     * @param boolean $session_messages to use or not session to store system messages
3098     */
3099    protected function dbDelete($session_messages = false)
3100    {
3101    	$this->_db_errno = 0;
3102		$this->_db_errmsg = '';
3103		$this->_db_qry = '';
3104
3105		if($this->hasError())
3106		{
3107			return false;
3108		}
3109
3110		if(!$this->getId())
3111		{
3112			$this->addMessageError('Record not found', $session_messages); //TODO - Lan
3113			return 0;
3114		}
3115		$sql = e107::getDb();
3116		$id = $this->getId();
3117		if(is_numeric($id)) $id = intval($id);
3118		else  $id = "'".e107::getParser()->toDB($id)."'";
3119		$table  = $this->getModelTable();
3120		$where = $this->getFieldIdName().'='.$id;
3121		$res = $sql->delete($table, $where);
3122        $this->_db_qry = $sql->getLastQuery();
3123
3124		if(!$res)
3125		{
3126			$this->_db_errno = $sql->getLastErrorNumber();
3127			$this->_db_errmsg = $sql->getLastErrorText();
3128
3129			if($this->_db_errno)
3130			{
3131				$logData = ($table != 'admin_log') ? array('TABLE'=>$table, 'ERROR'=>$this->_db_errmsg, 'WHERE'=>$where) : false;
3132
3133				$this->addMessageError('SQL Delete Error', $session_messages, $logData); //TODO - Lan
3134				$this->addMessageDebug('SQL Error #'.$this->_db_errno.': '.$sql->getLastErrorText());
3135			}
3136		}
3137    	else
3138		{
3139			if($table != 'admin_log')
3140			{
3141				$logData = array('TABLE'=>$table, 'WHERE'=>$where);
3142				e107::getAdminLog()->addSuccess($table,false);
3143				e107::getAdminLog()->addArray($logData)->save('ADMINUI_03');
3144			}
3145
3146			$this->clearCache();
3147		}
3148		return $res;
3149    }
3150}
3151
3152/**
3153 * Model collection handler
3154 */
3155class e_tree_model extends e_front_model
3156{
3157	/**
3158	 * Current model DB table, used in all db calls
3159	 * This can/should be overwritten by extending the class
3160	 *
3161	 * @var string
3162	 */
3163	protected $_db_table;
3164
3165	/**
3166	 * All records (no limit) cache
3167	 *
3168	 * @var string
3169	 */
3170	protected $_total = false;
3171
3172	/**
3173	 * Constructor
3174	 *
3175	 */
3176	function __construct($tree_data = array())
3177	{
3178		if($tree_data)
3179		{
3180			$this->setTree($tree_data);
3181		}
3182	}
3183
3184	public function getTotal()
3185	{
3186		return $this->_total;
3187	}
3188
3189	public function setTotal($num)
3190	{
3191		$this->_total = $num;
3192		return $this;
3193	}
3194
3195	/**
3196	 * Set table name
3197	 * @param string $table
3198	 * @return e_tree_model
3199	 */
3200	public function setModelTable($table)
3201	{
3202		$this->_db_table = $table;
3203		return $this;
3204	}
3205
3206	/**
3207	 * Get table name
3208	 * @return string
3209	 */
3210	public function getModelTable()
3211	{
3212		return $this->_db_table;
3213	}
3214
3215	/**
3216	 * Get array of models
3217	 * @return array
3218	 */
3219	function getTree()
3220	{
3221		return $this->get('__tree', array());
3222	}
3223
3224	/**
3225	 * Set array of models
3226	 * @return e_tree_model
3227	 */
3228	function setTree($tree_data, $force = false)
3229	{
3230		if($force || !$this->isTree())
3231		{
3232			$this->set('__tree', $tree_data);
3233		}
3234
3235		return $this;
3236	}
3237
3238	/**
3239	 * Unset all current data
3240	 * @return e_tree_model
3241	 */
3242	function unsetTree()
3243	{
3244		$this->remove('__tree');
3245		return $this;
3246	}
3247
3248	public function isCacheEnabled($checkId = true)
3249	{
3250		return (null !== $this->getCacheString());
3251	}
3252
3253	public function getCacheString($replace = false)
3254	{
3255		return $this->_cache_string;
3256	}
3257
3258	public function setCacheString($str = null)
3259	{
3260		if(isset($str))
3261			return parent::setCacheString($str);
3262
3263		if($this->isCacheEnabled() && !$this->getParam('noCacheStringModify'))
3264		{
3265			$str = !$this->getParam('db_query')
3266				?
3267					$this->getModelTable()
3268					.$this->getParam('nocount')
3269					.$this->getParam('db_where')
3270					.$this->getParam('db_order')
3271					.$this->getParam('db_limit')
3272				:
3273					$this->getParam('db_query');
3274
3275			return $this->setCacheString($this->getCacheString().'_'.md5($str));
3276		}
3277
3278		return parent::setCacheString($str);
3279	}
3280
3281	protected function _setCacheData()
3282	{
3283		if(!$this->isCacheEnabled())
3284		{
3285			return $this;
3286		}
3287
3288		e107::getCache()->set_sys(
3289			$this->getCacheString(true),
3290			$this->toString(false, null, $this->getParam('nocount') ? false : true),
3291			$this->_cache_force,
3292			false
3293		);
3294		return $this;
3295	}
3296
3297	protected function _loadFromArray($array)
3298	{
3299		if(isset($array['total']))
3300		{
3301			$this->setTotal((integer) $array['total']);
3302			unset($array['total']);
3303		}
3304		$class_name = $this->getParam('model_class', 'e_model');
3305		$tree = array();
3306		foreach ($array as $id => $data)
3307		{
3308			$tree[$id] = new $class_name($data);
3309			$this->_onLoad($tree[$id]);
3310		}
3311
3312		$this->setTree($tree, true);
3313	}
3314
3315	/**
3316	 * Additional on load logic to be set from subclasses
3317	 *
3318	 * @param e_model $node
3319	 * @return e_tree_model
3320	 */
3321	protected function _onLoad($node)
3322	{
3323		return $this;
3324	}
3325
3326	/**
3327	 * Default load method
3328	 *
3329	 * @return e_tree_model
3330	 */
3331	public function loadBatch($force = false)
3332	{
3333		if ($force)
3334		{
3335			$this->unsetTree()
3336				->_clearCacheData();
3337
3338			$this->_total = false;
3339		}
3340
3341		// XXX What would break if changed to the most proper isTree()?
3342		elseif($this->isTree()) //!$this->isEmpty()
3343		{
3344			return $this;
3345		}
3346
3347		$this->setCacheString();
3348		$cached = $this->_getCacheData();
3349		if($cached !== false)
3350		{
3351			$this->_loadFromArray($cached);
3352			return $this;
3353		}
3354
3355		// auto-load all
3356		if(!$this->getParam('db_query') && $this->getModelTable())
3357		{
3358			$this->setParam('db_query', 'SELECT'.(!$this->getParam('nocount') ? ' SQL_CALC_FOUND_ROWS' : '')
3359				.($this->getParam('db_cols') ? ' '.$this->getParam('db_cols') : ' *').' FROM #'.$this->getModelTable()
3360				.($this->getParam('db_joins') ? ' '.$this->getParam('db_joins') : '')
3361				.($this->getParam('db_where') ? ' WHERE '.$this->getParam('db_where') : '')
3362				.($this->getParam('db_order') ? ' ORDER BY '.$this->getParam('db_order') : '')
3363				.($this->getParam('db_limit') ? ' LIMIT '.$this->getParam('db_limit') : '')
3364			);
3365		}
3366
3367		$class_name = $this->getParam('model_class', 'e_model');
3368		if($this->getParam('db_query') && $class_name && class_exists($class_name))
3369		{
3370			$sql = e107::getDb($this->getParam('model_class', 'e_model'));
3371			$this->_total = $sql->total_results = false;
3372
3373			if($rows = $this->getRows($sql))
3374			{
3375				foreach($rows as $tmp)
3376				{
3377					$tmp = new $class_name($tmp);
3378					if($this->getParam('model_message_stack'))
3379					{
3380						$tmp->setMessageStackName($this->getParam('model_message_stack'));
3381					}
3382					$this->_onLoad($tmp)->setNode($tmp->get($this->getFieldIdName()), $tmp);
3383				}
3384				unset($tmp);
3385
3386				$this->countResults($sql);
3387			}
3388
3389			if($sql->getLastErrorNumber())
3390			{
3391
3392				$data = array(
3393					'TABLE'     => $this->getModelTable(),
3394					'error_no' => $sql->getLastErrorNumber(),
3395					'error_msg' => $sql->getLastErrorText(),
3396					'qry'       => $sql->getLastQuery(),
3397					'url'       => e_REQUEST_URI,
3398				);
3399
3400
3401				$this->addMessageError('Application Error - DB query failed.', false, $data) // TODO LAN
3402					->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText())
3403					->addMessageDebug($sql->getLastQuery());
3404			}
3405			else
3406			{
3407				$this->_setCacheData();
3408			}
3409
3410		}
3411		return $this;
3412	}
3413
3414	protected function getRows($sql)
3415	{
3416		// Tree (Parent-Child Relationship)
3417		if ($this->getParam('sort_parent') && $this->getParam('sort_field'))
3418		{
3419			return $this->getRowsTree($sql);
3420		}
3421		// Flat List
3422		return $this->getRowsList($sql);
3423	}
3424
3425	protected function getRowsList($sql)
3426	{
3427		$success = $sql->gen($this->getParam('db_query'), $this->getParam('db_debug') ? true : false);
3428		if (!$success) return false;
3429
3430		return $sql->rows();
3431	}
3432
3433	protected function getRowsTree($sql)
3434	{
3435		// Workaround: Parse and modify db_query param for simulated pagination
3436		$this->prepareSimulatedPagination();
3437		// Workaround: Parse and modify db_query param for simulated custom ordering
3438		$this->prepareSimulatedCustomOrdering();
3439
3440		$success = $sql->gen($this->getParam('db_query'), $this->getParam('db_debug') ? true : false);
3441		if (!$success) return false;
3442
3443		$rows_tree = self::arrayToTree($sql->rows(),
3444			$this->getParam('primary_field'),
3445			$this->getParam('sort_parent'));
3446		$rows = self::flattenTree($rows_tree,
3447			$this->getParam('sort_field'),
3448			$this->getParam('sort_order'));
3449
3450		// Simulated pagination
3451		$rows = array_splice($rows,
3452			(int) $this->getParam('db_limit_offset'),
3453			($this->getParam('db_limit_count') ? $this->getParam('db_limit_count') : count($rows))
3454		);
3455
3456		return $rows;
3457	}
3458
3459	/**
3460	 * Converts a relational array with a parent field and a sort order field to a tree
3461	 * @param array $rows Relational array with a parent field and a sort order field
3462	 * @param string $primary_field The field name of the primary key (matches children to parents)
3463	 * @param string $sort_parent The field name whose value is the parent ID
3464	 * @return array Multidimensional array with child nodes under the "_children" key
3465	 */
3466	protected static function arrayToTree($rows, $primary_field, $sort_parent)
3467	{
3468		$nodes = array();
3469		$root = array($primary_field => 0);
3470		$nodes[] = &$root;
3471
3472		while(!empty($nodes))
3473		{
3474			self::moveRowsToTreeNodes($nodes, $rows, $primary_field, $sort_parent);
3475		}
3476
3477		return array(0 => $root);
3478	}
3479
3480	/**
3481	 * Put rows with parent matching the ID of the first node into the next node's children
3482	 * @param array &$nodes Current queue of nodes, the first of which may have children added to it
3483	 * @param array &rows The remaining rows that have yet to be converted into children of nodes
3484	 * @param string $primary_field The field name of the primary key (matches children to parents)
3485	 * @param string $sort_parent The field name whose value is the parent ID
3486	 * @returns null
3487	 */
3488	protected static function moveRowsToTreeNodes(&$nodes, &$rows, $primary_field, $sort_parent)
3489	{
3490		$node = &$nodes[0];
3491		array_shift($nodes);
3492		$nodeID = (int) $node[$primary_field];
3493		foreach($rows as $key => $row)
3494		{
3495			$rowParentID = (int) $row[$sort_parent];
3496
3497			// Note: This optimization only works if the SQL query executed was ordered by the sort parent.
3498			if($rowParentID > $nodeID) break;
3499
3500			$node['_children'][] = &$row;
3501			unset($rows[$key]);
3502			$nodes[] = &$row;
3503			unset($row);
3504		}
3505	}
3506
3507	/**
3508	 * Flattens a tree into a depth-first array, sorting each node by a field's values
3509	 * @param array $tree Tree with child nodes under the "_children" key
3510	 * @param mixed $sort_field The field name (string) or field names (array) whose value
3511	 *                          is or values are the sort order in the current tree node
3512	 * @param int $sort_order Desired sorting direction: 1 if ascending, -1 if descending
3513	 * @param int $depth The depth that this level of recursion is entering
3514	 * @return array One-dimensional array in depth-first order with depth indicated by the "_depth" key
3515	 */
3516	protected static function flattenTree($tree, $sort_field = null, $sort_order = 1, $depth = 0)
3517	{
3518		$flat = array();
3519
3520		foreach($tree as $item)
3521		{
3522			$children = isset($item['_children']) ? $item['_children'] : null;
3523			unset($item['_children']);
3524			$item['_depth'] = $depth;
3525			if($depth > 0)
3526				$flat[] = $item;
3527			if(is_array($children))
3528			{
3529				uasort($children, function($node1, $node2) use ($sort_field, $sort_order)
3530				{
3531					return self::multiFieldCmp($node1, $node2, $sort_field, $sort_order);
3532				});
3533				$flat = array_merge($flat, self::flattenTree($children, $sort_field, $sort_order, $depth+1));
3534			}
3535		}
3536
3537		return $flat;
3538	}
3539
3540	/**
3541	 * Naturally compares two associative arrays given multiple sort keys and a reverse order flag
3542	 * @param array $row1 Associative array to compare to $row2
3543	 * @param array $row2 Associative array to compare to $row1
3544	 * @param mixed $sort_field Key (string) or keys (array) to compare
3545	 *                          the values of in both $row1 and $row2
3546	 * @param int $sort_order -1 to reverse the sorting order or 1 to keep the order as ascending
3547	 * @return int -1 if $row1 is less than $row2
3548	 *             0 if $row1 is equal to $row2
3549	 *             1 if $row1 is greater than $row2
3550	 */
3551	protected static function multiFieldCmp($row1, $row2, $sort_field, $sort_order = 1)
3552	{
3553		if (!is_array($sort_field))
3554			$sort_field = [$sort_field];
3555		$field = array_shift($sort_field);
3556
3557		$cmp = strnatcmp((string) $row1[$field], (string) $row2[$field]);
3558		if ($sort_order === -1 || $sort_order === 1) $cmp *= $sort_order;
3559		if ($cmp === 0 && count($sort_field) >= 1)
3560			return self::multiFieldCmp($row1, $row2, $sort_field, $sort_order);
3561		return $cmp;
3562	}
3563
3564	/**
3565	 * Resiliently counts the results from the last SQL query in the given resource
3566	 *
3567	 * Sets the count in $this->_total
3568	 *
3569	 * @param resource $sql SQL resource that executed a query
3570	 * @return int Number of results from the latest query
3571	 */
3572	protected function countResults($sql)
3573	{
3574		$this->_total = is_integer($sql->total_results) ? $sql->total_results : false; //requires SQL_CALC_FOUND_ROWS in query - see db handler
3575		if(false === $this->_total && $this->getModelTable() && !$this->getParam('nocount'))
3576		{
3577			//SQL_CALC_FOUND_ROWS not found in the query, do one more query
3578		//	$this->_total = e107::getDb()->db_Count($this->getModelTable()); // fails with specific listQry
3579
3580			// Calculates correct total when using filters and search. //XXX Optimize.
3581			$countQry = preg_replace('/(LIMIT ([\d,\s])*)$/', "", $this->getParam('db_query'));
3582
3583			$this->_total = e107::getDb()->gen($countQry);
3584
3585		}
3586		return $this->_total;
3587	}
3588
3589	/**
3590	 * Workaround: Parse and modify query to prepare for simulation of tree pagination
3591	 *
3592	 * This is a hack to maintain compatibility of pagination of tree
3593	 * models without SQL LIMITs
3594	 *
3595	 * Implemented out of necessity under
3596	 * https://github.com/e107inc/e107/issues/3015
3597	 *
3598	 * @returns null
3599	 */
3600	protected function prepareSimulatedPagination()
3601	{
3602		$db_query = $this->getParam('db_query');
3603		$db_query = preg_replace_callback("/LIMIT ([\d]+)[ ]*(?:,|OFFSET){0,1}[ ]*([\d]*)/i", function($matches)
3604		{
3605			// Count only
3606			if (empty($matches[2]))
3607			{
3608				$this->setParam('db_limit_count', $matches[1]);
3609			}
3610			// Offset and count
3611			else
3612			{
3613				$this->setParam('db_limit_offset', $matches[1]);
3614				$this->setParam('db_limit_count', $matches[2]);
3615			}
3616
3617			return "";
3618		}, $db_query);
3619		$this->setParam('db_query', $db_query);
3620	}
3621
3622	/**
3623	 * Workaround: Parse and modify query to prepare for simulation of custom ordering
3624	 *
3625	 * XXX: Not compliant with all forms of ORDER BY clauses
3626	 * XXX: Does not support quoted identifiers (`identifier`)
3627	 * XXX: Does not support mixed sort orders (identifier1 ASC, identifier2 DESC)
3628	 *
3629	 * This is a hack to enable custom ordering of tree models when
3630	 * flattening the tree.
3631	 *
3632	 * Implemented out of necessity under
3633	 * https://github.com/e107inc/e107/issues/3029
3634	 *
3635	 * @returns null
3636	 */
3637	protected function prepareSimulatedCustomOrdering()
3638	{
3639		$db_query = $this->getParam('db_query');
3640		$db_query = preg_replace_callback('/ORDER BY (?:.+\.)*[\.]*([A-Za-z0-9$_,]+)[ ]*(ASC|DESC)*/i', function($matches)
3641			{
3642				if (!empty($matches[1]))
3643				{
3644					$current_sort_field = $this->getParam('sort_field');
3645					if (!empty($current_sort_field))
3646					{
3647						$matches[1] = $current_sort_field.",".$matches[1];
3648					}
3649					$this->setParam('sort_field', array_map('trim', explode(',', $matches[1])));
3650				}
3651				if (!empty($matches[2]))
3652					$this->setParam('sort_order',
3653						(0 === strcasecmp($matches[2], 'DESC') ? -1 : 1)
3654					);
3655
3656				return "";
3657			}, $db_query)
3658			// Optimization goes with e_tree_model::moveRowsToTreeNodes()
3659			. " ORDER BY " . $this->getParam('sort_parent') . "," . $this->getParam('primary_field');
3660		$this->setParam('db_query', $db_query);
3661	}
3662
3663	/**
3664	 * Get single model instance from the collection
3665	 * @param integer $node_id
3666	 * @return e_model
3667	 */
3668	function getNode($node_id)
3669	{
3670		return $this->getData('__tree/'.$node_id);
3671	}
3672
3673	/**
3674	 * Add or remove (when $node is null) model to the collection
3675	 *
3676	 * @param integer $node_id
3677	 * @param e_model $node
3678	 * @return e_tree_model
3679	 */
3680	function setNode($node_id, $node)
3681	{
3682		if(null === $node)
3683		{
3684			$this->removeData('__tree/'.$node_id);
3685			return $this;
3686		}
3687
3688		$this->setData('__tree/'.$node_id, $node);
3689		return $this;
3690	}
3691
3692	/**
3693	 * Check if model with passed id exists in the collection
3694	 *
3695	 * @param integer $node_id
3696	 * @return boolean
3697	 */
3698	public function isNode($node_id)
3699	{
3700		return $this->isData('__tree/'.$node_id);
3701	}
3702
3703	/**
3704	 * Check if model with passed id exists in the collection and is not empty
3705	 *
3706	 * @param integer $node_id
3707	 * @return boolean
3708	 */
3709	public function hasNode($node_id)
3710	{
3711		return $this->hasData('__tree/'.$node_id);
3712	}
3713
3714	/**
3715	 * Check if collection is empty
3716	 *
3717	 * @return boolean
3718	 */
3719	function isEmpty()
3720	{
3721		return (!$this->has('__tree'));
3722	}
3723
3724	/**
3725	 * Check if collection is loaded (not null)
3726	 *
3727	 * @return boolean
3728	 */
3729	function isTree()
3730	{
3731		return $this->is('__tree');
3732	}
3733
3734	/**
3735	 * Same as isEmpty(), but with opposite boolean logic
3736	 *
3737	 * @return boolean
3738	 */
3739	function hasTree()
3740	{
3741		return $this->has('__tree');
3742	}
3743
3744	/**
3745	 * Render model data, all 'sc_*' methods will be recongnized
3746	 * as shortcodes.
3747	 *
3748	 * @param string $template
3749	 * @param boolean $parsesc parse external shortcodes, default is true
3750	 * @param e_vars $eVars simple parser data
3751	 * @return string parsed template
3752	 */
3753	public function toHTML($template, $parsesc = true, $eVars = null)
3754	{
3755		$ret = '';
3756		$i = 1;
3757		foreach ($this->getTree() as $model)
3758		{
3759			if($eVars) $eVars->treeCounter = $i;
3760			$ret .= $model->toHTML($template, $parsesc, $eVars);
3761			$i++;
3762		}
3763		return $ret;
3764	}
3765
3766	public function toXML()
3767	{
3768		return '';
3769		// UNDER CONSTRUCTION
3770	}
3771
3772	/**
3773	 * Convert model object to array
3774	 * @param boolean $total include total results property
3775	 * @return array object data
3776	 */
3777	public function toArray($total = false)
3778	{
3779		$ret = array();
3780		foreach ($this->getTree() as $id => $model)
3781		{
3782			$ret[$id] = $model->toArray();
3783		}
3784		if($total) $ret['total'] = $this->getTotal();
3785
3786		return $ret;
3787	}
3788
3789	/**
3790	 * Convert object data to a string
3791	 *
3792	 * @param boolean $AddSlashes
3793	 * @param string $node_id optional, if set method will return corresponding value as a string
3794	 * @param boolean $total include total results property
3795	 * @return string
3796	 */
3797	public function toString($AddSlashes = true, $node_id = null, $total = false)
3798	{
3799		if (null !== $node_id && $this->isNode($node_id))
3800		{
3801			return $this->getNode($node_id)->toString($AddSlashes);
3802		}
3803		return (string) e107::getArrayStorage()->WriteArray($this->toArray($total), $AddSlashes);
3804	}
3805
3806	public function update($from_post = true, $force = false, $session_messages = false)
3807	{
3808	}
3809
3810	public function delete($ids, $destroy = true, $session_messages = false)
3811	{
3812	}
3813}
3814
3815class e_front_tree_model extends e_tree_model
3816{
3817	/**
3818	 * @var integer Last SQL error number
3819	 */
3820	protected $_db_errno = 0;
3821
3822	/**
3823	 * @var string Last SQL error message
3824	 */
3825	protected $_db_errmsg = '';
3826
3827	/**
3828	 * @var string Last SQL query
3829	 */
3830	protected $_db_qry = '';
3831
3832    /**
3833     * @return boolean
3834     */
3835    public function hasSqlError()
3836    {
3837    	return !empty($this->_db_errno);
3838    }
3839
3840    /**
3841     * @return integer last mysql error number
3842     */
3843    public function getSqlErrorNumber()
3844    {
3845    	return $this->_db_errno;
3846    }
3847
3848    /**
3849     * @return string last mysql error message
3850     */
3851    public function getSqlError()
3852    {
3853    	return $this->_db_errmsg;
3854    }
3855
3856    /**
3857     * @return string last mysql error message
3858     */
3859    public function getSqlQuery()
3860    {
3861    	return $this->_db_qry;
3862    }
3863
3864    /**
3865     * @return boolean
3866     */
3867    public function hasError()
3868    {
3869    	return $this->hasSqlError();
3870    }
3871
3872	/**
3873	 * Batch update tree records/nodes
3874	 * @param string $field field name
3875	 * @param string $value
3876	 * @param string|array $ids numerical array or string comma separated ids
3877	 * @param mixed $syncvalue value to be used for model data synchronization (db value could be something like '1-field_name'), null - no sync
3878	 * @param boolean $sanitize [optional] default true
3879	 * @param boolean $session_messages [optional] default false
3880	 * @return integer updated count or false on error
3881	 */
3882	public function batchUpdate($field, $value, $ids, $syncvalue = null, $sanitize = true, $session_messages = false)
3883	{
3884		$tp = e107::getParser();
3885		$sql = e107::getDb();
3886		if(empty($ids))
3887		{
3888			return 0;
3889		}
3890		if(!is_array($ids))
3891		{
3892			$ids = explode(',', $ids);
3893		}
3894
3895		if(true === $syncvalue)
3896		{
3897			$syncvalue = $value;
3898		}
3899
3900		if($sanitize)
3901		{
3902			$ids = array_map(array($tp, 'toDB'), $ids);
3903			$field = $tp->toDB($field);
3904			$value = "'".$tp->toDB($value)."'";
3905		}
3906		$idstr = implode(', ', $ids);
3907
3908		$table = $this->getModelTable();
3909
3910		$res = $sql->update($table, "{$field}={$value} WHERE ".$this->getFieldIdName().' IN ('.$idstr.')', $this->getParam('db_debug', false));
3911		$this->_db_errno = $sql->getLastErrorNumber();
3912		$this->_db_errmsg = $sql->getLastErrorText();
3913		$this->_db_qry = $sql->getLastQuery();
3914
3915		if(!$res)
3916		{
3917			if($sql->getLastErrorNumber())
3918			{
3919				$data = array(
3920					'TABLE'     => $table ,
3921					'error_no' => $this->_db_errno,
3922					'error_msg' => $this->_db_errmsg,
3923					'qry'       => $this->_db_qry,
3924					'url'       => e_REQUEST_URI,
3925				);
3926
3927
3928				$this->addMessageError(LAN_UPDATED_FAILED, $session_messages, $data);
3929				$this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText());
3930			}
3931			else
3932			{
3933				$this->addMessageInfo(LAN_NO_CHANGE, $session_messages);
3934			}
3935		}
3936		else
3937		{
3938			$this->clearCache();
3939		}
3940
3941		$modelCacheCheck = $this->getParam('clearModelCache');
3942		if(null === $syncvalue && !$modelCacheCheck) return $res;
3943
3944		foreach ($ids as $id)
3945		{
3946			$node = $this->getNode($id);
3947			if(!$node) continue;
3948
3949			if(null !== $syncvalue)
3950			{
3951				$node->set($field, $syncvalue)
3952					->setMessages($session_messages);
3953			}
3954			if($modelCacheCheck) $this->clearCache();
3955		}
3956		return $res;
3957	}
3958}
3959
3960class e_admin_tree_model extends e_front_tree_model
3961{
3962
3963
3964	/**
3965	 * Batch Delete records
3966	 * @param mixed $ids
3967	 * @param boolean $destroy [optional] destroy object instance after db delete
3968	 * @param boolean $session_messages [optional]
3969	 * @return integer deleted records number or false on DB error
3970	 */
3971	public function delete($ids, $destroy = true, $session_messages = false)
3972	{
3973		if(!$ids) return 0;
3974
3975		if(!is_array($ids))
3976		{
3977			$ids = explode(',', $ids);
3978		}
3979
3980		$tp = e107::getParser();
3981		$ids = array_map(array($tp, 'toDB'), $ids);
3982		$idstr = implode(', ', $ids);
3983
3984		$sql = e107::getDb();
3985		$table = $this->getModelTable();
3986		$sqlQry = $this->getFieldIdName().' IN (\''.$idstr.'\')';
3987
3988		$res = $sql->delete($table, $sqlQry);
3989
3990		$this->_db_errno = $sql->getLastErrorNumber();
3991		$this->_db_errmsg = $sql->getLastErrorText();
3992		$this->_db_qry = $sql->getLastQuery();
3993
3994		$modelCacheCheck = $this->getParam('clearModelCache');
3995
3996		if(!$res)
3997		{
3998			if($sql->getLastErrorNumber())
3999			{
4000				$data = array(
4001					'TABLE'     => $table,
4002					'error_no' => $this->_db_errno,
4003					'error_msg' => $this->_db_errmsg,
4004					'qry'       => $this->_db_qry,
4005					'url'       => e_REQUEST_URI,
4006				);
4007
4008
4009				$this->addMessageError('SQL Delete Error: ' . $sql->getLastQuery(), $session_messages, $data); //TODO - Lan
4010				$this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText());
4011			}
4012		}
4013		elseif($destroy || $modelCacheCheck)
4014		{
4015			foreach ($ids as $id)
4016			{
4017				if($this->hasNode($id))
4018				{
4019					$this->getNode($id)->clearCache()->setMessages($session_messages);
4020					if($destroy)
4021					{
4022						call_user_func(array($this->getNode(trim($id)), 'destroy')); // first call model destroy method if any
4023						$this->setNode($id, null);
4024					}
4025				}
4026			}
4027		}
4028
4029		if($table != 'admin_log')
4030		{
4031			$logData = array('TABLE'=>$table, 'WHERE'=>$sqlQry);
4032			e107::getAdminLog()->addArray($logData)->save('ADMINUI_03');
4033		}
4034		return $res;
4035	}
4036
4037	/**
4038	 * Batch Copy Table Rows.
4039	 */
4040	public function copy($ids, $session_messages = false)
4041	{
4042		if(empty($ids[0]))
4043		{
4044			$this->addMessageError('No IDs provided', $session_messages); //TODO - Lan
4045			$this->addMessageDebug(print_a(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),true),$session_messages); //TODO - Lan
4046			return false;
4047		}
4048
4049
4050		$tp = e107::getParser();
4051		$ids = array_map(array($tp, 'toDB'), $ids);
4052		$idstr = implode(', ', $ids);
4053
4054		$sql = e107::getDb();
4055		$res = $sql->db_CopyRow($this->getModelTable(), "*", $this->getFieldIdName().' IN ('.$idstr.')');
4056		if(false !== $res)
4057		{
4058			$this->addMessageSuccess('Copied #'.$idstr);
4059		}
4060		else
4061		{
4062			if($sql->getLastErrorNumber())
4063			{
4064				$this->addMessageError('SQL Copy Error', $session_messages); //TODO - Lan
4065				$this->addMessageDebug('SQL Error #'.$sql->getLastErrorNumber().': '.$sql->getLastErrorText());
4066				$this->addMessageDebug('$SQL Query'.print_a($sql->getLastQuery(),true));
4067			}
4068		}
4069		$this->_db_errno = $sql->getLastErrorNumber();
4070		$this->_db_errmsg = $sql->getLastErrorText();
4071		$this->_db_qry = $sql->getLastQuery();
4072		return $res;
4073	}
4074
4075
4076	/**
4077	 * Get urls/url data for given nodes
4078	 */
4079    public function url($ids, $options = array(), $extended = false)
4080    {
4081    	$ret = array();
4082    	foreach ($ids as $id)
4083    	{
4084    		if(!$this->hasNode($id)) continue;
4085
4086			$model = $this->getNode($id);
4087			if($this->getUrl()) $model->setUrl($this->getUrl()); // copy url config data if available
4088			$ret[$id] = $model->url(null, $options, $extended);
4089		}
4090		return $ret;
4091    }
4092
4093
4094	/**
4095	 * Export Selected Data
4096	 * @param $ids
4097	 * @return null
4098	 */
4099	public function export($ids)
4100    {
4101        $ids = e107::getParser()->filter($ids,'int');
4102
4103        if(empty($ids))
4104        {
4105            return false;
4106        }
4107
4108        $idstr = implode(', ', $ids);
4109
4110	    $table      = array($this->getModelTable());
4111
4112	    $filename   = "e107Export_" .$this->getModelTable()."_". date("YmdHi").".xml";
4113	    $query      = $this->getFieldIdName().' IN ('.$idstr.') '; //  ORDER BY '.$this->getParam('db_order') ;
4114
4115		e107::getXml()->e107Export(null,$table,null,null, array('file'=>$filename,'query'=>$query));
4116
4117		return null;
4118
4119    }
4120}
4121