1<?php
2/*
3 * e107 website system
4 *
5 * Copyright (C) 2008-2011 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 * User Model
10 *
11 * $URL$
12 * $Id$
13 */
14
15/**
16 * @package e107
17 * @subpackage	e107_handlers
18 * @version $Id$
19 * @author SecretR
20 *
21 * Front-end User Models
22 */
23
24if (!defined('e107_INIT'))
25{
26	exit;
27}
28
29class e_user_model extends e_admin_model
30{
31	/**
32	 * Describes all model data, used as _FIELD_TYPE array as well
33	 * @var array
34	 */
35	protected $_data_fields = array(
36		'user_id'			 => 'integer',
37		'user_name'			 => 'string',
38		'user_loginname'	 => 'string',
39		'user_customtitle'	 => 'string',
40		'user_password'		 => 'string',
41		'user_sess'			 => 'string',
42		'user_email'		 => 'string',
43		'user_signature'	 => 'string',
44		'user_image'		 => 'string',
45		'user_hideemail'	 => 'integer',
46		'user_join'			 => 'integer',
47		'user_lastvisit'	 => 'integer',
48		'user_currentvisit'	 => 'integer',
49		'user_lastpost'		 => 'integer',
50		'user_chats'		 => 'integer',
51		'user_comments'		 => 'integer',
52		'user_ip'			 => 'string',
53		'user_ban'			 => 'integer',
54		'user_prefs'		 => 'string',
55		'user_visits'		 => 'integer',
56		'user_admin'		 => 'integer',
57		'user_login'		 => 'string',
58		'user_class'		 => 'string',
59		'user_perms'		 => 'string',
60		'user_realm'		 => 'string',
61		'user_pwchange'		 => 'integer',
62		'user_xup'			 => 'string',
63	);
64
65	/**
66	 * Validate required fields
67	 * @var array
68	 */
69	protected $_validation_rules = array(
70		'user_name' => array('string', '1', 'LAN_USER_01', 'LAN_USER_HELP_01'), // TODO - regex
71		'user_loginname' => array('string', '1', 'LAN_USER_02', 'LAN_USER_HELP_02'), // TODO - regex
72		'user_password' => array('compare', '5', 'LAN_PASSWORD', 'LAN_USER_HELP_05'), // TODO - pref - modify it somewhere below - prepare_rules()?
73		'user_email' => array('email', '', 'LAN_EMAIL', 'LAN_USER_HELP_08'),
74	);
75
76	/**
77	 * Validate optional fields - work in progress, not working yet
78	 * @var array
79	 */
80	protected $_optional_rules = array(
81		'user_customtitle' => array('string', '1', 'LAN_USER_01'), // TODO - regex
82	);
83
84	/**
85	 * @see e_model
86	 * @var string
87	 */
88	protected $_db_table = 'user';
89
90	/**
91	 * @see e_model
92	 * @var string
93	 */
94	protected $_field_id = 'user_id';
95
96	/**
97	 * @see e_model
98	 * @var string
99	 */
100	protected $_message_stack = 'user';
101
102	/**
103	 * User class as set in user Adminsitration
104	 *
105	 * @var integer
106	 */
107	protected $_memberlist_access = null;
108
109	/**
110	 * Extended data
111	 *
112	 * @var e_user_extended_model
113	 */
114	protected $_extended_model = null;
115
116	/**
117	 * Extended structure
118	 *
119	 * @var e_user_extended_structure
120	 */
121	protected $_extended_structure = null;
122
123	/**
124	 * User preferences model
125	 * @var e_user_pref
126	 */
127	protected $_user_config = null;
128
129	/**
130	 * User model of current editor
131	 * @var e_user_model
132	 */
133	protected $_editor = null;
134
135	protected $_class_list;
136
137	/**
138	 * Constructor
139	 * @param array $data
140	 * @return void
141	 */
142	public function __construct($data = array())
143	{
144		$this->_memberlist_access = e107::getPref('memberlist_access');
145		parent::__construct($data);
146	}
147
148	/**
149	 * Always return integer
150	 *
151	 * @see e107_handlers/e_model#getId()
152	 */
153	public function getId()
154	{
155		return (integer) parent::getId();
156	}
157
158	/**
159	 * Try display name, fall back to login name when empty (shouldn't happen)
160	 */
161	final public function getName($anon = false)
162	{
163		if($this->isUser())
164		{
165			return ($this->get('user_name') ? $this->get('user_name') : $this->get('user_loginname'));
166		}
167		return $anon;
168	}
169
170	/**
171	 * Display name getter. Use it as DB field name will be changed soon.
172	 */
173	final public function getDisplayName()
174	{
175		return $this->get('user_name');
176	}
177
178	/**
179	 * Login name getter. Use it as DB field name will be changed soon.
180	 */
181	final public function getLoginName()
182	{
183		return $this->get('user_loginname');
184	}
185
186	/**
187	 * Real name getter. Use it as DB field name will be changed soon.
188	 * @param bool $strict if false, fall back to Display name when empty
189	 * @return mixed
190	 */
191	final public function getRealName($strict = false)
192	{
193		if($strict) return $this->get('user_login');
194		return ($this->get('user_login') ? $this->get('user_login') : $this->get('user_name'));
195	}
196
197	final public function getAdminId()
198	{
199		return ($this->isAdmin() ? $this->getId() : false);
200	}
201
202	final public function getAdminName()
203	{
204		return ($this->isAdmin() ? $this->get('user_name') : false);
205	}
206
207	final public function getAdminEmail()
208	{
209		return ($this->isAdmin() ? $this->get('user_email') : false);
210	}
211
212	final public function getAdminPwchange()
213	{
214		return ($this->isAdmin() ? $this->get('user_pwchange') : false);
215	}
216
217	final public function getAdminPerms()
218	{
219		return ($this->isAdmin() ? $this->get('user_perms') : false);
220	}
221
222	final public function getTimezone()
223	{
224		// If timezone is not set, we return an empty string in order to use the
225		// default timezone is set for e107.
226		return ($this->get('user_timezone') ? $this->get('user_timezone') : '');
227	}
228
229	/**
230	 * DEPRECATED - will be removed or changed soon (see e_session)
231	 * @return string
232	 */
233	public function getToken()
234	{
235		if(null === $this->get('user_token'))
236		{
237			//$this->set('user_token', md5($this->get('user_password').$this->get('user_lastvisit').$this->get('user_pwchange').$this->get('user_class')));
238			$this->set('user_token', e107::getSession()->getFormToken(false));
239		}
240		return $this->get('user_token');
241	}
242
243	public static function randomKey()
244	{
245		return md5(uniqid(rand(), 1));
246	}
247
248	public function isCurrent()
249	{
250		return false;
251	}
252
253	final public function isAdmin()
254	{
255		return ($this->get('user_admin') ? true : false);
256	}
257
258	final public function isNewUser()
259	{
260		$new_user_period = e107::getPref('user_new_period', 0);
261
262		if(empty($new_user_period))	{ return false; }
263
264		return (($this->get('user_join') > strtotime($new_user_period." days ago")) ? true : false);
265	}
266
267	final public function isBot()
268	{
269		$userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
270
271		if(empty($userAgent))
272		{
273			return false;
274		}
275
276		$botlist = array( "googlebot", "Bingbot", 'slurp', 'baidu', 'ichiro','nutch','yacy', "Teoma",
277		"alexa", "froogle", "Gigabot", "inktomi",
278		"looksmart", "URL_Spider_SQL", "Firefly", "NationalDirectory",
279		"Ask Jeeves", "TECNOSEEK", "InfoSeek", "WebFindBot", "girafabot",
280		"crawler", "www.galaxy.com", "Scooter", "msnbot", "appie", "FAST", "WebBug", "Spade", "ZyBorg", "rabaz",
281		"Baiduspider", "Feedfetcher-Google", "TechnoratiSnoop", "Rankivabot",
282		"Mediapartners-Google", "Sogou web spider", "WebAlta Crawler","TweetmemeBot",
283		"Butterfly","Twitturls","Me.dium","Twiceler");
284
285		foreach($botlist as $bot)
286		{
287			if(stripos($userAgent, $bot) !== false){ return true; }
288		}
289
290		return false;
291	}
292
293	final public function isMainAdmin()
294	{
295		return $this->checkAdminPerms('0');
296	}
297
298	final public function isUser()
299	{
300		return ($this->getId() ? true : false);
301	}
302
303	final public function isGuest()
304	{
305		return ($this->getId() ? false : true);
306	}
307
308	final public function hasBan()
309	{
310		return ((integer)$this->get('user_ban') === 1 ? true : false);
311	}
312
313	final public function hasRestriction()
314	{
315		return ((integer)$this->get('user_ban') === 0 ? false : true);
316	}
317
318	public function hasEditor()
319	{
320		return (null !== $this->_editor);
321	}
322
323	final protected function _setClassList()
324	{
325		$this->_class_list = array();
326		if ($this->isUser())
327		{
328			if ($this->get('user_class'))
329			{
330				// list of all 'inherited' user classes, convert elements to integer
331				$this->_class_list = array_map('intval', e107::getUserClass()->get_all_user_classes($this->get('user_class'), true));
332			}
333
334			$this->_class_list[] = e_UC_MEMBER;
335
336			if($this->isNewUser())
337			{
338				$this->_class_list[] = e_UC_NEWUSER;
339			}
340
341			if ($this->isAdmin())
342			{
343				$this->_class_list[] = e_UC_ADMIN;
344			}
345
346			if ($this->isMainAdmin())
347			{
348				$this->_class_list[] = e_UC_MAINADMIN;
349			}
350		}
351		else
352		{
353			$this->_class_list[] = e_UC_GUEST;
354
355			if($this->isBot())
356			{
357				$this->_class_list[] = e_UC_BOTS;
358			}
359
360		}
361
362		$this->_class_list[] = e_UC_READONLY;
363		$this->_class_list[] = e_UC_PUBLIC;
364
365		// unique, rebuild indexes
366		$this->_class_list = array_merge(array_unique($this->_class_list));
367		return $this;
368	}
369
370	/**
371	 * @param bool $toString
372	 * @return string|array
373	 */
374	final public function getClassList($toString = false)
375	{
376		if (null === $this->_class_list)
377		{
378			$this->_setClassList();
379		}
380		return ($toString ? implode(',', $this->_class_list) : $this->_class_list);
381	}
382
383	final public function getClassRegex()
384	{
385		return '(^|,)('.str_replace(',', '|', $this->getClassList(true)).')(,|$)';
386	}
387
388	final public function checkClass($class, $allowMain = true)
389	{
390		// FIXME - replace check_class() here
391		return (($allowMain && $this->isMainAdmin()) || check_class($class, $this->getClassList(), 0));
392	}
393
394	final public function checkAdminPerms($perm_str)
395	{
396		// FIXME - method to replace getperms()
397		return ($this->isAdmin() && getperms($perm_str, $this->getAdminPerms()));
398	}
399
400	final public function checkEditorPerms($class = '')
401	{
402		if (!$this->hasEditor())
403			return false;
404
405		$editor = $this->getEditor();
406
407		if ('' !== $class)
408			return ($editor->isAdmin() && $editor->checkClass($class));
409
410		return $editor->isAdmin();
411	}
412
413	/**
414	 * Check passed value against current user token
415	 * DEPRECATED - will be removed or changed soon (see e_core_session)
416	 * @param string $token md5 sum of e.g. posted token
417	 * @return boolean
418	 */
419	final public function checkToken($token)
420	{
421		$utoken = $this->getToken();
422		return (null !== $utoken && $token === md5($utoken));
423	}
424
425	/**
426	 * Bad but required (BC) method of retrieving all user data
427	 * It's here to be used from get_user_data() core function.
428	 * DON'T USE THEM BOTH unless you have VERY good reason to do it.
429	 *
430	 * @return array
431	 */
432	public function getUserData()
433	{
434		// revised - don't call extended object, no permission checks, just return joined user data
435		$ret = $this->getData();
436		// $ret = array_merge($this->getExtendedModel()->getExtendedData(), $this->getData());
437		if ($ret['user_perms'] == '0.') $ret['user_perms'] = '0';
438		$ret['user_baseclasslist'] = $ret['user_class'];
439		$ret['user_class'] = $this->getClassList(true);
440		return $ret;
441	}
442
443	/**
444	 * Check if given field name is present in core user table structure
445	 *
446	 * @param string $field
447	 * @param boolean $short
448	 * @return boolean
449	 */
450	public function isCoreField($field, $short = true)
451	{
452		if($short) $field = 'user_'.$field;
453		return isset($this->_data_fields[$field]);
454	}
455
456	/**
457	 * Check if given field name is present in extended user table structure
458	 *
459	 * @param string $field
460	 * @param boolean $short
461	 * @return boolean
462	 */
463	public function isExtendedField($field, $short = true)
464	{
465		if($short) $field = 'user_'.$field;
466		if($this->isCoreField($field, false))
467		{
468			return false;
469		}
470		return $this->getExtendedModel()->isField($field, false);
471	}
472
473	/**
474	 * Get User value from core user table.
475	 * This method doesn't perform any read permission cheks.
476	 *
477	 * @param string $field
478	 * @param mixed $default
479	 * @param boolean $short if true, 'user_' prefix will be added to field name
480	 * @return mixed if field is not part of core user table returns null by default
481	 */
482	public function getCore($field, $default = null, $short = true)
483	{
484		if($short) $field = 'user_'.$field;
485		if($this->isCoreField($field, false)) return $this->get($field, $default);
486		return $default;
487	}
488
489	/**
490	 * Set User value (core user field).
491	 * This method doesn't perform any write permission cheks.
492	 *
493	 * @param string $field
494	 * @param mixed $value
495	 * @param boolean $short if true, 'user_' prefix will be added to field name
496	 * @param boolean $strict if false no Applicable check will be made
497	 * @return e_user_model
498	 */
499	public function setCore($field, $value, $short = true, $strict = false)
500	{
501		if($short) $field = 'user_'.$field;
502		if($this->isCoreField($field, false)) $this->set($field, $value, $strict);
503		return $this;
504	}
505
506	/**
507	 * Get User extended value.
508	 * This method doesn't perform any read permission cheks.
509	 *
510	 * @param string $field
511	 * @param boolean $short if true, 'user_' prefix will be added to field name
512	 * @param boolean $raw get raw DB values (no SQL query)
513	 * @return mixed
514	 */
515	public function getExtended($field, $short = true, $raw = true)
516	{
517		return $this->getExtendedModel()->getSystem($field, $short, $raw);
518	}
519
520	/**
521	 * Set User extended value.
522	 * This method doesn't perform any write permission cheks.
523	 *
524	 * @param string $field
525	 * @param mixed $value
526	 * @param boolean $short if true, 'user_' prefix will be added to field name
527	 * @param boolean $strict if false no Applicable check will be made
528	 * @return e_user_model
529	 */
530	public function setExtended($field, $value, $short = true, $strict = false)
531	{
532		$this->getExtendedModel()->setSystem($field, $value, $short, $strict);
533		return $this;
534	}
535
536	/**
537	 * Get User extended value after checking read permissions against current Editor
538	 *
539	 * @param string $field
540	 * @param boolean $short if true, 'user_' prefix will be added to field name
541	 * @param boolean $raw get raw DB values (no SQL query)
542	 * @return mixed
543	 */
544	public function getExtendedFront($field, $short = true, $raw = false)
545	{
546		return $this->getExtendedModel()->getValue($field, $short, $raw);
547	}
548
549	/**
550	 * Set User extended value after checking write permissions against current Editor.
551	 *
552	 * @param string $field
553	 * @param mixed $value
554	 * @param boolean $short if true, 'user_' prefix will be added to field name
555	 * @return e_user_model
556	 */
557	public function setExtendedFront($field, $value, $short = true)
558	{
559		$this->getExtendedModel()->setValue($field, $value, $short);
560		return $this;
561	}
562
563	/**
564	 * Transparent front-end getter. It performs all required read/applicable permission checks
565	 * against current editor/user. It doesn't distinguish core and extended fields.
566	 * It grants BC.
567	 * It's what you'd need in all front-end parsing code (e.g. shortcodes)
568	 *
569	 * @param string $field
570	 * @param mixed $default
571	 * @param boolean $short if true, 'user_' prefix will be added to field name
572	 * @param boolean $rawExtended get raw DB values (no SQL query) - used only for extended fields
573	 * @return mixed if field is not readable returns null by default
574	 */
575	public function getValue($field, $default = null, $short = true, $rawExtended = false)
576	{
577		if($short)
578		{
579			$mfield = $field;
580			$field = 'user_'.$field;
581		}
582		else
583		{
584			$mfield = substr($field, 5);
585		}
586
587		// check for BC/override method first e.g. getSingatureValue($default, $system = false, $rawExtended);
588		$method = 'get'.ucfirst($mfield).'Value';
589		if(method_exists($this, $method)) return $this->$method($default, false, $rawExtended);
590
591		if($this->isCoreField($field, false))
592		{
593			if(!$this->isReadable($field)) return $default;
594			return $this->getCore($field, $default, false);
595		}
596
597		return $this->getExtendedFront($field, false, $rawExtended);
598	}
599
600	/**
601	 * Transparent front-end setter. It performs all required write/applicable permission checks
602	 * against current editor/user. It doesn't distinguish core and extended fields.
603	 * It grants BC.
604	 * It's what you'd need on all user front-end manipulation events (e.g. user settings page related code)
605	 * NOTE: untrusted data should be provided via setPosted() method!
606	 *
607	 * @param string $field
608	 * @param mixed $value
609	 * @param boolean $short if true, 'user_' prefix will be added to field name
610	 * @return e_user_model
611	 */
612	public function setValue($field, $value, $short = true)
613	{
614		if($short)
615		{
616			$mfield = $field;
617			$field = 'user_'.$field;
618		}
619		else
620		{
621			$mfield = substr($field, 5);
622		}
623
624		// check for BC/override method first e.g. setSingatureValue($value, $system = false);
625		$method = 'set'.ucfirst($mfield).'Value';
626		if(method_exists($this, $method))
627		{
628			$this->$method($value, false);
629			return $this;
630		}
631
632		if($this->isCoreField($field, false))
633		{
634			if($this->isWritable($field)) $this->setCore($field, $value, false, true);
635		}
636		else
637		{
638			$this->setExtendedFront($field, $value, false);
639		}
640
641		return $this;
642	}
643
644	/**
645	 * Transparent system getter. It doesn't perform any read/applicable permission checks
646	 * against current editor/user. It doesn't distinguish core and extended fields.
647	 * It grants BC.
648	 * It's here to serve in your application logic.
649	 *
650	 * @param string $field
651	 * @param mixed $default
652	 * @param boolean $short if true, 'user_' prefix will be added to field name
653	 * @param boolean $rawExtended get raw DB values (no SQL query) - used only for extended fields
654	 * @return mixed
655	 */
656	public function getSystem($field, $default = null, $short = true, $rawExtended = true)
657	{
658		if($short)
659		{
660			$mfield = $field;
661			$field = 'user_'.$field;
662		}
663		else
664		{
665			$mfield = substr($field, 5);
666		}
667
668		// check for BC/override method first e.g. getSingatureValue($default, $system = true, $rawExtended);
669		$method = 'get'.ucfirst($mfield).'Value';
670		if(method_exists($this, $method)) return $this->$method($default, true, $rawExtended);
671
672		if($this->isCoreField($field, false))
673		{
674			return $this->getCore($field, $default, false);
675		}
676
677		return $this->getExtended($field, false, $rawExtended);
678	}
679
680	/**
681	 * Transparent front-end setter. It doesn't perform any write/applicable permission checks
682	 * against current editor/user. It doesn't distinguish core and extended fields.
683	 * It's here to serve in your application logic.
684	 * NOTE: untrusted data should be provided via setPosted() method!
685	 *
686	 * @param string $field
687	 * @param mixed $value
688	 * @param boolean $short if true, 'user_' prefix will be added to field name
689	 * @param boolean $strict if false no Applicable check will be made
690	 * @return e_user_model
691	 */
692	public function setSystem($field, $value, $short = true, $strict = false)
693	{
694		if($short)
695		{
696			$mfield = $field;
697			$field = 'user_'.$field;
698		}
699		else
700		{
701			$mfield = substr($field, 5);
702		}
703
704		// check for BC/override method first e.g. setSingatureValue($value, $system = true);
705		$method = 'set'.ucfirst($mfield).'Value';
706		if(method_exists($this, $method))
707		{
708			$this->$method($value, true);
709			return $this;
710		}
711
712		if($this->isCoreField($field, false))
713		{
714			$this->setCore($field, $value, false, $strict);
715		}
716		else
717		{
718			$this->setExtended($field, $value, false, $strict);
719		}
720
721		return $this;
722	}
723
724	/**
725	 * Just an example override method. This method is auto-magically called by getValue/System
726	 * getters.
727	 * $rawExtended is not used (here for example purposes only)
728	 * If user_signature become extended field one day, we'd need this method
729	 * for real - it'll call extended getters to retrieve the required value.
730	 *
731	 * @param mixed $default optional
732	 * @param boolean $system optional
733	 * @param boolean $rawExtended optional
734	 * @return mixed value
735	 */
736	public function getSignatureValue($default = null, $system = false, $rawExtended = true)
737	{
738		if($system || $this->isReadable('user_signature')) return $this->getCore('signature', $default);
739		return $default;
740	}
741
742	/**
743	 * Just an example override method. This method is auto-magically called by setValue/System
744	 * setters.
745	 * If user_signature become extended field one day, we'd need this method
746	 * for real - it'll call extended setters to set the new signature value
747	 *
748	 * @param string $value
749	 * @param boolean $system
750	 * @return e_user_model
751	 */
752	public function setSignatureValue($value, $system = false)
753	{
754		if($system || $this->isWritable('user_signature')) $this->setCore('signature', $value);
755		return $this;
756	}
757
758	/**
759	 * Get user preference
760	 * @param string $pref_name
761	 * @param mixed $default
762	 * @return mixed
763	 */
764	public function getPref($pref_name = null, $default = null)
765	{
766		if(null === $pref_name) return $this->getConfig()->getData();
767		return $this->getConfig()->get($pref_name, $default);
768	}
769
770	/**
771	 * Set user preference
772	 * @param string $pref_name
773	 * @param mixed $value
774	 * @return e_user_model
775	 */
776	public function setPref($pref_name, $value = null)
777	{
778		$this->getConfig()->set($pref_name, $value);
779		return $this;
780	}
781
782	/**
783	 * Get user preference (advanced - slower)
784	 * @param string $pref_path
785	 * @param mixed $default
786	 * @param integer $index if number, value will be exploded by "\n" and corresponding index will be returned
787	 * @return mixed
788	 */
789	public function findPref($pref_path = null, $default = null, $index = null)
790	{
791		return $this->getConfig()->getData($pref_path, $default, $index);
792	}
793
794	/**
795	 * Set user preference (advanced - slower)
796	 * @param string $pref_path
797	 * @param mixed $value
798	 * @return e_user_model
799	 */
800	public function setPrefData($pref_path, $value = null)
801	{
802		$this->getConfig()->setData($pref_path, $value = null);
803		return $this;
804	}
805
806	/**
807	 * New - External login providers support
808	 * @return string Provider name
809	 */
810	public function getProviderName()
811	{
812		if($this->get('user_xup'))
813		{
814			$provider = explode('_', $this->get('user_xup'));
815			return array_shift($provider);
816		}
817		return null;
818	}
819
820	/**
821	 * New - External login providers support
822	 * @return boolean Check if there is external provider data
823	 */
824	public function hasProviderName()
825	{
826		return $this->has('user_xup');
827	}
828
829	/**
830	 * Get user extended model
831	 *
832	 * @return e_user_extended_model
833	 */
834	public function getExtendedModel()
835	{
836		if (null === $this->_extended_model)
837		{
838			$this->_extended_model = new e_user_extended_model($this);
839		}
840		return $this->_extended_model;
841	}
842
843	/**
844	 * Set user extended model
845	 *
846	 * @param e_user_extended_model $extended_model
847	 * @return e_user_model
848	 */
849	public function setExtendedModel($extended_model)
850	{
851		$this->_extended_model = $extended_model;
852		return $this;
853	}
854
855	/**
856	 * Get user config model
857	 *
858	 * @return e_user_pref
859	 */
860	public function getConfig()
861	{
862		if (null === $this->_user_config)
863		{
864			$this->_user_config = new e_user_pref($this);
865		}
866		return $this->_user_config;
867	}
868
869	/**
870	 * Set user config model
871	 *
872	 * @param e_user_pref $user_config
873	 * @return e_user_model
874	 */
875	public function setConfig(e_user_pref $user_config)
876	{
877		$this->_user_config = $user_config;
878		return $this;
879	}
880
881	/**
882	 * Get current user editor model
883	 * @return e_user_model
884	 */
885	public function getEditor()
886	{
887		return $this->_editor;
888	}
889
890	/**
891	 * Set current user editor model
892	 * @return e_user_model
893	 */
894	public function setEditor(e_user_model $user_model)
895	{
896		$this->_editor = $user_model;
897		return $this;
898	}
899
900	/**
901	 * Check if passed field is writable
902	 * @param string $field
903	 * @return boolean
904	 */
905	public function isWritable($field)
906	{
907		$perm = false;
908		$editor = $this->getEditor();
909		if($this->getId() === $editor->getId() || $editor->isMainAdmin() || $editor->checkAdminPerms('4'))
910			$perm = true;
911		return ($perm && !in_array($field, array($this->getFieldIdName(), 'user_admin', 'user_perms', 'user_prefs')));
912	}
913
914	/**
915	 * Check if passed field is readable by the Editor
916	 * @param string $field
917	 * @return boolean
918	 */
919	public function isReadable($field)
920	{
921		$perm = false;
922		$editor = $this->getEditor();
923		if($this->getId() === $editor->getId() || $editor->isMainAdmin() || $editor->checkAdminPerms('4'))
924			$perm = true;
925		return ($perm || (!in_array($field, array('user_admin', 'user_perms', 'user_prefs', 'user_password') && $editor->checkClass($this->_memberlist_access))));
926	}
927
928	/**
929	 * Set current object as a target
930	 *
931	 * @return e_user_model
932	 */
933	protected function setAsTarget()
934	{
935		e107::setRegistry('core/e107/user/'.$this->getId(), $this);
936		return $this;
937	}
938
939	/**
940	 * Clear registered target
941	 *
942	 * @return e_user_model
943	 */
944	protected function clearTarget()
945	{
946		e107::setRegistry('core/e107/user'.$this->getId(), null);
947		return $this;
948	}
949
950	/**
951	 * @see e_model#load($id, $force)
952	 */
953	public function load($user_id = 0, $force = false)
954	{
955		$qry = "SELECT u.*, ue.* FROM #user AS u LEFT JOIN #user_extended as ue ON u.user_id=ue.user_extended_id WHERE u.user_id={ID}";
956		$this->setParam('db_query', $qry);
957		parent::load($user_id, $force);
958		if ($this->getId())
959		{
960			// no errors - register
961			$this->setAsTarget()
962				->setEditor(e107::getUser()); //set current user as default editor
963		}
964	}
965
966	/**
967	 * Additional security while applying posted
968	 * data to user model
969	 * @return e_user_model
970	 */
971	public function mergePostedData($strict = true, $sanitize = true, $validate = true)
972    {
973    	$posted = $this->getPostedData();
974    	foreach ($posted as $key => $value)
975    	{
976    		if(!$this->isWritable($key))
977    		{
978    			$this->removePosted($key);
979    			continue;
980    		}
981    		$this->_modifyPostedData($key, $value);
982    	}
983		parent::mergePostedData(true, true, true);
984		return $this;
985    }
986
987	protected function _modifyPostedData($key, $value)
988    {
989    	// TODO - add more here
990    	switch ($key)
991    	{
992    		case 'password1':
993    			// compare validation rule
994    			$this->setPosted('user_password', array($value, $this->getPosted('password2')));
995    		break;
996    	}
997    }
998
999	/**
1000	 * Send model data to DB
1001	 */
1002	public function save($noEditorCheck = false, $force = false, $session = false)
1003	{
1004		if (!$noEditorCheck && !$this->checkEditorPerms())
1005		{
1006			return false; // TODO - message, admin log
1007		}
1008
1009		// sync user prefs
1010		$this->getConfig()->apply();
1011
1012		// TODO - do the save manually in this order: validate() on user model, save() on extended fields, save() on user model
1013		$ret = parent::save(true, $force, $session);
1014
1015		if(false !== $ret && null !== $this->_extended_model) // don't load extended fields if not already used
1016		{
1017			$ret_e = $this->_extended_model->save(true, $force, $session);
1018			if(false !== $ret_e)
1019			{
1020				return ($ret_e + $ret);
1021			}
1022			return false;
1023		}
1024		return $ret;
1025	}
1026
1027	public function saveDebug($extended = true, $return = false, $undo = true)
1028	{
1029		$ret = array();
1030		$ret['CORE_FIELDS'] = parent::saveDebug(true, $undo);
1031		if($extended && null !== $this->_extended_model)
1032		{
1033			$ret['EXTENDED_FIELDS'] = $this->_extended_model->saveDebug(true, $undo);
1034		}
1035
1036		if($return) return $ret;
1037		print_a($ret);
1038	}
1039
1040	public function destroy()
1041	{
1042		$this->clearTarget()
1043			->removeData();
1044
1045		$this->_class_list = array();
1046		$this->_editor = null;
1047		$this->_extended_structure = null;
1048		$this->_user_config = null;
1049
1050		if (null !== $this->_extended_model)
1051		{
1052			$this->_extended_model->destroy();
1053			 $this->_extended_model = null;
1054		}
1055	}
1056
1057
1058	/**
1059	 * Add userclass to user and save.
1060	 * @param null $userClassId
1061	 * @return bool
1062	 */
1063	public function addClass($userClassId=null)
1064	{
1065		if(empty($userClassId))
1066		{
1067			return false;
1068		}
1069
1070//		$curClasses = explode(",", $this->getData('user_class'));
1071//		$curClasses[] = $userClassId;
1072//		$curClasses = array_unique($curClasses);
1073//
1074//		$insert = implode(",", $curClasses);
1075
1076		//FIXME - @SecretR - I'm missing something here with setCore() etc.
1077	//	$this->setCore('user_class',$insert );
1078	//	$this->saveDebug(false);
1079
1080		// Switched to unified remove user class method
1081		$insert = e107::getUserClass()->ucAdd($userClassId, $this->getData('user_class'), false);
1082
1083		if(!$uid = $this->getData('user_id'))
1084		{
1085			return false;
1086		}
1087
1088		return e107::getDb()->update('user',"user_class='".$insert."' WHERE user_id = ".$uid." LIMIT 1");
1089
1090	}
1091
1092
1093	/**
1094	 * Remove a userclass from the user.
1095	 * @param null $userClassId
1096	 * @return bool
1097	 */
1098	public function removeClass($userClassId=null)
1099	{
1100		if(empty($userClassId))
1101		{
1102			return false;
1103		}
1104
1105//		$curClasses = explode(",", $this->getData('user_class'));
1106//
1107//		foreach($curClasses as $k=>$v)
1108//		{
1109//			if($v == $userClassId)
1110//			{
1111//				unset($curClasses[$k]);
1112//			}
1113//		}
1114
1115//		$uid = $this->getData('user_id');
1116
1117//		$insert = implode(",", $curClasses);
1118
1119		// Switched to unified remove user class method
1120		$insert = e107::getUserClass()->ucRemove($userClassId, $this->getData('user_class'), false);
1121
1122		if(!$uid = $this->getData('user_id'))
1123		{
1124			return false;
1125		}
1126
1127		return e107::getDb()->update('user',"user_class='".$insert."' WHERE user_id = ".$uid." LIMIT 1");
1128
1129
1130	}
1131
1132
1133}
1134
1135// TODO - add some more useful methods, sc_* methods support
1136class e_system_user extends e_user_model
1137{
1138	public $debug = false;
1139	/**
1140	 * Constructor
1141	 *
1142	 * @param array $user_data trusted data, loaded from DB
1143	 * @return void
1144	 */
1145	public function __construct($user_data = array())
1146	{
1147		parent::__construct($user_data);
1148		$this->setEditor(e107::getUser());
1149	}
1150
1151	/**
1152	 * Returns always false
1153	 * Even if user data belongs to the current user, Current User interface
1154	 * is not available
1155	 *
1156	 * @return boolean
1157	 */
1158	final public function isCurrent()
1159	{
1160		// check against current system user
1161		//return ($this->getId() && $this->getId() == e107::getUser()->getId());
1162		return false;
1163	}
1164
1165	/**
1166	 * Send user email
1167	 * @param mixed $userInfo array data or null for current logged in user or any object subclass of e_object (@see e_system_user::renderEmail() for field requirements)
1168	 */
1169	public function email($type = 'email', $options = array(), $userInfo = null)
1170	{
1171
1172		if(null === $userInfo)
1173		{
1174			$userInfo = $this->getData();
1175		}
1176		elseif(is_object($userInfo) && get_class($userInfo) == 'e_object' || is_subclass_of($userInfo, 'e_object'))
1177		{
1178			$userInfo = $userInfo->getData();
1179		}
1180
1181		if(empty($userInfo) || !vartrue($userInfo['user_email'])) return false;
1182
1183		// plain password could be passed only via $options
1184		unset($userInfo['user_password']);
1185		if($options && is_array($options))
1186		{
1187			$userInfo = array_merge($options, $userInfo);
1188		}
1189
1190		$eml = $this->renderEmail($type, $userInfo);
1191
1192
1193
1194		if(empty($eml))
1195		{
1196			if($this->debug)
1197			{
1198				echo '$eml returned nothing on Line '.__LINE__.' of user_model.php using $type = '.$type;
1199				print_a($userInfo);
1200			}
1201			 return false;
1202		}
1203		else
1204		{
1205			if($this->debug)
1206			{
1207				echo '<h3>$eml array</h3>';
1208				print_a($eml);
1209				$temp = var_export($eml, true);
1210				print_a($temp);
1211			}
1212		}
1213
1214		$mailer = e107::getEmail();
1215
1216		$mailer->template = $eml['template'];
1217
1218
1219		// Custom e107 Header
1220		if($userInfo['user_id'])
1221		{
1222			$eml['e107_header'] = $userInfo['user_id'];
1223		//	$mailer->AddCustomHeader("X-e107-id: {$userInfo['user_id']}");
1224		}
1225
1226
1227		if(getperms('0') && E107_DEBUG_LEVEL > 0)
1228		{
1229			e107::getMessage()->addDebug("Email Debugger active. <b>Simulation Only!</b>");
1230			e107::getMessage()->addDebug($mailer->preview($eml));
1231			return true;
1232		}
1233
1234		if(!empty($options['debug']))
1235		{
1236			return $mailer->preview($eml);
1237		}
1238
1239
1240		return $mailer->sendEmail($userInfo['user_email'], $userInfo['user_name'], $eml, false);
1241	}
1242
1243	/**
1244	 * Render user email.
1245	 * Additional user fields:
1246	 * 'mail_subject' -> required when type is not signup
1247	 * 'mail_body' -> required when type is not signup
1248	 * 'mail_copy_to' -> optional, carbon copy, used when type is not signup
1249	 * 'mail_bcopy_to' -> optional, blind carbon copy, used when type is not signup
1250	 * 'mail_attach' -> optional, attach files, available for all types, additionally it overrides $SIGNUPEMAIL_ATTACHMENTS when type is signup
1251	 * 'mail_options' -> optional, available for all types, any additional valid mailer option as described in e107Email::sendEmail() phpDoc help (options above can override them)
1252	 * All standard user fields from the DB (user_name, user_loginname, etc.)
1253	 *
1254	 * @param string $type signup|notify|email|quickadd
1255	 * @param array $userInfo
1256	 * @return array
1257	 */
1258	public function renderEmail($type, $userInfo)
1259	{
1260		global $SIGNUPEMAIL_USETHEME, $QUICKADDUSER_TEMPLATE, $NOTIFY_TEMPLATE;
1261		$pref = e107::getPref();
1262		$ret = array();
1263		$tp = e107::getParser();
1264		$mes = e107::getMessage();
1265
1266
1267		// mailer options
1268		if(isset($userInfo['mail_options']) && is_array($userInfo['mail_options']))
1269		{
1270			$ret = $userInfo['mail_options'];
1271		}
1272
1273		// required for signup and quickadd email type
1274		e107::coreLan('signup');
1275
1276		$EMAIL_TEMPLATE = e107::getCoreTemplate('email');
1277
1278		if(!is_array($EMAIL_TEMPLATE)) //BC Fixes. pre v2 alpha3.
1279		{
1280			// load from old location. (root of theme folder if it exists)
1281
1282			$SIGNUPEMAIL_SUBJECT = '';
1283			$SIGNUPEMAIL_CC = '';
1284			$SIGNUPEMAIL_BCC = '';
1285			$SIGNUPEMAIL_ATTACHMENTS = '';
1286			$SIGNUPEMAIL_TEMPLATE = '';
1287
1288
1289			if (file_exists(THEME.'email_template.php'))
1290			{
1291				include(THEME.'email_template.php');
1292			}
1293			else
1294			{
1295				// include core default.
1296				include(e107::coreTemplatePath('email'));
1297			}
1298
1299			// BC Fixes.
1300			$EMAIL_TEMPLATE['signup']['subject'] 		= $SIGNUPEMAIL_SUBJECT;
1301			$EMAIL_TEMPLATE['signup']['cc']				= $SIGNUPEMAIL_CC;
1302			$EMAIL_TEMPLATE['signup']['bcc']			= $SIGNUPEMAIL_BCC;
1303			$EMAIL_TEMPLATE['signup']['attachments']	= $SIGNUPEMAIL_ATTACHMENTS;
1304			$EMAIL_TEMPLATE['signup']['body']			= $SIGNUPEMAIL_TEMPLATE;
1305
1306			$EMAIL_TEMPLATE['quickadduser']['body']	= vartrue($QUICKADDUSER_TEMPLATE['email_body'], '');
1307			$EMAIL_TEMPLATE['notify']['body']			= vartrue($NOTIFY_TEMPLATE['email_body'], '');
1308
1309		}
1310
1311		$template = '';
1312		switch ($type)
1313		{
1314			case 'signup':
1315				$template = (vartrue($SIGNUPPROVIDEREMAIL_TEMPLATE)) ? $SIGNUPPROVIDEREMAIL_TEMPLATE :  $EMAIL_TEMPLATE['signup']['body'];
1316				$ret['template'] = 'signup'; //  // false Don't allow additional headers (mailer) ??
1317			break;
1318
1319			case 'quickadd':
1320				$template = $EMAIL_TEMPLATE['quickadduser']['body'];
1321				$ret['template'] = 'quickadduser'; // Don't allow additional headers (mailer)
1322			break;
1323
1324			case 'notify':
1325				if(vartrue($userInfo['mail_body'])) $template = $userInfo['mail_body']; //$NOTIFY_HEADER.$userInfo['mail_body'].$NOTIFY_FOOTER;
1326				$ret['template'] = 'notify';
1327			break;
1328
1329			case 'email':
1330			case 'default':
1331				if(vartrue($userInfo['mail_body'])) $template = $userInfo['mail_body']; //$EMAIL_HEADER.$userInfo['mail_body'].$EMAIL_FOOTER;
1332				$ret['template'] = 'default';
1333			break;
1334		}
1335
1336		if(!$template)
1337		{
1338			$mes->addDebug('$template is empty in user_model.php line 1171.'); // Debug only, do not translate.
1339			return array();
1340		}
1341
1342
1343
1344	//
1345
1346		// signup email only
1347		if($type == 'signup')
1348		{
1349			$HEAD = '';
1350			$FOOT = '';
1351
1352			$pass_show = e107::pref('core','user_reg_secureveri', false);
1353
1354			$ret['e107_header'] = $userInfo['user_id'];
1355
1356			if (vartrue($EMAIL_TEMPLATE['signup']['cc'])) { $ret['email_copy_to'] = $EMAIL_TEMPLATE['signup']['cc']; }
1357			if (vartrue($EMAIL_TEMPLATE['signup']['bcc'])) { $ret['email_bcopy_to'] = $EMAIL_TEMPLATE['signup']['bcc']; }
1358			if (vartrue($userInfo['email_attach'])) { $ret['email_attach'] = $userInfo['mail_attach']; }
1359			elseif (vartrue($EMAIL_TEMPLATE['signup']['attachments'])) { $ret['email_attach'] = $EMAIL_TEMPLATE['signup']['attachments']; }
1360
1361			$style = vartrue($SIGNUPEMAIL_LINKSTYLE) ? "style='{$SIGNUPEMAIL_LINKSTYLE}'" : "";
1362
1363
1364			if(empty($userInfo['activation_url']) && !empty($userInfo['user_sess']) && !empty($userInfo['user_id']))
1365			{
1366				$userInfo['activation_url'] = SITEURL."signup.php?activate.".$userInfo['user_id'].".".$userInfo['user_sess'];
1367			}
1368
1369
1370			$sc = array();
1371
1372			$sc['LOGINNAME'] 		= intval($pref['allowEmailLogin']) === 0 ? $userInfo['user_loginname'] : $userInfo['user_email'];
1373			$sc['PASSWORD']			= ($pass_show && !empty($userInfo['user_password'])) ?  '*************' : $userInfo['user_password'];
1374			$sc['ACTIVATION_LINK']	= strpos($userInfo['activation_url'], 'http') === 0 ? '<a href="'.$userInfo['activation_url'].'">'.$userInfo['activation_url'].'</a>' : $userInfo['activation_url'];
1375		//	$sc['SITENAME']			= SITENAME;
1376			$sc['SITEURL']			= "<a href='".SITEURL."' {$style}>".SITEURL."</a>";
1377			$sc['USERNAME']			= $userInfo['user_name'];
1378			$sc['USERURL']			= vartrue($userInfo['user_website']) ? $userInfo['user_website'] : "";
1379			$sc['DISPLAYNAME']		= $userInfo['user_login'] ? $userInfo['user_login'] : $userInfo['user_name'];
1380			$sc['EMAIL']			= $userInfo['user_email'];
1381			$sc['ACTIVATION_URL']	= $userInfo['activation_url'];
1382
1383			$ret['subject'] =  $EMAIL_TEMPLATE['signup']['subject']; // $subject;
1384			$ret['send_html'] = TRUE;
1385			$ret['shortcodes'] = $sc;
1386
1387			if(!varset($EMAIL_TEMPLATE['signup']['header']))
1388			{
1389
1390				$HEAD = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n";
1391				$HEAD .= "<html xmlns='http://www.w3.org/1999/xhtml' >\n";
1392				$HEAD .= "<head><meta http-equiv='content-type' content='text/html; charset=utf-8' />\n";
1393				$HEAD .= ($SIGNUPEMAIL_USETHEME == 1) ? "<link rel=\"stylesheet\" href=\"".SITEURLBASE.THEME_ABS."style.css\" type=\"text/css\" />\n" : "";
1394			    $HEAD .= "<title>".LAN_SIGNUP_58."</title>\n";
1395
1396				if($SIGNUPEMAIL_USETHEME == 2) // @deprecated in favor of {STYLESHEET}
1397				{
1398					$CSS = file_get_contents(THEME."style.css");
1399					$HEAD .= "<style>\n".$CSS."\n</style>";
1400				}
1401
1402				$HEAD .= "</head>\n";
1403				if(vartrue($SIGNUPEMAIL_BACKGROUNDIMAGE)) // @deprecated.
1404				{
1405					$HEAD .= "<body background=\"".$SIGNUPEMAIL_BACKGROUNDIMAGE."\" >\n";
1406				}
1407				else
1408				{
1409					$HEAD .= "<body>\n";
1410				}
1411
1412			}
1413			else
1414			{
1415				$HEAD = ""; // $tp->parseTemplate($EMAIL_TEMPLATE['signup']['header'], true);
1416			}
1417
1418			if(!varset($EMAIL_TEMPLATE['signup']['footer']))
1419			{
1420				$FOOT = "\n</body>\n</html>\n";
1421			}
1422			else
1423			{
1424				$FOOT = ""; // $tp->parseTemplate($EMAIL_TEMPLATE['signup']['footer'], true);
1425			}
1426
1427			$ret['send_html'] 		= TRUE;
1428			$ret['email_body'] 		= $HEAD.$template.$FOOT; // e107::getParser()->parseTemplate(str_replace($search,$replace,$HEAD.$template.$FOOT), true);
1429			$ret['preview'] 		= $tp->parseTemplate($ret['email_body'],true, $sc);// Non-standard field
1430			$ret['shortcodes'] 		= $sc;
1431
1432
1433			return $ret;
1434		}
1435
1436
1437
1438
1439		// all other email types
1440		if(!$userInfo['mail_subject'])
1441		{
1442			$mes->addDebug('No Email subject provided to renderEmail() method.'); // Debug only, do not translate.
1443			return array();
1444		}
1445
1446
1447		$templateName = $ret['template'];
1448
1449//		$ret['email_subject'] 	=  varset($EMAIL_TEMPLATE[$templateName]['subject'], $EMAIL_TEMPLATE['default']['subject']) ; // $subject;
1450		$ret['subject']         = $userInfo['mail_subject'];
1451		$ret['e107_header'] 	= $userInfo['user_id'];
1452
1453		if (vartrue($userInfo['email_copy_to'])) 	{ 	$ret['email_copy_to']	= $userInfo['email_copy_to']; }
1454		if (vartrue($userInfo['email_bcopy_to'])) 	{ 	$ret['email_bcopy_to'] 	= $userInfo['email_bcopy_to']; }
1455		if (vartrue($userInfo['email_attach']))		{ 	$ret['email_attach'] 	= $userInfo['email_attach']; }
1456
1457		$sc = array();
1458
1459		$sc['LOGINNAME']			= intval($pref['allowEmailLogin']) === 0 ? $userInfo['user_loginname'] : $userInfo['user_email'];
1460		$sc['DISPLAYNAME']			= $userInfo['user_login'] ? $userInfo['user_login'] : $userInfo['user_name'];
1461		$sc['SITEURL']				= "<a href='".SITEURL."'>".SITEURL."</a>";
1462		$sc['USERNAME']				= $userInfo['user_name'];
1463		$sc['USERURL']				= vartrue($userInfo['user_website'], '');
1464		$sc['PASSWORD']				= vartrue($userInfo['user_password'], '***********');
1465		$sc['SUBJECT']				= $userInfo['mail_subject'];
1466
1467
1468		if(isset($userInfo['activation_url']))
1469		{
1470			$sc['ACTIVATION_URL']	= $userInfo['activation_url'];
1471			$sc['ACTIVATION_LINK']	= strpos($userInfo['activation_url'], 'http') === 0 ? '<a href="'.$userInfo['activation_url'].'">'.$userInfo['activation_url'].'</a>' : $userInfo['activation_url'];
1472		}
1473
1474		$ret['send_html'] 		= true;
1475		$ret['email_body'] 		= $template; // e107::getParser()->parseTemplate(str_replace($search, $replace, $template)); - performed in mail handler.
1476		$ret['preview'] 		= $ret['mail_body']; // Non-standard field
1477		$ret['shortcodes'] 		= $sc;
1478
1479		return $ret;
1480	}
1481}
1482
1483/**
1484 * Current system user
1485 * @author SecretR
1486 */
1487class e_user extends e_user_model
1488{
1489	private $_session_data = null;
1490	private $_session_key = null;
1491	private $_session_type = null;
1492	private $_session_error = false;
1493
1494	private $_parent_id = false;
1495	private $_parent_data = array();
1496	private $_parent_extmodel = null;
1497	private $_parent_extstruct = null;
1498	private $_parent_config = null;
1499
1500	/**
1501	 * @var e_user_provider|null
1502	 */
1503	protected $_provider;
1504
1505	public function __construct()
1506	{
1507		parent::__construct();
1508		$this->setSessionData() // retrieve data from current session
1509			->load() // load current user from DB
1510			->setEditor($this); // reference to self
1511	}
1512
1513	/**
1514	 * Yes, it's current user - return always true
1515	 * NOTE: it's not user check, use isUser() instead!
1516	 * @return boolean
1517	 */
1518	final public function isCurrent()
1519	{
1520		return true;
1521	}
1522
1523	/**
1524	 * Get parent user ID - present if main admin is browsing
1525	 * front-end logged in as another user account
1526	 *
1527	 * @return integer or false if not present
1528	 */
1529	final public function getParentId()
1530	{
1531		return $this->_parent_id;
1532	}
1533
1534	/**
1535	 * Init external user login/signup provider
1536	 * @return e_user
1537	 */
1538	public function initProvider()
1539	{
1540		if(null !== $this->_provider) return $this;
1541
1542		if($this->get('user_xup'))
1543		{
1544			$providerId = $this->getProviderName();
1545			$this->_provider = e107::getUserProvider($providerId);
1546		}
1547
1548		return $this;
1549	}
1550
1551	/**
1552	 * Get external user provider
1553	 * @return e_user_provider|null
1554	 */
1555	public function getProvider()
1556	{
1557		if(null === $this->_provider) $this->initProvider();
1558		return $this->_provider;
1559	}
1560
1561
1562	/**
1563	 * Set external user provider (already initialized)
1564	 * @return e_user
1565	 */
1566	public function setProvider($provider)
1567	{
1568		$this->_provider = $provider;
1569		return $this;
1570	}
1571
1572	/**
1573	 * Check if this user has assigned login provider
1574	 * @return boolean
1575	 */
1576	public function hasProvider()
1577	{
1578		return ($this->getProvider() !== null);
1579	}
1580
1581	/**
1582	 * User login
1583	 * @param string $uname
1584	 * @param string $upass_plain
1585	 * @param boolean $uauto
1586	 * @param string $uchallange
1587	 * @param boolean $noredirect
1588	 * @return boolean success
1589	 */
1590	final public function login($uname, $upass_plain, $uauto = false, $uchallange = false, $noredirect = true)
1591	{
1592		if($this->isUser()) return false;
1593
1594		$userlogin = new userlogin();
1595		$loginSuccess = $userlogin->login($uname, $upass_plain, $uauto, $uchallange, $noredirect);
1596
1597		$userdata  = $userlogin->getUserData();
1598		$this->setSessionData(true)->setData($userdata);
1599		if ($loginSuccess === false) return false;
1600
1601		e107::getEvent()->trigger('user_login', $userdata);
1602
1603		return $this->isUser();
1604	}
1605
1606	/**
1607	 * User login via external user provider
1608	 * @param string $xup external user provider identifier
1609	 * @return boolean success
1610	 */
1611	final public function loginProvider($xup)
1612	{
1613		if(!e107::getUserProvider()->isSocialLoginEnabled())  return false;
1614
1615		if($this->isUser()) return true;
1616
1617		$userlogin = new userlogin();
1618		$userlogin->login($xup, '', 'provider', false, true);
1619
1620		$userdata  = $userlogin->getUserData();
1621
1622		if(defset('E107_DEBUG_LEVEL', 0) > 0)
1623		{
1624			e107::getLog()->add('XUP Debug', (__CLASS__ . ':' . __METHOD__ . '-' . __LINE__), E_LOG_INFORMATIVE, "XUP_DEBUG");
1625		}
1626
1627		$this->setSessionData(true)->setData($userdata);
1628
1629		e107::getEvent()->trigger('user_xup_login', $userdata);
1630
1631		return $this->isUser();
1632	}
1633
1634	/**
1635	 * Login as another user account
1636	 * @param integer $user_id
1637	 * @return boolean success
1638	 */
1639	final public function loginAs($user_id)
1640	{
1641		// TODO - set session data required for loadAs()
1642		if($this->getParentId()
1643			|| !$this->isMainAdmin()
1644			|| empty($user_id)
1645			|| $this->getSessionDataAs()
1646			|| $user_id == $this->getId()
1647		) return false;
1648
1649		$key = $this->_session_key.'_as';
1650
1651		if('session' == $this->_session_type)
1652		{
1653			$_SESSION[$key] = $user_id;
1654		}
1655		elseif('cookie' == $this->_session_type)
1656		{
1657			$_COOKIE[$key] = $user_id;
1658			cookie($key, $user_id);
1659		}
1660
1661		// TODO - lan
1662		e107::getAdminLog()->log_event('Head Admin used Login As feature', 'Head Admin [#'.$this->getId().'] '.$this->getName().' logged in user account #'.$user_id);
1663		//$this->loadAs(); - shouldn't be called here - loginAs should be called in Admin area only, loadAs - front-end
1664		return true;
1665	}
1666
1667	/**
1668	 *
1669	 * @return e_user
1670	 */
1671	protected function _initConstants()
1672	{
1673		//FIXME - BC - constants from init_session() should be defined here
1674		// [SecretR] Not sure we should do this here, it's too restricting - constants can be
1675		// defined once, we need the freedom to do it multiple times - e.g. load() executed in constructor than login(), loginAs() etc.
1676		// called by a controller
1677		// We should switch to e.g. isAdmin() instead of ADMIN constant check
1678		return $this;
1679	}
1680
1681	/**
1682	 * Destroy cookie/session data, self destroy
1683	 * @return e_user
1684	 */
1685	final public function logout()
1686	{
1687		if($this->hasProvider())
1688		{
1689			$this->getProvider()->logout();
1690		}
1691		$this->logoutAs()
1692			->_destroySession();
1693
1694		parent::destroy();
1695		//if(session_id()) session_destroy();
1696		e107::getSession()->destroy();
1697
1698		e107::setRegistry('core/e107/current_user', null);
1699		return $this;
1700	}
1701
1702	/**
1703	 * Destroy cookie/session/model data for current user, resurrect parent user
1704	 * @return e_user
1705	 */
1706	final public function logoutAs()
1707	{
1708		if($this->getParentId())
1709		{
1710			// load parent user data
1711			$this->_extended_model = $this->_parent_extmodel;
1712			$this->_extended_structure = $this->_parent_extstruct;
1713			$this->_user_config = $this->_parent_config;
1714			if($this->_parent_model)
1715				$this->setData($this->_parent_model->getData());
1716
1717			// cleanup
1718			$this->_parent_id = false;
1719			$this->_parent_model = $this->_parent_extstruct = $this->_parent_extmodel = $this->_parent_config = null;
1720		}
1721		$this->_destroyAsSession();
1722		return $this;
1723	}
1724
1725	/**
1726	 * TODO load user data by cookie/session data
1727	 * @return e_user
1728	 */
1729	final public function load($force = false, $denyAs = false)
1730	{
1731		if(!$force && $this->getId()) return $this;
1732
1733		if(deftrue('e_ADMIN_AREA')) $denyAs = true;
1734
1735		// always run cli as main admin
1736		if(e107::isCli())
1737		{
1738			$this->_load(1, $force);
1739			$this->_initConstants();
1740			return $this;
1741		}
1742
1743		// We have active session
1744		if(null !== $this->_session_data)
1745		{
1746			list($uid, $upw) = explode('.', $this->_session_data);
1747			// Bad cookie - destroy session
1748			if(empty($uid) || !is_numeric($uid) || empty($upw))
1749			{
1750				$this->_destroyBadSession();
1751				$this->_initConstants();
1752				return $this;
1753			}
1754
1755			$udata = $this->_load($uid, $force);
1756			// Bad cookie - destroy session
1757			if(empty($udata))
1758			{
1759				$this->_destroyBadSession();
1760				$this->_initConstants();
1761				return $this;
1762			}
1763
1764			// we have a match
1765			if(md5($udata['user_password']) == $upw)
1766			{
1767				// set current user data
1768				$this->setData($udata);
1769
1770				// NEW - try 'logged in as' feature
1771				if(!$denyAs) $this->loadAs();
1772
1773				// update lastvisit field
1774				$this->updateVisit();
1775
1776				// currently does nothing
1777				$this->_initConstants();
1778
1779				// init any available external user provider
1780				if(e107::getUserProvider()->isSocialLoginEnabled()) $this->initProvider();
1781
1782				return $this;
1783			}
1784
1785			$this->_destroyBadSession();
1786			$this->_initConstants();
1787			return $this;
1788		}
1789
1790		return $this;
1791	}
1792
1793	final public function loadAs()
1794	{
1795		// FIXME - option to avoid it when browsing Admin area
1796		$loginAs = $this->getSessionDataAs();
1797		if(!$this->getParentId() && false !== $loginAs && $loginAs !== $this->getId() && $loginAs !== 1 && $this->isMainAdmin())
1798		{
1799			$uasdata = $this->_load($loginAs);
1800			if(!empty($uasdata))
1801			{
1802				// backup parent user data to prevent further db queries
1803				$this->_parent_id = $this->getId();
1804				$this->_parent_model = new e_user_model($this->getData());
1805				$this->setData($uasdata);
1806
1807				// not allowed - revert back
1808				if($this->isMainAdmin())
1809				{
1810					$this->_parent_id = false;
1811					$this->setData($this->_parent_model->getData());
1812					$this->_parent_model = null;
1813					$this->_destroyAsSession();
1814				}
1815				else
1816				{
1817					$this->_parent_extmodel = $this->_extended_model;
1818					$this->_parent_extstruct = $this->_extended_structure;
1819					$this->_user_config = $this->_parent_config;
1820					$this->_extended_model = $this->_extended_structure = $this->_user_config = null;
1821				}
1822			}
1823		}
1824		else
1825		{
1826			$this->_parent_id = false;
1827			$this->_parent_model = null;
1828			$this->_parent_extstruct = $this->_parent_extmodel = null;
1829		}
1830		return $this;
1831	}
1832
1833	/**
1834	 * Update user visit timestamp
1835	 * @return void
1836	 */
1837	protected function updateVisit()
1838	{
1839		// Don't update if main admin is logged in as current (non main admin) user
1840		if(!$this->getParentId())
1841		{
1842			$sql = e107::getDb();
1843			$this->set('last_ip', $this->get('user_ip'));
1844			$current_ip = e107::getIPHandler()->getIP(FALSE);
1845			$update_ip = $this->get('user_ip' != $current_ip ? ", user_ip = '".$current_ip."'" : "");
1846			$this->set('user_ip', $current_ip);
1847			if($this->get('user_currentvisit') + 3600 < time() || !$this->get('user_lastvisit'))
1848			{
1849				$this->set('user_lastvisit', (integer) $this->get('user_currentvisit'));
1850				$this->set('user_currentvisit', time());
1851				$sql->update('user', "user_visits = user_visits + 1, user_lastvisit = ".$this->get('user_lastvisit').", user_currentvisit = ".$this->get('user_currentvisit')."{$update_ip} WHERE user_id='".$this->getId()."' ");
1852			}
1853			else
1854			{
1855				$this->set('user_currentvisit', time());
1856				$sql->update('user', "user_currentvisit = ".$this->get('user_currentvisit')."{$update_ip} WHERE user_id='".$this->getId()."' ");
1857			}
1858		}
1859	}
1860
1861	final protected function _destroySession()
1862	{
1863		cookie($this->_session_key, '', (time() - 2592000));
1864		unset($_SESSION[$this->_session_key]);
1865
1866		return $this;
1867	}
1868
1869	final protected function _destroyAsSession()
1870	{
1871		$key = $this->_session_key.'_as';
1872		cookie($key, '', (time() - 2592000));
1873		$_SESSION[$key] = '';
1874		unset($_SESSION[$key]);
1875
1876		return $this;
1877	}
1878
1879	final protected function _destroyBadSession()
1880	{
1881		$this->_session_error = true;
1882		return $this->_destroySession();
1883	}
1884
1885	final public function getSessionDataAs()
1886	{
1887		$id = false;
1888		$key = $this->_session_key.'_as';
1889
1890		if('session' == $this->_session_type && isset($_SESSION[$key]) && !empty($_SESSION[$key]))
1891		{
1892			$id = $_SESSION[$key];
1893		}
1894		elseif('cookie' == $this->_session_type && isset($_COOKIE[$key]) && !empty($_COOKIE[$key]))
1895		{
1896			$id = $_COOKIE[$key];
1897		}
1898
1899		if(!empty($id) && is_numeric($id)) return intval($id);
1900
1901		return false;
1902	}
1903
1904	final public function setSessionData($force = false)
1905	{
1906		if($force || null === $this->_session_data)
1907		{
1908			$this->_session_data = null;
1909			$this->_session_key = e107::getPref('cookie_name', 'e107cookie');
1910			$this->_session_type = e107::getPref('user_tracking', 'cookie');
1911
1912			if('session' == $this->_session_type && isset($_SESSION[$this->_session_key]) && !empty($_SESSION[$this->_session_key]))
1913			{
1914				$this->_session_data = &$_SESSION[$this->_session_key];
1915			}
1916			elseif('cookie' == $this->_session_type && isset($_COOKIE[$this->_session_key]) && !empty($_COOKIE[$this->_session_key]))
1917			{
1918				$this->_session_data = &$_COOKIE[$this->_session_key];
1919			}
1920		}
1921
1922		return $this;
1923	}
1924
1925	public function hasSessionError()
1926	{
1927		return $this->_session_error;
1928	}
1929
1930
1931	final protected function _load($user_id)
1932	{
1933		$qry = 'SELECT u.*, ue.* FROM #user AS u LEFT JOIN #user_extended as ue ON u.user_id=ue.user_extended_id WHERE u.user_id='.intval($user_id);
1934		if(e107::getDb()->gen($qry))
1935		{
1936			return e107::getDb()->fetch();
1937		}
1938		return array();
1939	}
1940
1941	/**
1942	 * Not allowed
1943	 *
1944	 * @return e_user_model
1945	 */
1946	final protected function setAsTarget()
1947	{
1948		return $this;
1949	}
1950
1951	/**
1952	 * Not allowed
1953	 *
1954	 * @return e_user_model
1955	 */
1956	final protected function clearTarget()
1957	{
1958		return $this;
1959	}
1960
1961	public function destroy()
1962	{
1963		// not allowed - see logout()
1964	}
1965}
1966
1967class e_user_extended_model extends e_admin_model
1968{
1969	/**
1970	 * Describes known model fields
1971	 * @var array
1972	 */
1973	protected $_data_fields = array(
1974		'user_extended_id'	 => 'integer',
1975		'user_hidden_fields' => 'string',
1976	);
1977
1978	/**
1979	 * @see e_model
1980	 * @var string
1981	 */
1982	protected $_db_table = 'user_extended';
1983
1984	/**
1985	 * @see e_model
1986	 * @var string
1987	 */
1988	protected $_field_id = 'user_extended_id';
1989
1990	/**
1991	 * @see e_model
1992	 * @var string
1993	 */
1994	protected $_message_stack = 'user';
1995
1996	/**
1997	 * User class as set in user Adminsitration
1998	 *
1999	 * @var integer
2000	 */
2001	protected $_memberlist_access = null;
2002
2003	/**
2004	 * @var e_user_extended_structure_tree
2005	 */
2006	protected $_structure = null;
2007
2008	/**
2009	 * User model, the owner of extended fields model
2010	 * @var e_user_model
2011	 */
2012	protected $_user = null;
2013
2014	/**
2015	 * Stores access classes and default value per custom field
2016	 * @var array
2017	 */
2018	protected $_struct_index = array();
2019
2020	/**
2021	 * Constructor
2022	 * @param e_user_model $user_model
2023	 * @return void
2024	 */
2025	public function __construct(e_user_model $user_model)
2026	{
2027		$this->_memberlist_access = e107::getPref('memberlist_access');
2028		$this->setUser($user_model)
2029			->load();
2030	}
2031
2032	/**
2033	 * Always return integer
2034	 */
2035	public function getId()
2036	{
2037		return (integer) parent::getId();
2038	}
2039
2040	/**
2041	 * Get user model
2042	 * @return e_user_model
2043	 */
2044	public function getUser()
2045	{
2046		return $this->_user;
2047	}
2048
2049	/**
2050	 * Set User model
2051	 * @param e_user_model $user_model
2052	 * @return e_user_extended_model
2053	 */
2054	public function setUser($user_model)
2055	{
2056		$this->_user = $user_model;
2057		return $this;
2058	}
2059
2060	/**
2061	 * Get current user editor model
2062	 * @return e_user_model
2063	 */
2064	public function getEditor()
2065	{
2066		return $this->getUser()->getEditor();
2067	}
2068
2069	/**
2070	 * Bad but required (BC) method of retrieving all user data
2071	 * It's here to be used from get_user_data() core function.
2072	 * DON'T USE IT unless you have VERY good reason to do it.
2073	 * TODO - revise this! Merge it to getSystemData, getApplicableData
2074	 *
2075	 * @return array
2076	 */
2077	public function getExtendedData()
2078	{
2079		$ret = array();
2080
2081		$fields = $this->getExtendedStructure()->getFieldTree();
2082		foreach ($fields as $id => $field)
2083		{
2084			$value = $this->getValue($field->getValue('name'));
2085			if(null !== $value) $ret[$field->getValue('name')] = $value;
2086		}
2087
2088		$ret['user_extended_id'] = $this->getId();
2089		$ret['user_hidden_fields'] = $this->get('user_hidden_fields');
2090
2091		return $ret;
2092	}
2093
2094	/**
2095	 * Get User extended field value. It performs all required read/applicable permission checks
2096	 * against current editor/user.
2097	 * Returns NULL when field/default value not found or not enough permissions
2098	 * @param string $field
2099	 * @param boolean $short if true, 'user_' prefix will be added to field name
2100	 * @param boolean $raw doesn't retrieve db value when true (no sql query)
2101	 * @return mixed
2102	 */
2103	public function getValue($field, $short = true, $raw = false)
2104	{
2105		if($short) $field = 'user_'.$field;
2106		if (!$this->checkRead($field))
2107			return null;
2108		if(!$raw && vartrue($this->_struct_index[$field]['db']))
2109		{
2110			return $this->getDbValue($field);
2111		}
2112		return $this->get($field, $this->getDefault($field));
2113	}
2114
2115	/**
2116	 * Set User extended field value, only if current editor has write permissions and field
2117	 * is applicable for the current user.
2118	 * Note: Data is not sanitized!
2119	 * @param string $field
2120	 * @param mixed $value
2121	 * @param boolean $short if true, 'user_' prefix will be added to field name
2122	 * @return e_user_extended_model
2123	 */
2124	public function setValue($field, $value, $short = true)
2125	{
2126		if($short) $field = 'user_'.$field;
2127		if (!$this->checkWrite($field))
2128			return $this;
2129
2130		$this->set($field, $value, true);
2131		return $this;
2132	}
2133
2134	/**
2135	 * Retrieve value of a field of type 'db'. It does sql request only once.
2136	 *
2137	 * @param string $field field name
2138	 * @return mixed db value
2139	 */
2140	protected function getDbValue($field)
2141	{
2142		if(null !== $this->_struct_index[$field]['db_value'])
2143		{
2144			return $this->_struct_index[$field]['db_value'];
2145		}
2146
2147		// retrieve db data
2148		$value = $this->get($field);
2149		list($table, $field_id, $field_name, $field_order) = explode(',', $this->_struct_index[$field]['db'], 4);
2150		$this->_struct_index[$field]['db_value'] = $value;
2151		if($value && $table && $field_id && $field_name && e107::getDb()->db_Select($table, $field_name, "{$field_id}='{$value}'"))
2152		{
2153			$res = e107::getDb()->db_Fetch();
2154			$this->_struct_index[$field]['db_value'] = $res[$field_name];
2155		}
2156
2157		return $this->_struct_index[$field]['db_value'];
2158	}
2159
2160	/**
2161	 * System getter. It doesn't perform any read/applicable permission checks
2162	 * against current editor/user.
2163	 * It's here to serve in your application logic.
2164	 *
2165	 * @param string $field
2166	 * @param boolean $short if true, 'user_' prefix will be added to field name
2167	 * @param boolean $raw don't retrieve db value
2168	 * @return mixed
2169	 */
2170	public function getSystem($field, $short = true, $raw = true)
2171	{
2172		if($short) $field = 'user_'.$field;
2173
2174		if(!$raw && vartrue($this->_struct_index[$field]['db']))
2175		{
2176			return $this->getDbValue($field);
2177		}
2178		return $this->get($field, $this->getDefault($field));
2179	}
2180
2181	/**
2182	 * System setter. It doesn't perform any write/applicable permission checks
2183	 * against current editor/user.
2184	 * It's here to serve in your application logic.
2185	 * NOTE: untrusted data should be provided via setPosted() method!
2186	 *
2187	 * @param string $field
2188	 * @param mixed $value
2189	 * @param boolean $short if true, 'user_' prefix will be added to field name
2190	 * @param boolean $strict if false no Applicable check will be made
2191	 * @return e_user_model
2192	 */
2193	public function setSystem($field, $value, $short = true, $strict = true)
2194	{
2195		if($short) $field = 'user_'.$field;
2196
2197		$this->set($field, $value, $strict);
2198		return $this;
2199	}
2200
2201	public function getReadData()
2202	{
2203		// TODO array allowed user profile page data (read mode)
2204	}
2205
2206	public function getWriteData()
2207	{
2208		// TODO array allowed user settings page data (edit mode)
2209	}
2210
2211	/**
2212	 * Get default field value, defined by extended field structure
2213	 * Returns NULL if field/default value not found
2214	 * @param string $field
2215	 * @return mixed
2216	 */
2217	public function getDefault($field)
2218	{
2219		return varset($this->_struct_index[$field]['default'], null);
2220	}
2221
2222	/**
2223	 * Check field read permissions against current editor
2224	 * @param string $field
2225	 * @return boolean
2226	 */
2227	public function checkRead($field)
2228	{
2229		$hidden = $this->get('user_hidden_fields');
2230		$editor = $this->getEditor();
2231
2232		if(!empty($hidden) && $this->getId() !== $editor->getId() && strpos($hidden, '^'.$field.'^') !== false) return false;
2233
2234		return ($this->checkApplicable($field) && $editor->checkClass($this->_memberlist_access) && $editor->checkClass(varset($this->_struct_index[$field]['read'])));
2235	}
2236
2237	/**
2238	 * Check field write permissions against current editor
2239	 * @param string $field
2240	 * @return boolean
2241	 */
2242	public function checkWrite($field)
2243	{
2244		if(!$this->checkApplicable($field)) return false;
2245
2246		$editor = $this->getEditor();
2247		// Main admin checked later in checkClass() method
2248		if($editor->checkAdminPerms('4') && varset($this->_struct_index[$field]['write']) != e_UC_NOBODY)
2249			return true;
2250
2251		return $editor->checkClass(varset($this->_struct_index[$field]['write']));
2252	}
2253
2254	/**
2255	 * Check field signup permissions
2256	 * @param string $field
2257	 * @return boolean
2258	 */
2259	public function checkSignup($field)
2260	{
2261		return $this->getUser()->checkClass(varset($this->_struct_index[$field]['signup']));
2262	}
2263
2264	/**
2265	 * Check field applicable permissions against current user
2266	 * @param string $field
2267	 * @return boolean
2268	 */
2269	public function checkApplicable($field)
2270	{
2271		return $this->getUser()->checkClass(varset($this->_struct_index[$field]['apply']));
2272	}
2273
2274	/**
2275	 * @see e_model#load($id, $force)
2276	 * @return e_user_extended_model
2277	 */
2278	public function load($id=null, $force = false)
2279	{
2280		if ($this->getId() && !$force)
2281			return $this;
2282
2283		$this->_loadDataAndAccess();
2284		return $this;
2285	}
2286
2287	/**
2288	 * Check if given field name is present in extended user table structure
2289	 *
2290	 * @param string $field
2291	 * @param boolean $short
2292	 * @return boolean
2293	 */
2294	public function isField($field, $short = true)
2295	{
2296		if($short) $field = 'user_'.$field;
2297		return (isset($this->_struct_index[$field]) || in_array($field, array($this->getFieldIdName(), 'user_hidden_fields')));
2298	}
2299
2300	/**
2301	 * Load extended fields permissions once (performance)
2302	 * @return e_user_extended_model
2303	 */
2304	protected function _loadDataAndAccess()
2305	{
2306		$struct_tree = $this->getExtendedStructure();
2307		$user = $this->getUser();
2308		if ($user && $struct_tree->hasTree())
2309		{
2310			// load structure dependencies
2311			$ignore = array($this->getFieldIdName(), 'user_hidden_fields');
2312
2313			// set ignored values
2314			foreach ($ignore as $field_name)
2315			{
2316				$this->set($field_name, $user->get($field_name));
2317			}
2318
2319			$fields = $struct_tree->getTree();
2320			foreach ($fields as $id => $field)
2321			{
2322				$field_name = 'user_'.$field->getValue('name');
2323				$this->set($field_name, $user->get($field_name));
2324				if (!in_array($field->getValue('name'), $ignore))
2325				{
2326					$this->_struct_index[$field_name] = array(
2327						'db'		 => $field->getValue('type') == 4 ? $field->getValue('values') : '',
2328						'db_value'	 => null, // used later for caching DB results
2329						'read'		 => $field->getValue('read'),
2330						'write'		 => $field->getValue('write'),
2331						'signup'	 => $field->getValue('signup'),
2332						'apply'		 => $field->getValue('applicable'),
2333						'default'	 => $field->getValue('default'),
2334					);
2335				}
2336			}
2337		}
2338		return $this;
2339	}
2340
2341	/**
2342	 * Build manage rules for single field
2343	 * @param $structure_model
2344	 * @return e_user_extended_model
2345	 */
2346	protected function _buildManageField(e_user_extended_structure_model $structure_model)
2347	{
2348		$ftype = $structure_model->getValue('type') == 6 ? 'integer' : 'string';
2349
2350		// 0- field control (html) attributes;1 - regex; 2 - validation error msg;
2351		$parms = explode('^,^', $structure_model->getValue('parms'));
2352
2353		// validaton rules
2354		$vtype = $parms[1] ? 'regex' : $ftype;
2355		$name = 'user_'.$structure_model->getValue('name');
2356		$this->setValidationRule($name, array($vtype, $parms[1], $structure_model->getValue('text'), $parms[2]), $structure_model->getValue('required'));
2357
2358		// data type, required for sql query
2359		$this->_data_fields[$name] = $ftype;
2360		return $this;
2361	}
2362
2363	/**
2364	 * Build manage rules for single field
2365	 * @param $structure_model
2366	 * @return e_user_extended_model
2367	 */
2368	protected function _buildManageRules()
2369	{
2370		$struct_tree = $this->getExtendedStructure();
2371		if ($this->getId() && $struct_tree->hasTree())
2372		{
2373			// load structure dependencies TODO protected fields check as method
2374			$ignore = array($this->getFieldIdName(), 'user_hidden_fields'); // TODO - user_hidden_fields? Old?
2375			$fields = $struct_tree->getTree();
2376			foreach ($fields as $id => $field)
2377			{
2378				if (!in_array('user_'.$field->getValue('name'), $ignore) && !$field->isCategory())
2379				{
2380					// build _data_type and rules
2381					$this->_buildManageField($field);
2382				}
2383			}
2384		}
2385		return $this;
2386	}
2387
2388	/**
2389	 * Get extended structure tree
2390	 * @return e_user_extended_structure_tree
2391	 */
2392	public function getExtendedStructure()
2393	{
2394		if (null === $this->_structure)
2395			$this->_structure = e107::getUserStructure();
2396		return $this->_structure;
2397	}
2398
2399	/**
2400	 * Additional security while applying posted
2401	 * data to user extended model
2402	 * @return e_user_extended_model
2403	 */
2404	public function mergePostedData($strict = true, $sanitize = true, $validate = true)
2405    {
2406    	$posted = $this->getPostedData();
2407    	foreach ($posted as $key => $value)
2408    	{
2409    		if(!$this->checkWrite($key))
2410    		{
2411    			$this->removePosted($key);
2412    		}
2413    	}
2414		parent::mergePostedData(true, true, true);
2415		return $this;
2416    }
2417
2418	/**
2419	 * Build data types and rules on the fly and save
2420	 * @see e_front_model::save()
2421	 */
2422	public function save($from_post = true, $force = false, $session = false)
2423	{
2424		// when not loaded from db, see the construct check
2425		if(!$this->getId())
2426		{
2427			$this->setId($this->getUser()->getId());
2428		}
2429		$this->_buildManageRules();
2430		// insert new record
2431		if(!e107::getDb()->db_Count('user_extended', '(user_extended_id)', "user_extended_id=".$this->getId()))
2432		{
2433			return $this->insert(true, $session);
2434		}
2435		return parent::save(true, $force, $session);
2436	}
2437
2438	/**
2439	 * Doesn't save anything actually...
2440	 */
2441	public function saveDebug($return = false, $undo = true)
2442	{
2443		$this->_buildManageRules();
2444		return parent::saveDebug($return, $undo);
2445	}
2446}
2447
2448class e_user_extended_structure_model extends e_model
2449{
2450	/**
2451	 * @see e_model
2452	 * @var string
2453	 */
2454	protected $_db_table = 'user_extended_struct';
2455
2456	/**
2457	 * @see e_model
2458	 * @var string
2459	 */
2460	protected $_field_id = 'user_extended_struct_id';
2461
2462	/**
2463	 * @see e_model
2464	 * @var string
2465	 */
2466	protected $_message_stack = 'user_struct';
2467
2468	/**
2469	 * Get User extended structure field value
2470	 *
2471	 * @param string$field
2472	 * @param string $default
2473	 * @return mixed
2474	 */
2475	public function getValue($field, $default = '')
2476	{
2477		$field = 'user_extended_struct_'.$field;
2478		return $this->get($field, $default);
2479	}
2480
2481	/**
2482	 * Set User extended structure field value
2483	 *
2484	 * @param string $field
2485	 * @param mixed $value
2486	 * @return e_user_model
2487	 */
2488	public function setValue($field, $value)
2489	{
2490		$field = 'user_extended_struct_'.$field;
2491		$this->set($field, $value, false);
2492		return $this;
2493	}
2494
2495	public function isCategory()
2496	{
2497		return ($this->getValue('type') ? false : true);
2498	}
2499
2500	public function getCategoryId()
2501	{
2502		return $this->getValue('parent');
2503	}
2504
2505	public function getLabel()
2506	{
2507		$label = $this->isCategory() ? $this->getValue('name') : $this->getValue('text');
2508		return defset($label, $label);
2509	}
2510
2511	/**
2512	 * Loading of single structure row not allowed for front model
2513	 */
2514	public function load($id = null, $force = false)
2515	{
2516		return $this;
2517	}
2518}
2519
2520class e_user_extended_structure_tree extends e_tree_model
2521{
2522	/**
2523	 * @see e_model
2524	 * @var string
2525	 */
2526	protected $_db_table = 'user_extended_struct';
2527
2528	/**
2529	 * @see e_model
2530	 * @var string
2531	 */
2532	protected $_field_id = 'user_extended_struct_id';
2533
2534	/**
2535	 * @see e_model
2536	 * @var string
2537	 */
2538	protected $_message_stack = 'user';
2539
2540	/**
2541	 * @var string
2542	 */
2543	protected $_cache_string = 'nomd5_user_extended_struct';
2544
2545	/**
2546	 * Force system cache (cache used even if disabled by site admin)
2547	 * @var boolen
2548	 */
2549	protected $_cache_force = true;
2550
2551	/**
2552	 * Index for speed up retrieving by name routine
2553	 * @var array
2554	 */
2555	protected $_name_index = array();
2556
2557	/**
2558	 * Category Index - numerical array of id's
2559	 * @var array
2560	 */
2561	protected $_category_index = array();
2562
2563	/**
2564	 * Items by category list
2565	 * @var array
2566	 */
2567	protected $_parent_index = array();
2568
2569	/**
2570	 * Constructor - auto-load
2571	 * @return void
2572	 */
2573	public function __construct()
2574	{
2575		$this->loadBatch();
2576	}
2577
2578	/**
2579	 * @param string $name name field value
2580	 * @return e_user_extended_structure_model
2581	 */
2582	public function getNodeByName($name)
2583	{
2584		if ($this->isNodeName($name))
2585		{
2586			return $this->getNode($this->getNodeId($name));
2587		}
2588		return null;
2589	}
2590
2591	/**
2592	 * Check if node exists by its name field value
2593	 * @param string $name
2594	 * @return boolean
2595	 */
2596	public function isNodeName($name)
2597	{
2598		return (isset($this->_name_index[$name]) && $this->isNode($this->_name_index[$name]));
2599	}
2600
2601	/**
2602	 * Get node ID by node name field
2603	 * @param string $name
2604	 * @return integer
2605	 */
2606	public function getNodeId($name)
2607	{
2608		return (isset($this->_name_index[$name]) ? $this->_name_index[$name] : null);
2609	}
2610
2611	/**
2612	 * Get collection of nodes of type category
2613	 * @return array
2614	 */
2615	public function getCategoryTree()
2616	{
2617		return $this->_array_intersect_key($this->getTree(), array_combine($this->_category_index, $this->_category_index));
2618	}
2619
2620	/**
2621	 * Get collection of nodes of type field
2622	 * @return array
2623	 */
2624	public function getFieldTree()
2625	{
2626		return array_diff_key($this->getTree(), array_combine($this->_category_index, $this->_category_index));
2627	}
2628
2629	/**
2630	 * Get collection of nodes assigned to a specific category
2631	 * @param integer $category_id
2632	 * @return array
2633	 */
2634	public function getTreeByCategory($category_id)
2635	{
2636		if(!isset($this->_parent_index[$category_id]) || empty($this->_parent_index[$category_id])) return array();
2637		return $this->_array_intersect_key($this->getTree(), array_combine($this->_parent_index[$category_id], $this->_parent_index[$category_id]));
2638	}
2639
2640	/**
2641	 * Load tree data
2642	 *
2643	 * @param boolean $force
2644	 */
2645	public function loadBatch($force = false)
2646	{
2647		$this->setParam('nocount', true)
2648			->setParam('model_class', 'e_user_extended_structure_model')
2649			->setParam('db_order', 'user_extended_struct_order ASC');
2650		parent::loadBatch($force);
2651
2652		return $this;
2653	}
2654
2655	/**
2656	 * Build all indexes on load
2657	 * (New) This method is auto-triggered by core load() method
2658	 * @param e_user_extended_structure_model $model
2659	 */
2660	protected function _onLoad($model)
2661	{
2662		if($model->isCategory())
2663		{
2664			$this->_category_index[] = $model->getId();
2665		}
2666		else
2667		{
2668			$this->_name_index['user_'.$model->getValue('name')] = $model->getId();
2669			$this->_parent_index[$model->getCategoryId()][] = $model->getId();
2670		}
2671		return $this;
2672	}
2673
2674	/**
2675	 * Compatibility - array_intersect_key() available since PHP 5.1
2676	 *
2677	 * @see http://php.net/manual/en/function.array-intersect-key.php
2678	 * @param array $array1
2679	 * @param array $array2
2680	 * @return array
2681	 */
2682	protected function _array_intersect_key($array1, $array2)
2683	{
2684		if(function_exists('array_intersect_key')) return array_intersect_key($array1, $array2);
2685
2686		$ret = array();
2687		foreach ($array1 as $k => $v)
2688		{
2689			if(isset($array2[$k])) $ret[$k] = $v;
2690		}
2691		return $ret;
2692	}
2693}
2694
2695class e_user_pref extends e_front_model
2696{
2697	/**
2698	 * @var e_user_model
2699	 */
2700	protected $_user;
2701
2702	/**
2703	 * Constructor
2704	 * @param e_user_model $user_model
2705	 * @return void
2706	 */
2707	public function __construct(e_user_model $user_model)
2708	{
2709		$this->_user = $user_model;
2710		$this->load();
2711	}
2712
2713	/**
2714	 * Load data from user preferences string
2715	 * @param boolean $force
2716	 * @return e_user_pref
2717	 */
2718	public function load($id = null, $force = false)
2719	{
2720		if($force || !$this->hasData())
2721		{
2722			$data = $this->_user->get('user_prefs', '');
2723			if(!empty($data))
2724			{
2725				// BC
2726				$data = substr($data, 0, 5) == "array" ? e107::unserialize($data) : unserialize($data);
2727				if(!$data) $data = array();
2728			}
2729			else $data = array();
2730
2731			$this->setData($data);
2732		}
2733		return $this;
2734	}
2735
2736	/**
2737	 * Apply current data to user data
2738	 * @return e_user_pref
2739	 */
2740	public function apply()
2741	{
2742		$data = $this->hasData() ? $this->toString(true) : '';
2743		$this->_user->set('user_prefs', $data);
2744		return $this;
2745	}
2746
2747	/**
2748	 * Save and apply user preferences
2749	 * @param boolean $from_post
2750	 * @param boolean $force
2751	 * @return boolean success
2752	 */
2753	public function save($from_post = false, $force = false, $session_messages = false)
2754	{
2755		if($this->_user->getId())
2756		{
2757			if($from_post)
2758			{
2759				$this->mergePostedData(false, true, false);
2760			}
2761			if($force || $this->dataHasChanged())
2762			{
2763				$data = $this->toString(true);
2764				$this->apply();
2765				return (e107::getDb('user_prefs')->update('user', "user_prefs='{$data}' WHERE user_id=".$this->_user->getId()) ? true : false);
2766			}
2767			return 0;
2768		}
2769		return false;
2770	}
2771
2772	/**
2773	 * Remove & apply user preferences, optionally - save to DB
2774	 * @return boolean success
2775	 */
2776	public function delete($ids, $destroy = true, $session_messages = false) // replaced $save = false for PHP7 fix.
2777	{
2778		$this->removeData()->apply();
2779	//	if($save) return $this->save(); //FIXME adjust within the context of the variables in the method.
2780		return true;
2781	}
2782}
2783