1<?php
2// Copyright (C) 2010-2018 Combodo SARL
3//
4//   This file is part of iTop.
5//
6//   iTop is free software; you can redistribute it and/or modify
7//   it under the terms of the GNU Affero General Public License as published by
8//   the Free Software Foundation, either version 3 of the License, or
9//   (at your option) any later version.
10//
11//   iTop is distributed in the hope that it will be useful,
12//   but WITHOUT ANY WARRANTY; without even the implied warranty of
13//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//   GNU Affero General Public License for more details.
15//
16//   You should have received a copy of the GNU Affero General Public License
17//   along with iTop. If not, see <http://www.gnu.org/licenses/>
18
19
20/**
21 * Typology for the attributes
22 *
23 * @copyright   Copyright (C) 2010-2018 Combodo SARL
24 * @license     http://opensource.org/licenses/AGPL-3.0
25 */
26
27
28require_once('MyHelpers.class.inc.php');
29require_once('ormdocument.class.inc.php');
30require_once('ormstopwatch.class.inc.php');
31require_once('ormpassword.class.inc.php');
32require_once('ormcaselog.class.inc.php');
33require_once('ormlinkset.class.inc.php');
34require_once('ormset.class.inc.php');
35require_once('ormtagset.class.inc.php');
36require_once('htmlsanitizer.class.inc.php');
37require_once(APPROOT.'sources/autoload.php');
38require_once('customfieldshandler.class.inc.php');
39require_once('ormcustomfieldsvalue.class.inc.php');
40require_once('datetimeformat.class.inc.php');
41// This should be changed to a use when we go full-namespace
42require_once(APPROOT.'sources/form/validator/validator.class.inc.php');
43require_once(APPROOT.'sources/form/validator/notemptyextkeyvalidator.class.inc.php');
44
45/**
46 * MissingColumnException - sent if an attribute is being created but the column is missing in the row
47 *
48 * @package     iTopORM
49 */
50class MissingColumnException extends Exception
51{
52}
53
54/**
55 * add some description here...
56 *
57 * @package     iTopORM
58 */
59define('EXTKEY_RELATIVE', 1);
60
61/**
62 * add some description here...
63 *
64 * @package     iTopORM
65 */
66define('EXTKEY_ABSOLUTE', 2);
67
68/**
69 * Propagation of the deletion through an external key - ask the user to delete the referencing object
70 *
71 * @package     iTopORM
72 */
73define('DEL_MANUAL', 1);
74
75/**
76 * Propagation of the deletion through an external key - ask the user to delete the referencing object
77 *
78 * @package     iTopORM
79 */
80define('DEL_AUTO', 2);
81/**
82 * Fully silent delete... not yet implemented
83 */
84define('DEL_SILENT', 2);
85/**
86 * For HierarchicalKeys only: move all the children up one level automatically
87 */
88define('DEL_MOVEUP', 3);
89
90
91/**
92 * For Link sets: tracking_level
93 *
94 * @package     iTopORM
95 */
96define('ATTRIBUTE_TRACKING_NONE', 0); // Do not track changes of the attribute
97define('ATTRIBUTE_TRACKING_ALL', 3); // Do track all changes of the attribute
98define('LINKSET_TRACKING_NONE', 0); // Do not track changes in the link set
99define('LINKSET_TRACKING_LIST', 1); // Do track added/removed items
100define('LINKSET_TRACKING_DETAILS', 2); // Do track modified items
101define('LINKSET_TRACKING_ALL', 3); // Do track added/removed/modified items
102
103define('LINKSET_EDITMODE_NONE', 0); // The linkset cannot be edited at all from inside this object
104define('LINKSET_EDITMODE_ADDONLY', 1); // The only possible action is to open a new window to create a new object
105define('LINKSET_EDITMODE_ACTIONS', 2); // Show the usual 'Actions' popup menu
106define('LINKSET_EDITMODE_INPLACE', 3); // The "linked" objects can be created/modified/deleted in place
107define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/removed in place
108
109
110/**
111 * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.)
112 *
113 * @package     iTopORM
114 */
115abstract class AttributeDefinition
116{
117	const SEARCH_WIDGET_TYPE_RAW = 'raw';
118	const SEARCH_WIDGET_TYPE_STRING = 'string';
119	const SEARCH_WIDGET_TYPE_NUMERIC = 'numeric';
120	const SEARCH_WIDGET_TYPE_ENUM = 'enum';
121	const SEARCH_WIDGET_TYPE_EXTERNAL_KEY = 'external_key';
122	const SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY = 'hierarchical_key';
123	const SEARCH_WIDGET_TYPE_EXTERNAL_FIELD = 'external_field';
124	const SEARCH_WIDGET_TYPE_DATE_TIME = 'date_time';
125	const SEARCH_WIDGET_TYPE_DATE = 'date';
126	const SEARCH_WIDGET_TYPE_SET = 'set';
127	const SEARCH_WIDGET_TYPE_TAG_SET = 'tag_set';
128
129
130	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
131
132	const INDEX_LENGTH = 95;
133
134	protected $aCSSClasses;
135
136	public function GetType()
137	{
138		return Dict::S('Core:'.get_class($this));
139	}
140
141	public function GetTypeDesc()
142	{
143		return Dict::S('Core:'.get_class($this).'+');
144	}
145
146	abstract public function GetEditClass();
147
148	/**
149	 * Return the search widget type corresponding to this attribute
150	 *
151	 * @return string
152	 */
153	public function GetSearchType()
154	{
155		return static::SEARCH_WIDGET_TYPE;
156	}
157
158	/**
159	 * @return bool
160	 */
161	public function IsSearchable()
162	{
163		return static::SEARCH_WIDGET_TYPE != static::SEARCH_WIDGET_TYPE_RAW;
164	}
165
166	protected $m_sCode;
167	private $m_aParams = array();
168	protected $m_sHostClass = '!undefined!';
169
170	public function Get($sParamName)
171	{
172		return $this->m_aParams[$sParamName];
173	}
174
175	public function GetIndexLength()
176	{
177		$iMaxLength = $this->GetMaxSize();
178		if (is_null($iMaxLength))
179		{
180			return null;
181		}
182		if ($iMaxLength > static::INDEX_LENGTH)
183		{
184			return static::INDEX_LENGTH;
185		}
186
187		return $iMaxLength;
188	}
189
190	public function IsParam($sParamName)
191	{
192		return (array_key_exists($sParamName, $this->m_aParams));
193	}
194
195	protected function GetOptional($sParamName, $default)
196	{
197		if (array_key_exists($sParamName, $this->m_aParams))
198		{
199			return $this->m_aParams[$sParamName];
200		}
201		else
202		{
203			return $default;
204		}
205	}
206
207	/**
208	 * AttributeDefinition constructor.
209	 *
210	 * @param string $sCode
211	 * @param array $aParams
212	 *
213	 * @throws \Exception
214	 */
215	public function __construct($sCode, $aParams)
216	{
217		$this->m_sCode = $sCode;
218		$this->m_aParams = $aParams;
219		$this->ConsistencyCheck();
220		$this->aCSSClasses = array('attribute');
221	}
222
223	public function GetParams()
224	{
225		return $this->m_aParams;
226	}
227
228	public function HasParam($sParam)
229	{
230		return array_key_exists($sParam, $this->m_aParams);
231	}
232
233	public function SetHostClass($sHostClass)
234	{
235		$this->m_sHostClass = $sHostClass;
236	}
237
238	public function GetHostClass()
239	{
240		return $this->m_sHostClass;
241	}
242
243	/**
244	 * @return array
245	 *
246	 * @throws \CoreException
247	 */
248	public function ListSubItems()
249	{
250		$aSubItems = array();
251		foreach(MetaModel::ListAttributeDefs($this->m_sHostClass) as $sAttCode => $oAttDef)
252		{
253			if ($oAttDef instanceof AttributeSubItem)
254			{
255				if ($oAttDef->Get('target_attcode') == $this->m_sCode)
256				{
257					$aSubItems[$sAttCode] = $oAttDef;
258				}
259			}
260		}
261
262		return $aSubItems;
263	}
264
265	// Note: I could factorize this code with the parameter management made for the AttributeDef class
266	// to be overloaded
267	static public function ListExpectedParams()
268	{
269		return array();
270	}
271
272	/**
273	 * @throws \Exception
274	 */
275	private function ConsistencyCheck()
276	{
277		// Check that any mandatory param has been specified
278		//
279		$aExpectedParams = $this->ListExpectedParams();
280		foreach($aExpectedParams as $sParamName)
281		{
282			if (!array_key_exists($sParamName, $this->m_aParams))
283			{
284				$aBacktrace = debug_backtrace();
285				$sTargetClass = $aBacktrace[2]["class"];
286				$sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"];
287				throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)");
288			}
289		}
290	}
291
292	/**
293	 * Check the validity of the given value
294	 *
295	 * @param \DBObject $oHostObject
296	 * @param $value Object error if any, null otherwise
297	 *
298	 * @return bool
299	 */
300	public function CheckValue(DBObject $oHostObject, $value)
301	{
302		// later: factorize here the cases implemented into DBObject
303		return true;
304	}
305
306	// table, key field, name field
307
308	/**
309	 * @return string
310	 * @deprecated never used
311	 */
312	public function ListDBJoins()
313	{
314		return "";
315		// e.g: return array("Site", "infrid", "name");
316	}
317
318	public function GetFinalAttDef()
319	{
320		return $this;
321	}
322
323	/**
324	 * Deprecated - use IsBasedOnDBColumns instead
325	 *
326	 * @return bool
327	 */
328	public function IsDirectField()
329	{
330		return static::IsBasedOnDBColumns();
331	}
332
333	/**
334	 * Returns true if the attribute value is built after DB columns
335	 *
336	 * @return bool
337	 */
338	static public function IsBasedOnDBColumns()
339	{
340		return false;
341	}
342
343	/**
344	 * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via
345	 * GetOQLExpression)
346	 *
347	 * @return bool
348	 */
349	static public function IsBasedOnOQLExpression()
350	{
351		return false;
352	}
353
354	/**
355	 * Returns true if the attribute value can be shown as a string
356	 *
357	 * @return bool
358	 */
359	static public function IsScalar()
360	{
361		return false;
362	}
363
364	/**
365	 * Returns true if the attribute value is a set of related objects (1-N or N-N)
366	 *
367	 * @return bool
368	 */
369	static public function IsLinkSet()
370	{
371		return false;
372	}
373
374	/**
375	 * @param int $iType
376	 *
377	 * @return bool true if the attribute is an external key, either directly (RELATIVE to the host class), or
378	 *     indirectly (ABSOLUTELY)
379	 */
380	public function IsExternalKey($iType = EXTKEY_RELATIVE)
381	{
382		return false;
383	}
384
385	/**
386	 * @return bool true if the attribute value is an external key, pointing to the host class
387	 */
388	static public function IsHierarchicalKey()
389	{
390		return false;
391	}
392
393	/**
394	 * @return bool true if the attribute value is stored on an object pointed to be an external key
395	 */
396	static public function IsExternalField()
397	{
398		return false;
399	}
400
401	/**
402	 * @return bool true if the attribute can be written (by essence : metamodel field option)
403	 * @see \DBObject::IsAttributeReadOnlyForCurrentState() for a specific object instance (depending on its workflow)
404	 */
405	public function IsWritable()
406	{
407		return false;
408	}
409
410	/**
411	 * @return bool true if the attribute has been added automatically by the framework
412	 */
413	public function IsMagic()
414	{
415		return $this->GetOptional('magic', false);
416	}
417
418	/**
419	 * @return bool true if the attribute value is kept in the loaded object (in memory)
420	 */
421	static public function LoadInObject()
422	{
423		return true;
424	}
425
426	/**
427	 * @return bool true if the attribute value comes from the database in one way or another
428	 */
429	static public function LoadFromDB()
430	{
431		return true;
432	}
433
434	/**
435	 * @return bool true if the attribute should be loaded anytime (in addition to the column selected by the user)
436	 */
437	public function AlwaysLoadInTables()
438	{
439		return $this->GetOptional('always_load_in_tables', false);
440	}
441
442	/**
443	 * @param \DBObject $oHostObject
444	 *
445	 * @return mixed Must return the value if LoadInObject returns false
446	 */
447	public function GetValue($oHostObject)
448	{
449		return null;
450	}
451
452	/**
453	 * Returns true if the attribute must not be stored if its current value is "null" (Cf. IsNull())
454	 *
455	 * @return bool
456	 */
457	public function IsNullAllowed()
458	{
459		return true;
460	}
461
462	/**
463	 * Returns the attribute code (identifies the attribute in the host class)
464	 *
465	 * @return string
466	 */
467	public function GetCode()
468	{
469		return $this->m_sCode;
470	}
471
472	/**
473	 * Find the corresponding "link" attribute on the target class, if any
474	 *
475	 * @return null | AttributeDefinition
476	 */
477	public function GetMirrorLinkAttribute()
478	{
479		return null;
480	}
481
482	/**
483	 * Helper to browse the hierarchy of classes, searching for a label
484	 *
485	 * @param string $sDictEntrySuffix
486	 * @param string $sDefault
487	 * @param bool $bUserLanguageOnly
488	 *
489	 * @return string
490	 * @throws \Exception
491	 */
492	protected function SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly)
493	{
494		$sLabel = Dict::S('Class:'.$this->m_sHostClass.$sDictEntrySuffix, '', $bUserLanguageOnly);
495		if (strlen($sLabel) == 0)
496		{
497			// Nothing found: go higher in the hierarchy (if possible)
498			//
499			$sLabel = $sDefault;
500			$sParentClass = MetaModel::GetParentClass($this->m_sHostClass);
501			if ($sParentClass)
502			{
503				if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode))
504				{
505					$oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode);
506					$sLabel = $oAttDef->SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly);
507				}
508			}
509		}
510
511		return $sLabel;
512	}
513
514	/**
515	 * @param string|null $sDefault
516	 *
517	 * @return string
518	 *
519	 * @throws \Exception
520	 */
521	public function GetLabel($sDefault = null)
522	{
523		$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, null, true /*user lang*/);
524		if (is_null($sLabel))
525		{
526			// If no default value is specified, let's define the most relevant one for developping purposes
527			if (is_null($sDefault))
528			{
529				$sDefault = str_replace('_', ' ', $this->m_sCode);
530			}
531			// Browse the hierarchy again, accepting default (english) translations
532			$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, $sDefault, false);
533		}
534
535		return $sLabel;
536	}
537
538	/**
539	 * To be overloaded for localized enums
540	 *
541	 * @param string $sValue
542	 *
543	 * @return string label corresponding to the given value (in plain text)
544	 */
545	public function GetValueLabel($sValue)
546	{
547		return $sValue;
548	}
549
550	/**
551	 * Get the value from a given string (plain text, CSV import)
552	 *
553	 * @param string $sProposedValue
554	 * @param bool $bLocalizedValue
555	 * @param string $sSepItem
556	 * @param string $sSepAttribute
557	 * @param string $sSepValue
558	 * @param string $sAttributeQualifier
559	 *
560	 * @return mixed null if no match could be found
561	 */
562	public function MakeValueFromString(
563		$sProposedValue,
564		$bLocalizedValue = false,
565		$sSepItem = null,
566		$sSepAttribute = null,
567		$sSepValue = null,
568		$sAttributeQualifier = null
569	) {
570		return $this->MakeRealValue($sProposedValue, null);
571	}
572
573	/**
574	 * Parses a search string coming from user input
575	 *
576	 * @param string $sSearchString
577	 *
578	 * @return string
579	 */
580	public function ParseSearchString($sSearchString)
581	{
582		return $sSearchString;
583	}
584
585	/**
586	 * @return string
587	 *
588	 * @throws \Exception
589	 */
590	public function GetLabel_Obsolete()
591	{
592		// Written for compatibility with a data model written prior to version 0.9.1
593		if (array_key_exists('label', $this->m_aParams))
594		{
595			return $this->m_aParams['label'];
596		}
597		else
598		{
599			return $this->GetLabel();
600		}
601	}
602
603	/**
604	 * @param string|null $sDefault
605	 *
606	 * @return string
607	 *
608	 * @throws \Exception
609	 */
610	public function GetDescription($sDefault = null)
611	{
612		$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', null, true /*user lang*/);
613		if (is_null($sLabel))
614		{
615			// If no default value is specified, let's define the most relevant one for developping purposes
616			if (is_null($sDefault))
617			{
618				$sDefault = '';
619			}
620			// Browse the hierarchy again, accepting default (english) translations
621			$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', $sDefault, false);
622		}
623
624		return $sLabel;
625	}
626
627	/**
628	 * @param string|null $sDefault
629	 *
630	 * @return string
631	 *
632	 * @throws \Exception
633	 */
634	public function GetHelpOnEdition($sDefault = null)
635	{
636		$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', null, true /*user lang*/);
637		if (is_null($sLabel))
638		{
639			// If no default value is specified, let's define the most relevant one for developping purposes
640			if (is_null($sDefault))
641			{
642				$sDefault = '';
643			}
644			// Browse the hierarchy again, accepting default (english) translations
645			$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', $sDefault, false);
646		}
647
648		return $sLabel;
649	}
650
651	public function GetHelpOnSmartSearch()
652	{
653		$aParents = array_merge(array(get_class($this) => get_class($this)), class_parents($this));
654		foreach($aParents as $sClass)
655		{
656			$sHelp = Dict::S("Core:$sClass?SmartSearch", '-missing-');
657			if ($sHelp != '-missing-')
658			{
659				return $sHelp;
660			}
661		}
662
663		return '';
664	}
665
666	/**
667	 * @return string
668	 *
669	 * @throws \Exception
670	 */
671	public function GetDescription_Obsolete()
672	{
673		// Written for compatibility with a data model written prior to version 0.9.1
674		if (array_key_exists('description', $this->m_aParams))
675		{
676			return $this->m_aParams['description'];
677		}
678		else
679		{
680			return $this->GetDescription();
681		}
682	}
683
684	public function GetTrackingLevel()
685	{
686		return $this->GetOptional('tracking_level', ATTRIBUTE_TRACKING_ALL);
687	}
688
689	/**
690	 * @return \ValueSetObjects
691	 */
692	public function GetValuesDef()
693	{
694		return null;
695	}
696
697	public function GetPrerequisiteAttributes($sClass = null)
698	{
699		return array();
700	}
701
702	public function GetNullValue()
703	{
704		return null;
705	}
706
707	public function IsNull($proposedValue)
708	{
709		return is_null($proposedValue);
710	}
711
712	/**
713	 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
714	 *
715	 * @param $proposedValue
716	 * @param $oHostObj
717	 *
718	 * @return mixed
719	 */
720	public function MakeRealValue($proposedValue, $oHostObj)
721	{
722		return $proposedValue;
723	}
724
725	public function Equals($val1, $val2)
726	{
727		return ($val1 == $val2);
728	}
729
730	/**
731	 * @param string $sPrefix
732	 *
733	 * @return array suffix/expression pairs (1 in most of the cases), for READING (Select)
734	 */
735	public function GetSQLExpressions($sPrefix = '')
736	{
737		return array();
738	}
739
740	/**
741	 * @param array $aCols
742	 * @param string $sPrefix
743	 *
744	 * @return mixed a value out of suffix/value pairs, for SELECT result interpretation
745	 */
746	public function FromSQLToValue($aCols, $sPrefix = '')
747	{
748		return null;
749	}
750
751	/**
752	 * @param bool $bFullSpec
753	 *
754	 * @return array column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation)
755	 * @see \CMDBSource::GetFieldSpec()
756	 */
757	public function GetSQLColumns($bFullSpec = false)
758	{
759		return array();
760	}
761
762	/**
763	 * @param $value
764	 *
765	 * @return array column/value pairs (1 in most of the cases), for WRITING (Insert, Update)
766	 */
767	public function GetSQLValues($value)
768	{
769		return array();
770	}
771
772	public function RequiresIndex()
773	{
774		return false;
775	}
776
777	public function RequiresFullTextIndex()
778	{
779		return false;
780	}
781
782	public function CopyOnAllTables()
783	{
784		return false;
785	}
786
787	public function GetOrderBySQLExpressions($sClassAlias)
788	{
789		// Note: This is the responsibility of this function to place backticks around column aliases
790		return array('`'.$sClassAlias.$this->GetCode().'`');
791	}
792
793	public function GetOrderByHint()
794	{
795		return '';
796	}
797
798	// Import - differs slightly from SQL input, but identical in most cases
799	//
800	public function GetImportColumns()
801	{
802		return $this->GetSQLColumns();
803	}
804
805	public function FromImportToValue($aCols, $sPrefix = '')
806	{
807		$aValues = array();
808		foreach($this->GetSQLExpressions($sPrefix) as $sAlias => $sExpr)
809		{
810			// This is working, based on the assumption that importable fields
811			// are not computed fields => the expression is the name of a column
812			$aValues[$sPrefix.$sAlias] = $aCols[$sExpr];
813		}
814
815		return $this->FromSQLToValue($aValues, $sPrefix);
816	}
817
818	public function GetValidationPattern()
819	{
820		return '';
821	}
822
823	public function CheckFormat($value)
824	{
825		return true;
826	}
827
828	public function GetMaxSize()
829	{
830		return null;
831	}
832
833	/**
834	 * @return mixed|null
835	 * @deprecated never used
836	 */
837	public function MakeValue()
838	{
839		$sComputeFunc = $this->Get("compute_func");
840		if (empty($sComputeFunc))
841		{
842			return null;
843		}
844
845		return call_user_func($sComputeFunc);
846	}
847
848	abstract public function GetDefaultValue(DBObject $oHostObject = null);
849
850	//
851	// To be overloaded in subclasses
852	//
853
854	abstract public function GetBasicFilterOperators(); // returns an array of "opCode"=>"description"
855
856	abstract public function GetBasicFilterLooseOperator(); // returns an "opCode"
857
858	//abstract protected GetBasicFilterHTMLInput();
859	abstract public function GetBasicFilterSQLExpr($sOpCode, $value);
860
861	public function GetFilterDefinitions()
862	{
863		return array();
864	}
865
866	public function GetEditValue($sValue, $oHostObj = null)
867	{
868		return (string)$sValue;
869	}
870
871	/**
872	 * For fields containing a potential markup, return the value without this markup
873	 *
874	 * @param string $sValue
875	 * @param \DBObject $oHostObj
876	 *
877	 * @return string
878	 */
879	public function GetAsPlainText($sValue, $oHostObj = null)
880	{
881		return (string)$this->GetEditValue($sValue, $oHostObj);
882	}
883
884	/**
885	 * Helper to get a value that will be JSON encoded
886	 * The operation is the opposite to FromJSONToValue
887	 *
888	 * @param $value
889	 *
890	 * @return string
891	 */
892	public function GetForJSON($value)
893	{
894		// In most of the cases, that will be the expected behavior...
895		return $this->GetEditValue($value);
896	}
897
898	/**
899	 * Helper to form a value, given JSON decoded data
900	 * The operation is the opposite to GetForJSON
901	 *
902	 * @param $json
903	 *
904	 * @return mixed
905	 */
906	public function FromJSONToValue($json)
907	{
908		// Passthrough in most of the cases
909		return $json;
910	}
911
912	/**
913	 * Override to display the value in the GUI
914	 *
915	 * @param string $sValue
916	 * @param \DBObject $oHostObject
917	 * @param bool $bLocalize
918	 *
919	 * @return string
920	 */
921	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
922	{
923		return Str::pure2html((string)$sValue);
924	}
925
926	/**
927	 * Override to export the value in XML
928	 *
929	 * @param string $sValue
930	 * @param \DBObject $oHostObject
931	 * @param bool $bLocalize
932	 *
933	 * @return mixed
934	 */
935	public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true)
936	{
937		return Str::pure2xml((string)$sValue);
938	}
939
940	/**
941	 * Override to escape the value when read by DBObject::GetAsCSV()
942	 *
943	 * @param string $sValue
944	 * @param string $sSeparator
945	 * @param string $sTextQualifier
946	 * @param \DBObject $oHostObject
947	 * @param bool $bLocalize
948	 * @param bool $bConvertToPlainText
949	 *
950	 * @return string
951	 */
952	public function GetAsCSV(
953		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
954		$bConvertToPlainText = false
955	) {
956		return (string)$sValue;
957	}
958
959	/**
960	 * Override to differentiate a value displayed in the UI or in the history
961	 *
962	 * @param string $sValue
963	 * @param \DBObject $oHostObject
964	 * @param bool $bLocalize
965	 *
966	 * @return string
967	 */
968	public function GetAsHTMLForHistory($sValue, $oHostObject = null, $bLocalize = true)
969	{
970		return $this->GetAsHTML($sValue, $oHostObject, $bLocalize);
971	}
972
973	static public function GetFormFieldClass()
974	{
975		return '\\Combodo\\iTop\\Form\\Field\\StringField';
976	}
977
978	/**
979	 * Override to specify Field class
980	 *
981	 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the
982	 * $oFormField is passed, MakeFormField behave more like a Prepare.
983	 *
984	 * @param \DBObject $oObject
985	 * @param \Combodo\iTop\Form\Field\Field $oFormField
986	 *
987	 * @return null
988	 * @throws \CoreException
989	 * @throws \Exception
990	 */
991	public function MakeFormField(DBObject $oObject, $oFormField = null)
992	{
993		// This is a fallback in case the AttributeDefinition subclass has no overloading of this function.
994		if ($oFormField === null)
995		{
996			$sFormFieldClass = static::GetFormFieldClass();
997			$oFormField = new $sFormFieldClass($this->GetCode());
998			//$oFormField->SetReadOnly(true);
999		}
1000
1001		$oFormField->SetLabel($this->GetLabel());
1002
1003		// Attributes flags
1004		// - Retrieving flags for the current object
1005		if ($oObject->IsNew())
1006		{
1007			$iFlags = $oObject->GetInitialStateAttributeFlags($this->GetCode());
1008		}
1009		else
1010		{
1011			$iFlags = $oObject->GetAttributeFlags($this->GetCode());
1012		}
1013
1014		// - Comparing flags
1015		if ($this->IsWritable() && (!$this->IsNullAllowed() || (($iFlags & OPT_ATT_MANDATORY) === OPT_ATT_MANDATORY)))
1016		{
1017			$oFormField->SetMandatory(true);
1018		}
1019		if ((!$oObject->IsNew() || !$oFormField->GetMandatory()) && (($iFlags & OPT_ATT_READONLY) === OPT_ATT_READONLY))
1020		{
1021			$oFormField->SetReadOnly(true);
1022		}
1023
1024		// CurrentValue
1025		$oFormField->SetCurrentValue($oObject->Get($this->GetCode()));
1026
1027		// Validation pattern
1028		if ($this->GetValidationPattern() !== '')
1029		{
1030			$oFormField->AddValidator(new \Combodo\iTop\Form\Validator\Validator($this->GetValidationPattern()));
1031		}
1032
1033		return $oFormField;
1034	}
1035
1036	/**
1037	 * List the available verbs for 'GetForTemplate'
1038	 */
1039	public function EnumTemplateVerbs()
1040	{
1041		return array(
1042			'' => 'Plain text (unlocalized) representation',
1043			'html' => 'HTML representation',
1044			'label' => 'Localized representation',
1045			'text' => 'Plain text representation (without any markup)',
1046		);
1047	}
1048
1049	/**
1050	 * Get various representations of the value, for insertion into a template (e.g. in Notifications)
1051	 *
1052	 * @param mixed $value The current value of the field
1053	 * @param string $sVerb The verb specifying the representation of the value
1054	 * @param \DBObject $oHostObject
1055	 * @param bool $bLocalize Whether or not to localize the value
1056	 *
1057	 * @return mixed|null|string
1058	 *
1059	 * @throws \Exception
1060	 */
1061	public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
1062	{
1063		if ($this->IsScalar())
1064		{
1065			switch ($sVerb)
1066			{
1067				case '':
1068					return $value;
1069
1070				case 'html':
1071					return $this->GetAsHtml($value, $oHostObject, $bLocalize);
1072
1073				case 'label':
1074					return $this->GetEditValue($value);
1075
1076				case 'text':
1077					return $this->GetAsPlainText($value);
1078					break;
1079
1080				default:
1081					throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject));
1082			}
1083		}
1084
1085		return null;
1086	}
1087
1088	/**
1089	 * @param array $aArgs
1090	 * @param string $sContains
1091	 *
1092	 * @return array|null
1093	 * @throws \CoreException
1094	 * @throws \OQLException
1095	 */
1096	public function GetAllowedValues($aArgs = array(), $sContains = '')
1097	{
1098		$oValSetDef = $this->GetValuesDef();
1099		if (!$oValSetDef)
1100		{
1101			return null;
1102		}
1103
1104		return $oValSetDef->GetValues($aArgs, $sContains);
1105	}
1106
1107	/**
1108	 * Explain the change of the attribute (history)
1109	 *
1110	 * @param string $sOldValue
1111	 * @param string $sNewValue
1112	 * @param string $sLabel
1113	 *
1114	 * @return string
1115	 * @throws \ArchivedObjectException
1116	 * @throws \CoreException
1117	 * @throws \DictExceptionMissingString
1118	 * @throws \OQLException
1119	 * @throws \Exception
1120	 */
1121	public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null)
1122	{
1123		if (is_null($sLabel))
1124		{
1125			$sLabel = $this->GetLabel();
1126		}
1127
1128		$sNewValueHtml = $this->GetAsHTMLForHistory($sNewValue);
1129		$sOldValueHtml = $this->GetAsHTMLForHistory($sOldValue);
1130
1131		if ($this->IsExternalKey())
1132		{
1133			/** @var \AttributeExternalKey $this */
1134			$sTargetClass = $this->GetTargetClass();
1135			$sOldValueHtml = (int)$sOldValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sOldValue) : null;
1136			$sNewValueHtml = (int)$sNewValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sNewValue) : null;
1137		}
1138		if ((($this->GetType() == 'String') || ($this->GetType() == 'Text')) &&
1139			(strlen($sNewValue) > strlen($sOldValue)))
1140		{
1141			// Check if some text was not appended to the field
1142			if (substr($sNewValue, 0, strlen($sOldValue)) == $sOldValue) // Text added at the end
1143			{
1144				$sDelta = $this->GetAsHTML(substr($sNewValue, strlen($sOldValue)));
1145				$sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel);
1146			}
1147			else
1148			{
1149				if (substr($sNewValue, -strlen($sOldValue)) == $sOldValue)   // Text added at the beginning
1150				{
1151					$sDelta = $this->GetAsHTML(substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue)));
1152					$sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel);
1153				}
1154				else
1155				{
1156					if (strlen($sOldValue) == 0)
1157					{
1158						$sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml);
1159					}
1160					else
1161					{
1162						if (is_null($sNewValue))
1163						{
1164							$sNewValueHtml = Dict::S('UI:UndefinedObject');
1165						}
1166						$sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel,
1167							$sNewValueHtml, $sOldValueHtml);
1168					}
1169				}
1170			}
1171		}
1172		else
1173		{
1174			if (strlen($sOldValue) == 0)
1175			{
1176				$sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml);
1177			}
1178			else
1179			{
1180				if (is_null($sNewValue))
1181				{
1182					$sNewValueHtml = Dict::S('UI:UndefinedObject');
1183				}
1184				$sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml,
1185					$sOldValueHtml);
1186			}
1187		}
1188
1189		return $sResult;
1190	}
1191
1192
1193	/**
1194	 * Parses a string to find some smart search patterns and build the corresponding search/OQL condition
1195	 * Each derived class is reponsible for defining and processing their own smart patterns, the base class
1196	 * does nothing special, and just calls the default (loose) operator
1197	 *
1198	 * @param string $sSearchText The search string to analyze for smart patterns
1199	 * @param \FieldExpression $oField
1200	 * @param array $aParams Values of the query parameters
1201	 *
1202	 * @return \Expression The search condition to be added (AND) to the current search
1203	 *
1204	 * @throws \CoreException
1205	 */
1206	public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams)
1207	{
1208		$sParamName = $oField->GetParent().'_'.$oField->GetName();
1209		$oRightExpr = new VariableExpression($sParamName);
1210		$sOperator = $this->GetBasicFilterLooseOperator();
1211		switch ($sOperator)
1212		{
1213			case 'Contains':
1214				$aParams[$sParamName] = "%$sSearchText%";
1215				$sSQLOperator = 'LIKE';
1216				break;
1217
1218			default:
1219				$sSQLOperator = $sOperator;
1220				$aParams[$sParamName] = $sSearchText;
1221		}
1222		$oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr);
1223
1224		return $oNewCondition;
1225	}
1226
1227	/**
1228	 * Tells if an attribute is part of the unique fingerprint of the object (used for comparing two objects)
1229	 * All attributes which value is not based on a value from the object itself (like ExternalFields or LinkedSet)
1230	 * must be excluded from the object's signature
1231	 *
1232	 * @return boolean
1233	 */
1234	public function IsPartOfFingerprint()
1235	{
1236		return true;
1237	}
1238
1239	/**
1240	 * The part of the current attribute in the object's signature, for the supplied value
1241	 *
1242	 * @param mixed $value The value of this attribute for the object
1243	 *
1244	 * @return string The "signature" for this field/attribute
1245	 */
1246	public function Fingerprint($value)
1247	{
1248		return (string)$value;
1249	}
1250}
1251
1252class AttributeDashboard extends AttributeDefinition
1253{
1254	static public function ListExpectedParams()
1255	{
1256		return array_merge(parent::ListExpectedParams(),
1257			array("definition_file", "is_user_editable"));
1258	}
1259
1260	public function GetDashboard()
1261	{
1262		$sAttCode = $this->GetCode();
1263		$sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode);
1264		$sFilePath = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$this->Get('definition_file');
1265		return RuntimeDashboard::GetDashboard($sFilePath, $sClass.'__'.$sAttCode);
1266	}
1267
1268	public function IsUserEditable()
1269	{
1270		return $this->Get('is_user_editable');
1271	}
1272
1273	public function IsWritable()
1274	{
1275		return false;
1276	}
1277
1278	public function GetEditClass()
1279	{
1280		return "";
1281	}
1282
1283	public function GetDefaultValue(DBObject $oHostObject = null)
1284	{
1285		return null;
1286	}
1287
1288	public function GetBasicFilterOperators()
1289	{
1290		return array();
1291	}
1292
1293	public function GetBasicFilterLooseOperator()
1294	{
1295		return '=';
1296	}
1297
1298	public function GetBasicFilterSQLExpr($sOpCode, $value)
1299	{
1300		return '';
1301	}
1302
1303	/**
1304	 * @inheritdoc
1305	 */
1306	public function MakeFormField(DBObject $oObject, $oFormField = null)
1307	{
1308		return null;
1309	}
1310
1311	// if this verb returns false, then GetValue must be implemented
1312	static public function LoadInObject()
1313	{
1314		return false;
1315	}
1316
1317	public function GetValue($oHostObject)
1318	{
1319		return '';
1320	}
1321}
1322
1323/**
1324 * Set of objects directly linked to an object, and being part of its definition
1325 *
1326 * @package     iTopORM
1327 */
1328class AttributeLinkedSet extends AttributeDefinition
1329{
1330	static public function ListExpectedParams()
1331	{
1332		return array_merge(parent::ListExpectedParams(),
1333			array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max"));
1334	}
1335
1336	public function GetEditClass()
1337	{
1338		return "LinkedSet";
1339	}
1340
1341	public function IsWritable()
1342	{
1343		return true;
1344	}
1345
1346	static public function IsLinkSet()
1347	{
1348		return true;
1349	}
1350
1351	public function IsIndirect()
1352	{
1353		return false;
1354	}
1355
1356	public function GetValuesDef()
1357	{
1358		return $this->Get("allowed_values");
1359	}
1360
1361	public function GetPrerequisiteAttributes($sClass = null)
1362	{
1363		return $this->Get("depends_on");
1364	}
1365
1366	/**
1367	 * @param \DBObject|null $oHostObject
1368	 *
1369	 * @return \ormLinkSet
1370	 *
1371	 * @throws \Exception
1372	 * @throws \CoreException
1373	 * @throws \CoreWarning
1374	 */
1375	public function GetDefaultValue(DBObject $oHostObject = null)
1376	{
1377		if ($oHostObject === null)
1378		{
1379			return null;
1380		}
1381
1382		$sLinkClass = $this->GetLinkedClass();
1383		$sExtKeyToMe = $this->GetExtKeyToMe();
1384
1385		// The class to target is not the current class, because if this is a derived class,
1386		// it may differ from the target class, then things start to become confusing
1387		/** @var \AttributeExternalKey $oRemoteExtKeyAtt */
1388		$oRemoteExtKeyAtt = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToMe);
1389		$sMyClass = $oRemoteExtKeyAtt->GetTargetClass();
1390
1391		$oMyselfSearch = new DBObjectSearch($sMyClass);
1392		if ($oHostObject !== null)
1393		{
1394			$oMyselfSearch->AddCondition('id', $oHostObject->GetKey(), '=');
1395		}
1396
1397		$oLinkSearch = new DBObjectSearch($sLinkClass);
1398		$oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe);
1399		if ($this->IsIndirect())
1400		{
1401			// Join the remote class so that the archive flag will be taken into account
1402			/** @var \AttributeLinkedSetIndirect $this */
1403			$sExtKeyToRemote = $this->GetExtKeyToRemote();
1404			/** @var \AttributeExternalKey $oExtKeyToRemote */
1405			$oExtKeyToRemote = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToRemote);
1406			$sRemoteClass = $oExtKeyToRemote->GetTargetClass();
1407			if (MetaModel::IsArchivable($sRemoteClass))
1408			{
1409				$oRemoteSearch = new DBObjectSearch($sRemoteClass);
1410				/** @var \AttributeLinkedSetIndirect $this */
1411				$oLinkSearch->AddCondition_PointingTo($oRemoteSearch, $this->GetExtKeyToRemote());
1412			}
1413		}
1414		$oLinks = new DBObjectSet($oLinkSearch);
1415		$oLinkSet = new ormLinkSet($this->GetHostClass(), $this->GetCode(), $oLinks);
1416
1417		return $oLinkSet;
1418	}
1419
1420	public function GetTrackingLevel()
1421	{
1422		return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_default'));
1423	}
1424
1425	public function GetEditMode()
1426	{
1427		return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS);
1428	}
1429
1430	public function GetLinkedClass()
1431	{
1432		return $this->Get('linked_class');
1433	}
1434
1435	public function GetExtKeyToMe()
1436	{
1437		return $this->Get('ext_key_to_me');
1438	}
1439
1440	public function GetBasicFilterOperators()
1441	{
1442		return array();
1443	}
1444
1445	public function GetBasicFilterLooseOperator()
1446	{
1447		return '';
1448	}
1449
1450	public function GetBasicFilterSQLExpr($sOpCode, $value)
1451	{
1452		return '';
1453	}
1454
1455	/**
1456	 * @param string $sValue
1457	 * @param \DBObject $oHostObject
1458	 * @param bool $bLocalize
1459	 *
1460	 * @return string|null
1461	 *
1462	 * @throws \CoreException
1463	 */
1464	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
1465	{
1466		if (is_object($sValue) && ($sValue instanceof ormLinkSet))
1467		{
1468			$sValue->Rewind();
1469			$aItems = array();
1470			while ($oObj = $sValue->Fetch())
1471			{
1472				// Show only relevant information (hide the external key to the current object)
1473				$aAttributes = array();
1474				foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef)
1475				{
1476					if ($sAttCode == $this->GetExtKeyToMe())
1477					{
1478						continue;
1479					}
1480					if ($oAttDef->IsExternalField())
1481					{
1482						continue;
1483					}
1484					$sAttValue = $oObj->GetAsHTML($sAttCode);
1485					if (strlen($sAttValue) > 0)
1486					{
1487						$aAttributes[] = $sAttValue;
1488					}
1489				}
1490				$sAttributes = implode(', ', $aAttributes);
1491				$aItems[] = $sAttributes;
1492			}
1493
1494			return implode('<br/>', $aItems);
1495		}
1496
1497		return null;
1498	}
1499
1500	/**
1501	 * @param string $sValue
1502	 * @param \DBObject $oHostObject
1503	 * @param bool $bLocalize
1504	 *
1505	 * @return string
1506	 *
1507	 * @throws \CoreException
1508	 */
1509	public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true)
1510	{
1511		if (is_object($sValue) && ($sValue instanceof ormLinkSet))
1512		{
1513			$sValue->Rewind();
1514			$sRes = "<Set>\n";
1515			while ($oObj = $sValue->Fetch())
1516			{
1517				$sObjClass = get_class($oObj);
1518				$sRes .= "<$sObjClass id=\"".$oObj->GetKey()."\">\n";
1519				// Show only relevant information (hide the external key to the current object)
1520				foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef)
1521				{
1522					if ($sAttCode == 'finalclass')
1523					{
1524						if ($sObjClass == $this->GetLinkedClass())
1525						{
1526							// Simplify the output if the exact class could be determined implicitely
1527							continue;
1528						}
1529					}
1530					if ($sAttCode == $this->GetExtKeyToMe())
1531					{
1532						continue;
1533					}
1534					if ($oAttDef->IsExternalField())
1535					{
1536						/** @var \AttributeExternalField $oAttDef */
1537						if ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe())
1538						{
1539							continue;
1540						}
1541						/** @var AttributeExternalField $oAttDef */
1542						if ($oAttDef->IsFriendlyName())
1543						{
1544							continue;
1545						}
1546					}
1547					if ($oAttDef instanceof AttributeFriendlyName)
1548					{
1549						continue;
1550					}
1551					if (!$oAttDef->IsScalar())
1552					{
1553						continue;
1554					}
1555					$sAttValue = $oObj->GetAsXML($sAttCode, $bLocalize);
1556					$sRes .= "<$sAttCode>$sAttValue</$sAttCode>\n";
1557				}
1558				$sRes .= "</$sObjClass>\n";
1559			}
1560			$sRes .= "</Set>\n";
1561		}
1562		else
1563		{
1564			$sRes = '';
1565		}
1566
1567		return $sRes;
1568	}
1569
1570	/**
1571	 * @param $sValue
1572	 * @param string $sSeparator
1573	 * @param string $sTextQualifier
1574	 * @param \DBObject $oHostObject
1575	 * @param bool $bLocalize
1576	 * @param bool $bConvertToPlainText
1577	 *
1578	 * @return mixed|string
1579	 * @throws \CoreException
1580	 */
1581	public function GetAsCSV(
1582		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
1583		$bConvertToPlainText = false
1584	) {
1585		$sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator');
1586		$sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator');
1587		$sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator');
1588		$sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier');
1589
1590		if (is_object($sValue) && ($sValue instanceof ormLinkSet))
1591		{
1592			$sValue->Rewind();
1593			$aItems = array();
1594			while ($oObj = $sValue->Fetch())
1595			{
1596				$sObjClass = get_class($oObj);
1597				// Show only relevant information (hide the external key to the current object)
1598				$aAttributes = array();
1599				foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef)
1600				{
1601					if ($sAttCode == 'finalclass')
1602					{
1603						if ($sObjClass == $this->GetLinkedClass())
1604						{
1605							// Simplify the output if the exact class could be determined implicitely
1606							continue;
1607						}
1608					}
1609					if ($sAttCode == $this->GetExtKeyToMe())
1610					{
1611						continue;
1612					}
1613					if ($oAttDef->IsExternalField())
1614					{
1615						continue;
1616					}
1617					if (!$oAttDef->IsBasedOnDBColumns())
1618					{
1619						continue;
1620					}
1621					if (!$oAttDef->IsScalar())
1622					{
1623						continue;
1624					}
1625					$sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '', $bLocalize);
1626					if (strlen($sAttValue) > 0)
1627					{
1628						$sAttributeData = str_replace($sAttributeQualifier, $sAttributeQualifier.$sAttributeQualifier,
1629							$sAttCode.$sSepValue.$sAttValue);
1630						$aAttributes[] = $sAttributeQualifier.$sAttributeData.$sAttributeQualifier;
1631					}
1632				}
1633				$sAttributes = implode($sSepAttribute, $aAttributes);
1634				$aItems[] = $sAttributes;
1635			}
1636			$sRes = implode($sSepItem, $aItems);
1637		}
1638		else
1639		{
1640			$sRes = '';
1641		}
1642		$sRes = str_replace($sTextQualifier, $sTextQualifier.$sTextQualifier, $sRes);
1643		$sRes = $sTextQualifier.$sRes.$sTextQualifier;
1644
1645		return $sRes;
1646	}
1647
1648	/**
1649	 * List the available verbs for 'GetForTemplate'
1650	 */
1651	public function EnumTemplateVerbs()
1652	{
1653		return array(
1654			'' => 'Plain text (unlocalized) representation',
1655			'html' => 'HTML representation (unordered list)',
1656		);
1657	}
1658
1659	/**
1660	 * Get various representations of the value, for insertion into a template (e.g. in Notifications)
1661	 *
1662	 * @param mixed $value The current value of the field
1663	 * @param string $sVerb The verb specifying the representation of the value
1664	 * @param DBObject $oHostObject The object
1665	 * @param bool $bLocalize Whether or not to localize the value
1666	 *
1667	 * @return string
1668	 * @throws \Exception
1669	 */
1670	public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
1671	{
1672		$sRemoteName = $this->IsIndirect() ?
1673			/** @var \AttributeLinkedSetIndirect $this */
1674			$this->GetExtKeyToRemote().'_friendlyname' : 'friendlyname';
1675
1676		$oLinkSet = clone $value; // Workaround/Safety net for Trac #887
1677		$iLimit = MetaModel::GetConfig()->Get('max_linkset_output');
1678		$iCount = 0;
1679		$aNames = array();
1680		foreach($oLinkSet as $oItem)
1681		{
1682			if (($iLimit > 0) && ($iCount == $iLimit))
1683			{
1684				$iTotal = $oLinkSet->Count();
1685				$aNames[] = '... '.Dict::Format('UI:TruncatedResults', $iCount, $iTotal);
1686				break;
1687			}
1688			$aNames[] = $oItem->Get($sRemoteName);
1689			$iCount++;
1690		}
1691
1692		switch ($sVerb)
1693		{
1694			case '':
1695				return implode("\n", $aNames);
1696
1697			case 'html':
1698				return '<ul><li>'.implode("</li><li>", $aNames).'</li></ul>';
1699
1700			default:
1701				throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject));
1702		}
1703	}
1704
1705	public function DuplicatesAllowed()
1706	{
1707		return false;
1708	} // No duplicates for 1:n links, never
1709
1710	public function GetImportColumns()
1711	{
1712		$aColumns = array();
1713		$aColumns[$this->GetCode()] = 'TEXT';
1714
1715		return $aColumns;
1716	}
1717
1718	/**
1719	 * @param string $sProposedValue
1720	 * @param bool $bLocalizedValue
1721	 * @param string $sSepItem
1722	 * @param string $sSepAttribute
1723	 * @param string $sSepValue
1724	 * @param string $sAttributeQualifier
1725	 *
1726	 * @return \DBObjectSet|mixed
1727	 * @throws \CSVParserException
1728	 * @throws \CoreException
1729	 * @throws \CoreUnexpectedValue
1730	 * @throws \MissingQueryArgument
1731	 * @throws \MySQLException
1732	 * @throws \MySQLHasGoneAwayException
1733	 * @throws \Exception
1734	 */
1735	public function MakeValueFromString(
1736		$sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
1737		$sAttributeQualifier = null
1738	) {
1739		if (is_null($sSepItem) || empty($sSepItem))
1740		{
1741			$sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator');
1742		}
1743		if (is_null($sSepAttribute) || empty($sSepAttribute))
1744		{
1745			$sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator');
1746		}
1747		if (is_null($sSepValue) || empty($sSepValue))
1748		{
1749			$sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator');
1750		}
1751		if (is_null($sAttributeQualifier) || empty($sAttributeQualifier))
1752		{
1753			$sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier');
1754		}
1755
1756		$sTargetClass = $this->Get('linked_class');
1757
1758		$sInput = str_replace($sSepItem, "\n", $sProposedValue);
1759		$oCSVParser = new CSVParser($sInput, $sSepAttribute, $sAttributeQualifier);
1760
1761		$aInput = $oCSVParser->ToArray(0 /* do not skip lines */);
1762
1763		$aLinks = array();
1764		foreach($aInput as $aRow)
1765		{
1766			// 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value
1767			$aExtKeys = array();
1768			$aValues = array();
1769			foreach($aRow as $sCell)
1770			{
1771				$iSepPos = strpos($sCell, $sSepValue);
1772				if ($iSepPos === false)
1773				{
1774					// Houston...
1775					throw new CoreException('Wrong format for link attribute specification', array('value' => $sCell));
1776				}
1777
1778				$sAttCode = trim(substr($sCell, 0, $iSepPos));
1779				$sValue = substr($sCell, $iSepPos + strlen($sSepValue));
1780
1781				if (preg_match('/^(.+)->(.+)$/', $sAttCode, $aMatches))
1782				{
1783					$sKeyAttCode = $aMatches[1];
1784					$sRemoteAttCode = $aMatches[2];
1785					$aExtKeys[$sKeyAttCode][$sRemoteAttCode] = $sValue;
1786					if (!MetaModel::IsValidAttCode($sTargetClass, $sKeyAttCode))
1787					{
1788						throw new CoreException('Wrong attribute code for link attribute specification',
1789							array('class' => $sTargetClass, 'attcode' => $sKeyAttCode));
1790					}
1791					/** @var \AttributeExternalKey $oKeyAttDef */
1792					$oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
1793					$sRemoteClass = $oKeyAttDef->GetTargetClass();
1794					if (!MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode))
1795					{
1796						throw new CoreException('Wrong attribute code for link attribute specification',
1797							array('class' => $sRemoteClass, 'attcode' => $sRemoteAttCode));
1798					}
1799				}
1800				else
1801				{
1802					if (!MetaModel::IsValidAttCode($sTargetClass, $sAttCode))
1803					{
1804						throw new CoreException('Wrong attribute code for link attribute specification',
1805							array('class' => $sTargetClass, 'attcode' => $sAttCode));
1806					}
1807					$oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttCode);
1808					$aValues[$sAttCode] = $oAttDef->MakeValueFromString($sValue, $bLocalizedValue, $sSepItem,
1809						$sSepAttribute, $sSepValue, $sAttributeQualifier);
1810				}
1811			}
1812
1813			// 2nd - Instanciate the object and set the value
1814			if (isset($aValues['finalclass']))
1815			{
1816				$sLinkClass = $aValues['finalclass'];
1817				if (!is_subclass_of($sLinkClass, $sTargetClass))
1818				{
1819					throw new CoreException('Wrong class for link attribute specification',
1820						array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass));
1821				}
1822			}
1823			elseif (MetaModel::IsAbstract($sTargetClass))
1824			{
1825				throw new CoreException('Missing finalclass for link attribute specification');
1826			}
1827			else
1828			{
1829				$sLinkClass = $sTargetClass;
1830			}
1831
1832			$oLink = MetaModel::NewObject($sLinkClass);
1833			foreach($aValues as $sAttCode => $sValue)
1834			{
1835				$oLink->Set($sAttCode, $sValue);
1836			}
1837
1838			// 3rd - Set external keys from search conditions
1839			foreach($aExtKeys as $sKeyAttCode => $aReconciliation)
1840			{
1841				$oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
1842				$sKeyClass = $oKeyAttDef->GetTargetClass();
1843				$oExtKeyFilter = new DBObjectSearch($sKeyClass);
1844				$aReconciliationDesc = array();
1845				foreach($aReconciliation as $sRemoteAttCode => $sValue)
1846				{
1847					$oExtKeyFilter->AddCondition($sRemoteAttCode, $sValue, '=');
1848					$aReconciliationDesc[] = "$sRemoteAttCode=$sValue";
1849				}
1850				$oExtKeySet = new DBObjectSet($oExtKeyFilter);
1851				switch ($oExtKeySet->Count())
1852				{
1853					case 0:
1854						$sReconciliationDesc = implode(', ', $aReconciliationDesc);
1855						throw new CoreException("Found no match",
1856							array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc));
1857						break;
1858					case 1:
1859						$oRemoteObj = $oExtKeySet->Fetch();
1860						$oLink->Set($sKeyAttCode, $oRemoteObj->GetKey());
1861						break;
1862					default:
1863						$sReconciliationDesc = implode(', ', $aReconciliationDesc);
1864						throw new CoreException("Found several matches",
1865							array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc));
1866					// Found several matches, ambiguous
1867				}
1868			}
1869
1870			// Check (roughly) if such a link is valid
1871			$aErrors = array();
1872			foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef)
1873			{
1874				if ($oAttDef->IsExternalKey())
1875				{
1876					/** @var \AttributeExternalKey $oAttDef */
1877					if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(),
1878							$oAttDef->GetTargetClass())))
1879					{
1880						continue; // Don't check the key to self
1881					}
1882				}
1883
1884				if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed())
1885				{
1886					$aErrors[] = $sAttCode;
1887				}
1888			}
1889			if (count($aErrors) > 0)
1890			{
1891				throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors));
1892			}
1893
1894			$aLinks[] = $oLink;
1895		}
1896		$oSet = DBObjectSet::FromArray($sTargetClass, $aLinks);
1897
1898		return $oSet;
1899	}
1900
1901	/**
1902	 * Helper to get a value that will be JSON encoded
1903	 * The operation is the opposite to FromJSONToValue
1904	 *
1905	 * @param \ormLinkSet $value
1906	 *
1907	 * @return array
1908	 * @throws \CoreException
1909	 */
1910	public function GetForJSON($value)
1911	{
1912		$aRet = array();
1913		if (is_object($value) && ($value instanceof ormLinkSet))
1914		{
1915			$value->Rewind();
1916			while ($oObj = $value->Fetch())
1917			{
1918				$sObjClass = get_class($oObj);
1919				// Show only relevant information (hide the external key to the current object)
1920				$aAttributes = array();
1921				foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef)
1922				{
1923					if ($sAttCode == 'finalclass')
1924					{
1925						if ($sObjClass == $this->GetLinkedClass())
1926						{
1927							// Simplify the output if the exact class could be determined implicitely
1928							continue;
1929						}
1930					}
1931					if ($sAttCode == $this->GetExtKeyToMe())
1932					{
1933						continue;
1934					}
1935					if ($oAttDef->IsExternalField())
1936					{
1937						continue;
1938					}
1939					if (!$oAttDef->IsBasedOnDBColumns())
1940					{
1941						continue;
1942					}
1943					if (!$oAttDef->IsScalar())
1944					{
1945						continue;
1946					}
1947					$attValue = $oObj->Get($sAttCode);
1948					$aAttributes[$sAttCode] = $oAttDef->GetForJSON($attValue);
1949				}
1950				$aRet[] = $aAttributes;
1951			}
1952		}
1953
1954		return $aRet;
1955	}
1956
1957	/**
1958	 * Helper to form a value, given JSON decoded data
1959	 * The operation is the opposite to GetForJSON
1960	 *
1961	 * @param $json
1962	 *
1963	 * @return \DBObjectSet
1964	 * @throws \CoreException
1965	 * @throws \CoreUnexpectedValue
1966	 * @throws \Exception
1967	 */
1968	public function FromJSONToValue($json)
1969	{
1970		$sTargetClass = $this->Get('linked_class');
1971
1972		$aLinks = array();
1973		foreach($json as $aValues)
1974		{
1975			if (isset($aValues['finalclass']))
1976			{
1977				$sLinkClass = $aValues['finalclass'];
1978				if (!is_subclass_of($sLinkClass, $sTargetClass))
1979				{
1980					throw new CoreException('Wrong class for link attribute specification',
1981						array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass));
1982				}
1983			}
1984			elseif (MetaModel::IsAbstract($sTargetClass))
1985			{
1986				throw new CoreException('Missing finalclass for link attribute specification');
1987			}
1988			else
1989			{
1990				$sLinkClass = $sTargetClass;
1991			}
1992
1993			$oLink = MetaModel::NewObject($sLinkClass);
1994			foreach($aValues as $sAttCode => $sValue)
1995			{
1996				$oLink->Set($sAttCode, $sValue);
1997			}
1998
1999			// Check (roughly) if such a link is valid
2000			$aErrors = array();
2001			foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef)
2002			{
2003				if ($oAttDef->IsExternalKey())
2004				{
2005					/** @var AttributeExternalKey $oAttDef */
2006					if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(),
2007							$oAttDef->GetTargetClass())))
2008					{
2009						continue; // Don't check the key to self
2010					}
2011				}
2012
2013				if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed())
2014				{
2015					$aErrors[] = $sAttCode;
2016				}
2017			}
2018			if (count($aErrors) > 0)
2019			{
2020				throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors));
2021			}
2022
2023			$aLinks[] = $oLink;
2024		}
2025		$oSet = DBObjectSet::FromArray($sTargetClass, $aLinks);
2026
2027		return $oSet;
2028	}
2029
2030	/**
2031	 * @param $proposedValue
2032	 * @param $oHostObj
2033	 *
2034	 * @return mixed
2035	 * @throws \Exception
2036	 */
2037	public function MakeRealValue($proposedValue, $oHostObj)
2038	{
2039		if ($proposedValue === null)
2040		{
2041			$sLinkedClass = $this->GetLinkedClass();
2042			$aLinkedObjectsArray = array();
2043			$oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray);
2044
2045			return new ormLinkSet(
2046				get_class($oHostObj),
2047				$this->GetCode(),
2048				$oSet
2049			);
2050		}
2051
2052		return $proposedValue;
2053	}
2054
2055	/**
2056	 * @param ormLinkSet $val1
2057	 * @param ormLinkSet $val2
2058	 *
2059	 * @return bool
2060	 */
2061	public function Equals($val1, $val2)
2062	{
2063		if ($val1 === $val2)
2064		{
2065			$bAreEquivalent = true;
2066		}
2067		else
2068		{
2069			$bAreEquivalent = ($val2->HasDelta() === false);
2070		}
2071
2072		return $bAreEquivalent;
2073	}
2074
2075	/**
2076	 * Find the corresponding "link" attribute on the target class, if any
2077	 *
2078	 * @return null | AttributeDefinition
2079	 * @throws \Exception
2080	 */
2081	public function GetMirrorLinkAttribute()
2082	{
2083		$oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe());
2084
2085		return $oRemoteAtt;
2086	}
2087
2088	static public function GetFormFieldClass()
2089	{
2090		return '\\Combodo\\iTop\\Form\\Field\\LinkedSetField';
2091	}
2092
2093	/**
2094	 * @param \DBObject $oObject
2095	 * @param \Combodo\iTop\Form\Field\LinkedSetField $oFormField
2096	 *
2097	 * @return \Combodo\iTop\Form\Field\LinkedSetField
2098	 * @throws \CoreException
2099	 * @throws \DictExceptionMissingString
2100	 * @throws \Exception
2101	 */
2102	public function MakeFormField(DBObject $oObject, $oFormField = null)
2103	{
2104		if ($oFormField === null)
2105		{
2106			$sFormFieldClass = static::GetFormFieldClass();
2107			$oFormField = new $sFormFieldClass($this->GetCode());
2108		}
2109
2110		// Setting target class
2111		if (!$this->IsIndirect())
2112		{
2113			$sTargetClass = $this->GetLinkedClass();
2114		}
2115		else
2116		{
2117			/** @var \AttributeExternalKey $oRemoteAttDef */
2118			/** @var \AttributeLinkedSetIndirect $this */
2119			$oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
2120			$sTargetClass = $oRemoteAttDef->GetTargetClass();
2121
2122			/** @var \AttributeLinkedSetIndirect $this */
2123			$oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote());
2124		}
2125		$oFormField->SetTargetClass($sTargetClass);
2126		$oFormField->SetIndirect($this->IsIndirect());
2127		// Setting attcodes to display
2128		$aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list'));
2129		// - Adding friendlyname attribute to the list is not already in it
2130		$sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass);
2131		if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay))
2132		{
2133			$aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay);
2134		}
2135		// - Adding attribute labels
2136		$aAttributesToDisplay = array();
2137		foreach($aAttCodesToDisplay as $sAttCodeToDisplay)
2138		{
2139			$oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay);
2140			$aAttributesToDisplay[$sAttCodeToDisplay] = $oAttDefToDisplay->GetLabel();
2141		}
2142		$oFormField->SetAttributesToDisplay($aAttributesToDisplay);
2143
2144		parent::MakeFormField($oObject, $oFormField);
2145
2146		return $oFormField;
2147	}
2148
2149	public function IsPartOfFingerprint()
2150	{
2151		return false;
2152	}
2153}
2154
2155/**
2156 * Set of objects linked to an object (n-n), and being part of its definition
2157 *
2158 * @package     iTopORM
2159 */
2160class AttributeLinkedSetIndirect extends AttributeLinkedSet
2161{
2162	static public function ListExpectedParams()
2163	{
2164		return array_merge(parent::ListExpectedParams(), array("ext_key_to_remote"));
2165	}
2166
2167	public function IsIndirect()
2168	{
2169		return true;
2170	}
2171
2172	public function GetExtKeyToRemote()
2173	{
2174		return $this->Get('ext_key_to_remote');
2175	}
2176
2177	public function GetEditClass()
2178	{
2179		return "LinkedSet";
2180	}
2181
2182	public function DuplicatesAllowed()
2183	{
2184		return $this->GetOptional("duplicates", false);
2185	} // The same object may be linked several times... or not...
2186
2187	public function GetTrackingLevel()
2188	{
2189		return $this->GetOptional('tracking_level',
2190			MetaModel::GetConfig()->Get('tracking_level_linked_set_indirect_default'));
2191	}
2192
2193	/**
2194	 * Find the corresponding "link" attribute on the target class, if any
2195	 *
2196	 * @return null | AttributeDefinition
2197	 * @throws \CoreException
2198	 */
2199	public function GetMirrorLinkAttribute()
2200	{
2201		$oRet = null;
2202		/** @var \AttributeExternalKey $oExtKeyToRemote */
2203		$oExtKeyToRemote = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
2204		$sRemoteClass = $oExtKeyToRemote->GetTargetClass();
2205		foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef)
2206		{
2207			if (!$oRemoteAttDef instanceof AttributeLinkedSetIndirect)
2208			{
2209				continue;
2210			}
2211			if ($oRemoteAttDef->GetLinkedClass() != $this->GetLinkedClass())
2212			{
2213				continue;
2214			}
2215			if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetExtKeyToRemote())
2216			{
2217				continue;
2218			}
2219			if ($oRemoteAttDef->GetExtKeyToRemote() != $this->GetExtKeyToMe())
2220			{
2221				continue;
2222			}
2223			$oRet = $oRemoteAttDef;
2224			break;
2225		}
2226
2227		return $oRet;
2228	}
2229}
2230
2231/**
2232 * Abstract class implementing default filters for a DB column
2233 *
2234 * @package     iTopORM
2235 */
2236class AttributeDBFieldVoid extends AttributeDefinition
2237{
2238	static public function ListExpectedParams()
2239	{
2240		return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "sql"));
2241	}
2242
2243	// To be overriden, used in GetSQLColumns
2244	protected function GetSQLCol($bFullSpec = false)
2245	{
2246		return 'VARCHAR(255)'
2247			.CMDBSource::GetSqlStringColumnDefinition()
2248			.($bFullSpec ? $this->GetSQLColSpec() : '');
2249	}
2250
2251	protected function GetSQLColSpec()
2252	{
2253		$default = $this->ScalarToSQL($this->GetDefaultValue());
2254		if (is_null($default))
2255		{
2256			$sRet = '';
2257		}
2258		else
2259		{
2260			if (is_numeric($default))
2261			{
2262				// Though it is a string in PHP, it will be considered as a numeric value in MySQL
2263				// Then it must not be quoted here, to preserve the compatibility with the value returned by CMDBSource::GetFieldSpec
2264				$sRet = " DEFAULT $default";
2265			}
2266			else
2267			{
2268				$sRet = " DEFAULT ".CMDBSource::Quote($default);
2269			}
2270		}
2271
2272		return $sRet;
2273	}
2274
2275	public function GetEditClass()
2276	{
2277		return "String";
2278	}
2279
2280	public function GetValuesDef()
2281	{
2282		return $this->Get("allowed_values");
2283	}
2284
2285	public function GetPrerequisiteAttributes($sClass = null)
2286	{
2287		return $this->Get("depends_on");
2288	}
2289
2290	static public function IsBasedOnDBColumns()
2291	{
2292		return true;
2293	}
2294
2295	static public function IsScalar()
2296	{
2297		return true;
2298	}
2299
2300	public function IsWritable()
2301	{
2302		return !$this->IsMagic();
2303	}
2304
2305	public function GetSQLExpr()
2306	{
2307		return $this->Get("sql");
2308	}
2309
2310	public function GetDefaultValue(DBObject $oHostObject = null)
2311	{
2312		return $this->MakeRealValue("", $oHostObject);
2313	}
2314
2315	public function IsNullAllowed()
2316	{
2317		return false;
2318	}
2319
2320	//
2321	protected function ScalarToSQL($value)
2322	{
2323		return $value;
2324	} // format value as a valuable SQL literal (quoted outside)
2325
2326	public function GetSQLExpressions($sPrefix = '')
2327	{
2328		$aColumns = array();
2329		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
2330		$aColumns[''] = $this->Get("sql");
2331
2332		return $aColumns;
2333	}
2334
2335	public function FromSQLToValue($aCols, $sPrefix = '')
2336	{
2337		$value = $this->MakeRealValue($aCols[$sPrefix.''], null);
2338
2339		return $value;
2340	}
2341
2342	public function GetSQLValues($value)
2343	{
2344		$aValues = array();
2345		$aValues[$this->Get("sql")] = $this->ScalarToSQL($value);
2346
2347		return $aValues;
2348	}
2349
2350	public function GetSQLColumns($bFullSpec = false)
2351	{
2352		$aColumns = array();
2353		$aColumns[$this->Get("sql")] = $this->GetSQLCol($bFullSpec);
2354
2355		return $aColumns;
2356	}
2357
2358	public function GetFilterDefinitions()
2359	{
2360		return array($this->GetCode() => new FilterFromAttribute($this));
2361	}
2362
2363	public function GetBasicFilterOperators()
2364	{
2365		return array("=" => "equals", "!=" => "differs from");
2366	}
2367
2368	public function GetBasicFilterLooseOperator()
2369	{
2370		return "=";
2371	}
2372
2373	public function GetBasicFilterSQLExpr($sOpCode, $value)
2374	{
2375		$sQValue = CMDBSource::Quote($value);
2376		switch ($sOpCode)
2377		{
2378			case '!=':
2379				return $this->GetSQLExpr()." != $sQValue";
2380				break;
2381			case '=':
2382			default:
2383				return $this->GetSQLExpr()." = $sQValue";
2384		}
2385	}
2386}
2387
2388/**
2389 * Base class for all kind of DB attributes, with the exception of external keys
2390 *
2391 * @package     iTopORM
2392 */
2393class AttributeDBField extends AttributeDBFieldVoid
2394{
2395	static public function ListExpectedParams()
2396	{
2397		return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed"));
2398	}
2399
2400	public function GetDefaultValue(DBObject $oHostObject = null)
2401	{
2402		return $this->MakeRealValue($this->Get("default_value"), $oHostObject);
2403	}
2404
2405	public function IsNullAllowed()
2406	{
2407		return $this->Get("is_null_allowed");
2408	}
2409}
2410
2411/**
2412 * Map an integer column to an attribute
2413 *
2414 * @package     iTopORM
2415 */
2416class AttributeInteger extends AttributeDBField
2417{
2418	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
2419
2420	static public function ListExpectedParams()
2421	{
2422		return parent::ListExpectedParams();
2423		//return array_merge(parent::ListExpectedParams(), array());
2424	}
2425
2426	public function GetEditClass()
2427	{
2428		return "String";
2429	}
2430
2431	protected function GetSQLCol($bFullSpec = false)
2432	{
2433		return "INT(11)".($bFullSpec ? $this->GetSQLColSpec() : '');
2434	}
2435
2436	public function GetValidationPattern()
2437	{
2438		return "^[0-9]+$";
2439	}
2440
2441	public function GetBasicFilterOperators()
2442	{
2443		return array(
2444			"!=" => "differs from",
2445			"=" => "equals",
2446			">" => "greater (strict) than",
2447			">=" => "greater than",
2448			"<" => "less (strict) than",
2449			"<=" => "less than",
2450			"in" => "in"
2451		);
2452	}
2453
2454	public function GetBasicFilterLooseOperator()
2455	{
2456		// Unless we implement an "equals approximately..." or "same order of magnitude"
2457		return "=";
2458	}
2459
2460	public function GetBasicFilterSQLExpr($sOpCode, $value)
2461	{
2462		$sQValue = CMDBSource::Quote($value);
2463		switch ($sOpCode)
2464		{
2465			case '!=':
2466				return $this->GetSQLExpr()." != $sQValue";
2467				break;
2468			case '>':
2469				return $this->GetSQLExpr()." > $sQValue";
2470				break;
2471			case '>=':
2472				return $this->GetSQLExpr()." >= $sQValue";
2473				break;
2474			case '<':
2475				return $this->GetSQLExpr()." < $sQValue";
2476				break;
2477			case '<=':
2478				return $this->GetSQLExpr()." <= $sQValue";
2479				break;
2480			case 'in':
2481				if (!is_array($value))
2482				{
2483					throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')");
2484				}
2485
2486				return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')";
2487				break;
2488
2489			case '=':
2490			default:
2491				return $this->GetSQLExpr()." = \"$value\"";
2492		}
2493	}
2494
2495	public function GetNullValue()
2496	{
2497		return null;
2498	}
2499
2500	public function IsNull($proposedValue)
2501	{
2502		return is_null($proposedValue);
2503	}
2504
2505	public function MakeRealValue($proposedValue, $oHostObj)
2506	{
2507		if (is_null($proposedValue))
2508		{
2509			return null;
2510		}
2511		if ($proposedValue === '')
2512		{
2513			return null;
2514		} // 0 is transformed into '' !
2515
2516		return (int)$proposedValue;
2517	}
2518
2519	public function ScalarToSQL($value)
2520	{
2521		assert(is_numeric($value) || is_null($value));
2522
2523		return $value; // supposed to be an int
2524	}
2525}
2526
2527/**
2528 * An external key for which the class is defined as the value of another attribute
2529 *
2530 * @package     iTopORM
2531 */
2532class AttributeObjectKey extends AttributeDBFieldVoid
2533{
2534	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY;
2535
2536	static public function ListExpectedParams()
2537	{
2538		return array_merge(parent::ListExpectedParams(), array('class_attcode', 'is_null_allowed'));
2539	}
2540
2541	public function GetEditClass()
2542	{
2543		return "String";
2544	}
2545
2546	protected function GetSQLCol($bFullSpec = false)
2547	{
2548		return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");
2549	}
2550
2551	public function GetDefaultValue(DBObject $oHostObject = null)
2552	{
2553		return 0;
2554	}
2555
2556	public function IsNullAllowed()
2557	{
2558		return $this->Get("is_null_allowed");
2559	}
2560
2561
2562	public function GetBasicFilterOperators()
2563	{
2564		return parent::GetBasicFilterOperators();
2565	}
2566
2567	public function GetBasicFilterLooseOperator()
2568	{
2569		return parent::GetBasicFilterLooseOperator();
2570	}
2571
2572	public function GetBasicFilterSQLExpr($sOpCode, $value)
2573	{
2574		return parent::GetBasicFilterSQLExpr($sOpCode, $value);
2575	}
2576
2577	public function GetNullValue()
2578	{
2579		return 0;
2580	}
2581
2582	public function IsNull($proposedValue)
2583	{
2584		return ($proposedValue == 0);
2585	}
2586
2587	public function MakeRealValue($proposedValue, $oHostObj)
2588	{
2589		if (is_null($proposedValue))
2590		{
2591			return 0;
2592		}
2593		if ($proposedValue === '')
2594		{
2595			return 0;
2596		}
2597		if (MetaModel::IsValidObject($proposedValue))
2598		{
2599			/** @var \DBObject $proposedValue */
2600			return $proposedValue->GetKey();
2601		}
2602
2603		return (int)$proposedValue;
2604	}
2605}
2606
2607/**
2608 * Display an integer between 0 and 100 as a percentage / horizontal bar graph
2609 *
2610 * @package     iTopORM
2611 */
2612class AttributePercentage extends AttributeInteger
2613{
2614	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
2615
2616	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
2617	{
2618		$iWidth = 5; // Total width of the percentage bar graph, in em...
2619		$iValue = (int)$sValue;
2620		if ($iValue > 100)
2621		{
2622			$iValue = 100;
2623		}
2624		else
2625		{
2626			if ($iValue < 0)
2627			{
2628				$iValue = 0;
2629			}
2630		}
2631		if ($iValue > 90)
2632		{
2633			$sColor = "#cc3300";
2634		}
2635		else
2636		{
2637			if ($iValue > 50)
2638			{
2639				$sColor = "#cccc00";
2640			}
2641			else
2642			{
2643				$sColor = "#33cc00";
2644			}
2645		}
2646		$iPercentWidth = ($iWidth * $iValue) / 100;
2647
2648		return "<div style=\"width:{$iWidth}em;-moz-border-radius: 3px;-webkit-border-radius: 3px;border-radius: 3px;display:inline-block;border: 1px #ccc solid;\"><div style=\"width:{$iPercentWidth}em; display:inline-block;background-color:$sColor;\">&nbsp;</div></div>&nbsp;$sValue %";
2649	}
2650}
2651
2652/**
2653 * Map a decimal value column (suitable for financial computations) to an attribute
2654 * internally in PHP such numbers are represented as string. Should you want to perform
2655 * a calculation on them, it is recommended to use the BC Math functions in order to
2656 * retain the precision
2657 *
2658 * @package     iTopORM
2659 */
2660class AttributeDecimal extends AttributeDBField
2661{
2662	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
2663
2664	static public function ListExpectedParams()
2665	{
2666		return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */));
2667	}
2668
2669	public function GetEditClass()
2670	{
2671		return "String";
2672	}
2673
2674	protected function GetSQLCol($bFullSpec = false)
2675	{
2676		return "DECIMAL(".$this->Get('digits').",".$this->Get('decimals').")".($bFullSpec ? $this->GetSQLColSpec() : '');
2677	}
2678
2679	public function GetValidationPattern()
2680	{
2681		$iNbDigits = $this->Get('digits');
2682		$iPrecision = $this->Get('decimals');
2683		$iNbIntegerDigits = $iNbDigits - $iPrecision - 1; // -1 because the first digit is treated separately in the pattern below
2684
2685		return "^[-+]?[0-9]\d{0,$iNbIntegerDigits}(\.\d{0,$iPrecision})?$";
2686	}
2687
2688	public function GetBasicFilterOperators()
2689	{
2690		return array(
2691			"!=" => "differs from",
2692			"=" => "equals",
2693			">" => "greater (strict) than",
2694			">=" => "greater than",
2695			"<" => "less (strict) than",
2696			"<=" => "less than",
2697			"in" => "in"
2698		);
2699	}
2700
2701	public function GetBasicFilterLooseOperator()
2702	{
2703		// Unless we implement an "equals approximately..." or "same order of magnitude"
2704		return "=";
2705	}
2706
2707	public function GetBasicFilterSQLExpr($sOpCode, $value)
2708	{
2709		$sQValue = CMDBSource::Quote($value);
2710		switch ($sOpCode)
2711		{
2712			case '!=':
2713				return $this->GetSQLExpr()." != $sQValue";
2714				break;
2715			case '>':
2716				return $this->GetSQLExpr()." > $sQValue";
2717				break;
2718			case '>=':
2719				return $this->GetSQLExpr()." >= $sQValue";
2720				break;
2721			case '<':
2722				return $this->GetSQLExpr()." < $sQValue";
2723				break;
2724			case '<=':
2725				return $this->GetSQLExpr()." <= $sQValue";
2726				break;
2727			case 'in':
2728				if (!is_array($value))
2729				{
2730					throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')");
2731				}
2732
2733				return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')";
2734				break;
2735
2736			case '=':
2737			default:
2738				return $this->GetSQLExpr()." = \"$value\"";
2739		}
2740	}
2741
2742	public function GetNullValue()
2743	{
2744		return null;
2745	}
2746
2747	public function IsNull($proposedValue)
2748	{
2749		return is_null($proposedValue);
2750	}
2751
2752	public function MakeRealValue($proposedValue, $oHostObj)
2753	{
2754		if (is_null($proposedValue))
2755		{
2756			return null;
2757		}
2758		if ($proposedValue === '')
2759		{
2760			return null;
2761		}
2762
2763		return $this->ScalarToSQL($proposedValue);
2764	}
2765
2766	public function ScalarToSQL($value)
2767	{
2768		assert(is_null($value) || preg_match('/'.$this->GetValidationPattern().'/', $value));
2769
2770		if (!is_null($value) && ($value !== ''))
2771		{
2772			$value = sprintf("%01.".$this->Get('decimals')."f", $value);
2773		}
2774		return $value; // null or string
2775	}
2776}
2777
2778/**
2779 * Map a boolean column to an attribute
2780 *
2781 * @package     iTopORM
2782 */
2783class AttributeBoolean extends AttributeInteger
2784{
2785	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
2786
2787	static public function ListExpectedParams()
2788	{
2789		return parent::ListExpectedParams();
2790		//return array_merge(parent::ListExpectedParams(), array());
2791	}
2792
2793	public function GetEditClass()
2794	{
2795		return "Integer";
2796	}
2797
2798	protected function GetSQLCol($bFullSpec = false)
2799	{
2800		return "TINYINT(1)".($bFullSpec ? $this->GetSQLColSpec() : '');
2801	}
2802
2803	public function MakeRealValue($proposedValue, $oHostObj)
2804	{
2805		if (is_null($proposedValue))
2806		{
2807			return null;
2808		}
2809		if ($proposedValue === '')
2810		{
2811			return null;
2812		}
2813		if ((int)$proposedValue)
2814		{
2815			return true;
2816		}
2817
2818		return false;
2819	}
2820
2821	public function ScalarToSQL($value)
2822	{
2823		if ($value)
2824		{
2825			return 1;
2826		}
2827
2828		return 0;
2829	}
2830
2831	public function GetValueLabel($bValue)
2832	{
2833		if (is_null($bValue))
2834		{
2835			$sLabel = Dict::S('Core:'.get_class($this).'/Value:null');
2836		}
2837		else
2838		{
2839			$sValue = $bValue ? 'yes' : 'no';
2840			$sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue);
2841			$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, true /*user lang*/);
2842		}
2843
2844		return $sLabel;
2845	}
2846
2847	public function GetValueDescription($bValue)
2848	{
2849		if (is_null($bValue))
2850		{
2851			$sDescription = Dict::S('Core:'.get_class($this).'/Value:null+');
2852		}
2853		else
2854		{
2855			$sValue = $bValue ? 'yes' : 'no';
2856			$sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue.'+');
2857			$sDescription = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue.'+', $sDefault,
2858				true /*user lang*/);
2859		}
2860
2861		return $sDescription;
2862	}
2863
2864	public function GetAsHTML($bValue, $oHostObject = null, $bLocalize = true)
2865	{
2866		if (is_null($bValue))
2867		{
2868			$sRes = '';
2869		}
2870		elseif ($bLocalize)
2871		{
2872			$sLabel = $this->GetValueLabel($bValue);
2873			$sDescription = $this->GetValueDescription($bValue);
2874			// later, we could imagine a detailed description in the title
2875			$sRes = "<span title=\"$sDescription\">".parent::GetAsHtml($sLabel)."</span>";
2876		}
2877		else
2878		{
2879			$sRes = $bValue ? 'yes' : 'no';
2880		}
2881
2882		return $sRes;
2883	}
2884
2885	public function GetAsXML($bValue, $oHostObject = null, $bLocalize = true)
2886	{
2887		if (is_null($bValue))
2888		{
2889			$sFinalValue = '';
2890		}
2891		elseif ($bLocalize)
2892		{
2893			$sFinalValue = $this->GetValueLabel($bValue);
2894		}
2895		else
2896		{
2897			$sFinalValue = $bValue ? 'yes' : 'no';
2898		}
2899		$sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize);
2900
2901		return $sRes;
2902	}
2903
2904	public function GetAsCSV(
2905		$bValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
2906		$bConvertToPlainText = false
2907	) {
2908		if (is_null($bValue))
2909		{
2910			$sFinalValue = '';
2911		}
2912		elseif ($bLocalize)
2913		{
2914			$sFinalValue = $this->GetValueLabel($bValue);
2915		}
2916		else
2917		{
2918			$sFinalValue = $bValue ? 'yes' : 'no';
2919		}
2920		$sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize);
2921
2922		return $sRes;
2923	}
2924
2925	static public function GetFormFieldClass()
2926	{
2927		return '\\Combodo\\iTop\\Form\\Field\\SelectField';
2928	}
2929
2930	/**
2931	 * @param \DBObject $oObject
2932	 * @param \Combodo\iTop\Form\Field\SelectField $oFormField
2933	 *
2934	 * @return \Combodo\iTop\Form\Field\SelectField
2935	 * @throws \CoreException
2936	 */
2937	public function MakeFormField(DBObject $oObject, $oFormField = null)
2938	{
2939		if ($oFormField === null)
2940		{
2941			$sFormFieldClass = static::GetFormFieldClass();
2942			$oFormField = new $sFormFieldClass($this->GetCode());
2943		}
2944
2945		$oFormField->SetChoices(array('yes' => $this->GetValueLabel(true), 'no' => $this->GetValueLabel(false)));
2946		parent::MakeFormField($oObject, $oFormField);
2947
2948		return $oFormField;
2949	}
2950
2951	public function GetEditValue($value, $oHostObj = null)
2952	{
2953		if (is_null($value))
2954		{
2955			return '';
2956		}
2957		else
2958		{
2959			return $this->GetValueLabel($value);
2960		}
2961	}
2962
2963	/**
2964	 * Helper to get a value that will be JSON encoded
2965	 * The operation is the opposite to FromJSONToValue
2966	 *
2967	 * @param $value
2968	 *
2969	 * @return bool
2970	 */
2971	public function GetForJSON($value)
2972	{
2973		return (bool)$value;
2974	}
2975
2976	public function MakeValueFromString(
2977		$sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
2978		$sAttributeQualifier = null
2979	) {
2980		$sInput = strtolower(trim($sProposedValue));
2981		if ($bLocalizedValue)
2982		{
2983			switch ($sInput)
2984			{
2985				case '1': // backward compatibility
2986				case $this->GetValueLabel(true):
2987					$value = true;
2988					break;
2989				case '0': // backward compatibility
2990				case 'no':
2991				case $this->GetValueLabel(false):
2992					$value = false;
2993					break;
2994				default:
2995					$value = null;
2996			}
2997		}
2998		else
2999		{
3000			switch ($sInput)
3001			{
3002				case '1': // backward compatibility
3003				case 'yes':
3004					$value = true;
3005					break;
3006				case '0': // backward compatibility
3007				case 'no':
3008					$value = false;
3009					break;
3010				default:
3011					$value = null;
3012			}
3013		}
3014
3015		return $value;
3016	}
3017}
3018
3019/**
3020 * Map a varchar column (size < ?) to an attribute
3021 *
3022 * @package     iTopORM
3023 */
3024class AttributeString extends AttributeDBField
3025{
3026	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
3027
3028	static public function ListExpectedParams()
3029	{
3030		return parent::ListExpectedParams();
3031		//return array_merge(parent::ListExpectedParams(), array());
3032	}
3033
3034	public function GetEditClass()
3035	{
3036		return "String";
3037	}
3038
3039	protected function GetSQLCol($bFullSpec = false)
3040	{
3041		return 'VARCHAR(255)'
3042			.CMDBSource::GetSqlStringColumnDefinition()
3043			.($bFullSpec ? $this->GetSQLColSpec() : '');
3044	}
3045
3046	public function GetValidationPattern()
3047	{
3048		$sPattern = $this->GetOptional('validation_pattern', '');
3049		if (empty($sPattern))
3050		{
3051			return parent::GetValidationPattern();
3052		}
3053		else
3054		{
3055			return $sPattern;
3056		}
3057	}
3058
3059	public function CheckFormat($value)
3060	{
3061		$sRegExp = $this->GetValidationPattern();
3062		if (empty($sRegExp))
3063		{
3064			return true;
3065		}
3066		else
3067		{
3068			$sRegExp = str_replace('/', '\\/', $sRegExp);
3069
3070			return preg_match("/$sRegExp/", $value);
3071		}
3072	}
3073
3074	public function GetMaxSize()
3075	{
3076		return 255;
3077	}
3078
3079	public function GetBasicFilterOperators()
3080	{
3081		return array(
3082			"=" => "equals",
3083			"!=" => "differs from",
3084			"Like" => "equals (no case)",
3085			"NotLike" => "differs from (no case)",
3086			"Contains" => "contains",
3087			"Begins with" => "begins with",
3088			"Finishes with" => "finishes with"
3089		);
3090	}
3091
3092	public function GetBasicFilterLooseOperator()
3093	{
3094		return "Contains";
3095	}
3096
3097	public function GetBasicFilterSQLExpr($sOpCode, $value)
3098	{
3099		$sQValue = CMDBSource::Quote($value);
3100		switch ($sOpCode)
3101		{
3102			case '=':
3103			case '!=':
3104				return $this->GetSQLExpr()." $sOpCode $sQValue";
3105			case 'Begins with':
3106				return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("$value%");
3107			case 'Finishes with':
3108				return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value");
3109			case 'Contains':
3110				return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%");
3111			case 'NotLike':
3112				return $this->GetSQLExpr()." NOT LIKE $sQValue";
3113			case 'Like':
3114			default:
3115				return $this->GetSQLExpr()." LIKE $sQValue";
3116		}
3117	}
3118
3119	public function GetNullValue()
3120	{
3121		return '';
3122	}
3123
3124	public function IsNull($proposedValue)
3125	{
3126		return ($proposedValue == '');
3127	}
3128
3129	public function MakeRealValue($proposedValue, $oHostObj)
3130	{
3131		if (is_null($proposedValue))
3132		{
3133			return '';
3134		}
3135
3136		return (string)$proposedValue;
3137	}
3138
3139	public function ScalarToSQL($value)
3140	{
3141		if (!is_string($value) && !is_null($value))
3142		{
3143			throw new CoreWarning('Expected the attribute value to be a string', array(
3144				'found_type' => gettype($value),
3145				'value' => $value,
3146				'class' => $this->GetHostClass(),
3147				'attribute' => $this->GetCode()
3148			));
3149		}
3150
3151		return $value;
3152	}
3153
3154	public function GetAsCSV(
3155		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
3156		$bConvertToPlainText = false
3157	) {
3158		$sFrom = array("\r\n", $sTextQualifier);
3159		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
3160		$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
3161
3162		return $sTextQualifier.$sEscaped.$sTextQualifier;
3163	}
3164
3165	public function GetDisplayStyle()
3166	{
3167		return $this->GetOptional('display_style', 'select');
3168	}
3169
3170	static public function GetFormFieldClass()
3171	{
3172		return '\\Combodo\\iTop\\Form\\Field\\StringField';
3173	}
3174
3175	public function MakeFormField(DBObject $oObject, $oFormField = null)
3176	{
3177		if ($oFormField === null)
3178		{
3179			$sFormFieldClass = static::GetFormFieldClass();
3180			$oFormField = new $sFormFieldClass($this->GetCode());
3181		}
3182		parent::MakeFormField($oObject, $oFormField);
3183
3184		return $oFormField;
3185	}
3186
3187}
3188
3189/**
3190 * An attribute that matches an object class
3191 *
3192 * @package     iTopORM
3193 */
3194class AttributeClass extends AttributeString
3195{
3196	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM;
3197
3198	static public function ListExpectedParams()
3199	{
3200		return array_merge(parent::ListExpectedParams(), array("class_category", "more_values"));
3201	}
3202
3203	public function __construct($sCode, $aParams)
3204	{
3205		$this->m_sCode = $sCode;
3206		$aParams["allowed_values"] = new ValueSetEnumClasses($aParams['class_category'], $aParams['more_values']);
3207		parent::__construct($sCode, $aParams);
3208	}
3209
3210	public function GetDefaultValue(DBObject $oHostObject = null)
3211	{
3212		$sDefault = parent::GetDefaultValue($oHostObject);
3213		if (!$this->IsNullAllowed() && $this->IsNull($sDefault))
3214		{
3215			// For this kind of attribute specifying null as default value
3216			// is authorized even if null is not allowed
3217
3218			// Pick the first one...
3219			$aClasses = $this->GetAllowedValues();
3220			$sDefault = key($aClasses);
3221		}
3222
3223		return $sDefault;
3224	}
3225
3226	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
3227	{
3228		if (empty($sValue))
3229		{
3230			return '';
3231		}
3232
3233		return MetaModel::GetName($sValue);
3234	}
3235
3236	public function RequiresIndex()
3237	{
3238		return true;
3239	}
3240
3241	public function GetBasicFilterLooseOperator()
3242	{
3243		return '=';
3244	}
3245
3246}
3247
3248
3249/**
3250 * An attribute that matches a class state
3251 *
3252 * @package     iTopORM
3253 */
3254class AttributeClassState extends AttributeString
3255{
3256	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
3257
3258	static public function ListExpectedParams()
3259	{
3260		return array_merge(parent::ListExpectedParams(), array('class_field'));
3261	}
3262
3263	public function GetAllowedValues($aArgs = array(), $sContains = '')
3264	{
3265		if (isset($aArgs['this']))
3266		{
3267			$oHostObj = $aArgs['this'];
3268			$sTargetClass = $this->Get('class_field');
3269			$sClass = $oHostObj->Get($sTargetClass);
3270
3271			$aAllowedStates = array();
3272			foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass)
3273			{
3274				$aValues = MetaModel::EnumStates($sChildClass);
3275				foreach (array_keys($aValues) as $sState)
3276				{
3277					$aAllowedStates[$sState] = $sState.' ('.MetaModel::GetStateLabel($sChildClass, $sState).')';
3278				}
3279			}
3280			return $aAllowedStates;
3281		}
3282
3283		return null;
3284	}
3285
3286	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
3287	{
3288		if (empty($sValue))
3289		{
3290			return '';
3291		}
3292
3293		if (!empty($oHostObject))
3294		{
3295			$sTargetClass = $this->Get('class_field');
3296			$sClass = $oHostObject->Get($sTargetClass);
3297			foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass)
3298			{
3299				$aValues = MetaModel::EnumStates($sChildClass);
3300				if (in_array($sValue, $aValues))
3301				{
3302					$sHTML = '<span class="attribute-set-item" data-code="'.$sValue.'" data-label="'.$sValue.' ('.MetaModel::GetStateLabel($sChildClass, $sValue).')'.'" data-description="">'.$sValue.'</span>';
3303					return $sHTML;
3304				}
3305			}
3306		}
3307
3308		return $sValue;
3309	}
3310
3311}
3312
3313/**
3314 * An attibute that matches one of the language codes availables in the dictionnary
3315 *
3316 * @package     iTopORM
3317 */
3318class AttributeApplicationLanguage extends AttributeString
3319{
3320	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
3321
3322	static public function ListExpectedParams()
3323	{
3324		return parent::ListExpectedParams();
3325	}
3326
3327	public function __construct($sCode, $aParams)
3328	{
3329		$this->m_sCode = $sCode;
3330		$aAvailableLanguages = Dict::GetLanguages();
3331		$aLanguageCodes = array();
3332		foreach($aAvailableLanguages as $sLangCode => $aInfo)
3333		{
3334			$aLanguageCodes[$sLangCode] = $aInfo['description'].' ('.$aInfo['localized_description'].')';
3335		}
3336		$aParams["allowed_values"] = new ValueSetEnum($aLanguageCodes);
3337		parent::__construct($sCode, $aParams);
3338	}
3339
3340	public function RequiresIndex()
3341	{
3342		return true;
3343	}
3344
3345	public function GetBasicFilterLooseOperator()
3346	{
3347		return '=';
3348	}
3349}
3350
3351/**
3352 * The attribute dedicated to the finalclass automatic attribute
3353 *
3354 * @package     iTopORM
3355 */
3356class AttributeFinalClass extends AttributeString
3357{
3358	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
3359	public $m_sValue;
3360
3361	public function __construct($sCode, $aParams)
3362	{
3363		$this->m_sCode = $sCode;
3364		$aParams["allowed_values"] = null;
3365		parent::__construct($sCode, $aParams);
3366
3367		$this->m_sValue = $this->Get("default_value");
3368	}
3369
3370	public function IsWritable()
3371	{
3372		return false;
3373	}
3374
3375	public function IsMagic()
3376	{
3377		return true;
3378	}
3379
3380	public function RequiresIndex()
3381	{
3382		return true;
3383	}
3384
3385	public function SetFixedValue($sValue)
3386	{
3387		$this->m_sValue = $sValue;
3388	}
3389
3390	public function GetDefaultValue(DBObject $oHostObject = null)
3391	{
3392		return $this->m_sValue;
3393	}
3394
3395	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
3396	{
3397		if (empty($sValue))
3398		{
3399			return '';
3400		}
3401		if ($bLocalize)
3402		{
3403			return MetaModel::GetName($sValue);
3404		}
3405		else
3406		{
3407			return $sValue;
3408		}
3409	}
3410
3411	/**
3412	 * An enum can be localized
3413	 *
3414	 * @param string $sProposedValue
3415	 * @param bool $bLocalizedValue
3416	 * @param string $sSepItem
3417	 * @param string $sSepAttribute
3418	 * @param string $sSepValue
3419	 * @param string $sAttributeQualifier
3420	 *
3421	 * @return mixed|null|string
3422	 * @throws \CoreException
3423	 * @throws \OQLException
3424	 */
3425	public function MakeValueFromString(
3426		$sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
3427		$sAttributeQualifier = null
3428	) {
3429		if ($bLocalizedValue)
3430		{
3431			// Lookup for the value matching the input
3432			//
3433			$sFoundValue = null;
3434			$aRawValues = self::GetAllowedValues();
3435			if (!is_null($aRawValues))
3436			{
3437				foreach($aRawValues as $sKey => $sValue)
3438				{
3439					if ($sProposedValue == $sValue)
3440					{
3441						$sFoundValue = $sKey;
3442						break;
3443					}
3444				}
3445			}
3446			if (is_null($sFoundValue))
3447			{
3448				return null;
3449			}
3450
3451			return $this->MakeRealValue($sFoundValue, null);
3452		}
3453		else
3454		{
3455			return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue,
3456				$sAttributeQualifier);
3457		}
3458	}
3459
3460
3461	// Because this is sometimes used to get a localized/string version of an attribute...
3462	public function GetEditValue($sValue, $oHostObj = null)
3463	{
3464		if (empty($sValue))
3465		{
3466			return '';
3467		}
3468
3469		return MetaModel::GetName($sValue);
3470	}
3471
3472	/**
3473	 * Helper to get a value that will be JSON encoded
3474	 * The operation is the opposite to FromJSONToValue
3475	 *
3476	 * @param $value
3477	 *
3478	 * @return string
3479	 */
3480	public function GetForJSON($value)
3481	{
3482		// JSON values are NOT localized
3483		return $value;
3484	}
3485
3486	/**
3487	 * @param $value
3488	 * @param string $sSeparator
3489	 * @param string $sTextQualifier
3490	 * @param \DBObject $oHostObject
3491	 * @param bool $bLocalize
3492	 * @param bool $bConvertToPlainText
3493	 *
3494	 * @return string
3495	 * @throws \CoreException
3496	 * @throws \DictExceptionMissingString
3497	 */
3498	public function GetAsCSV(
3499		$value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
3500		$bConvertToPlainText = false
3501	) {
3502		if ($bLocalize && $value != '')
3503		{
3504			$sRawValue = MetaModel::GetName($value);
3505		}
3506		else
3507		{
3508			$sRawValue = $value;
3509		}
3510
3511		return parent::GetAsCSV($sRawValue, $sSeparator, $sTextQualifier, null, false, $bConvertToPlainText);
3512	}
3513
3514	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
3515	{
3516		if (empty($value))
3517		{
3518			return '';
3519		}
3520		if ($bLocalize)
3521		{
3522			$sRawValue = MetaModel::GetName($value);
3523		}
3524		else
3525		{
3526			$sRawValue = $value;
3527		}
3528
3529		return Str::pure2xml($sRawValue);
3530	}
3531
3532	public function GetBasicFilterLooseOperator()
3533	{
3534		return '=';
3535	}
3536
3537	public function GetValueLabel($sValue)
3538	{
3539		if (empty($sValue))
3540		{
3541			return '';
3542		}
3543
3544		return MetaModel::GetName($sValue);
3545	}
3546
3547	public function GetAllowedValues($aArgs = array(), $sContains = '')
3548	{
3549		$aRawValues = MetaModel::EnumChildClasses($this->GetHostClass(), ENUM_CHILD_CLASSES_ALL);
3550		$aLocalizedValues = array();
3551		foreach($aRawValues as $sClass)
3552		{
3553			$aLocalizedValues[$sClass] = MetaModel::GetName($sClass);
3554		}
3555
3556		return $aLocalizedValues;
3557	}
3558}
3559
3560
3561/**
3562 * Map a varchar column (size < ?) to an attribute that must never be shown to the user
3563 *
3564 * @package     iTopORM
3565 */
3566class AttributePassword extends AttributeString
3567{
3568	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
3569
3570	static public function ListExpectedParams()
3571	{
3572		return parent::ListExpectedParams();
3573		//return array_merge(parent::ListExpectedParams(), array());
3574	}
3575
3576	public function GetEditClass()
3577	{
3578		return "Password";
3579	}
3580
3581	protected function GetSQLCol($bFullSpec = false)
3582	{
3583		return "VARCHAR(64)"
3584			.CMDBSource::GetSqlStringColumnDefinition()
3585			.($bFullSpec ? $this->GetSQLColSpec() : '');
3586	}
3587
3588	public function GetMaxSize()
3589	{
3590		return 64;
3591	}
3592
3593	public function GetFilterDefinitions()
3594	{
3595		// Note: due to this, you will get an error if a password is being declared as a search criteria (see ZLists)
3596		// not allowed to search on passwords!
3597		return array();
3598	}
3599
3600	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
3601	{
3602		if (strlen($sValue) == 0)
3603		{
3604			return '';
3605		}
3606		else
3607		{
3608			return '******';
3609		}
3610	}
3611
3612	public function IsPartOfFingerprint()
3613	{
3614		return false;
3615	} // Cannot reliably compare two encrypted passwords since the same password will be encrypted in diffferent manners depending on the random 'salt'
3616}
3617
3618/**
3619 * Map a text column (size < 255) to an attribute that is encrypted in the database
3620 * The encryption is based on a key set per iTop instance. Thus if you export your
3621 * database (in SQL) to someone else without providing the key at the same time
3622 * the encrypted fields will remain encrypted
3623 *
3624 * @package     iTopORM
3625 */
3626class AttributeEncryptedString extends AttributeString
3627{
3628	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
3629
3630	static $sKey = null; // Encryption key used for all encrypted fields
3631	static $sLibrary = null; // Encryption library used for all encrypted fields
3632
3633	public function __construct($sCode, $aParams)
3634	{
3635		parent::__construct($sCode, $aParams);
3636		if (self::$sKey == null)
3637		{
3638			self::$sKey = MetaModel::GetConfig()->GetEncryptionKey();
3639		}
3640		if (self::$sLibrary == null)
3641		{
3642			self::$sLibrary = MetaModel::GetConfig()->GetEncryptionLibrary();
3643		}
3644	}
3645
3646	/**
3647	 * When the attribute definitions are stored in APC cache:
3648	 * 1) The static class variable $sKey is NOT serialized
3649	 * 2) The object's constructor is NOT called upon wakeup
3650	 * 3) mcrypt may crash the server if passed an empty key !!
3651	 *
3652	 * So let's restore the key (if needed) when waking up
3653	 **/
3654	public function __wakeup()
3655	{
3656		if (self::$sKey == null)
3657		{
3658			self::$sKey = MetaModel::GetConfig()->GetEncryptionKey();
3659		}
3660		if (self::$sLibrary == null)
3661		{
3662			self::$sLibrary = MetaModel::GetConfig()->GetEncryptionLibrary();
3663		}
3664	}
3665
3666
3667	protected function GetSQLCol($bFullSpec = false)
3668	{
3669		return "TINYBLOB";
3670	}
3671
3672	public function GetMaxSize()
3673	{
3674		return 255;
3675	}
3676
3677	public function GetFilterDefinitions()
3678	{
3679		// Note: due to this, you will get an error if a an encrypted field is declared as a search criteria (see ZLists)
3680		// not allowed to search on encrypted fields !
3681		return array();
3682	}
3683
3684	public function MakeRealValue($proposedValue, $oHostObj)
3685	{
3686		if (is_null($proposedValue))
3687		{
3688			return null;
3689		}
3690
3691		return (string)$proposedValue;
3692	}
3693
3694	/**
3695	 * Decrypt the value when reading from the database
3696	 *
3697	 * @param array $aCols
3698	 * @param string $sPrefix
3699	 *
3700	 * @return string
3701	 * @throws \Exception
3702	 */
3703	public function FromSQLToValue($aCols, $sPrefix = '')
3704	{
3705		$oSimpleCrypt = new SimpleCrypt(self::$sLibrary);
3706		$sValue = $oSimpleCrypt->Decrypt(self::$sKey, $aCols[$sPrefix]);
3707
3708		return $sValue;
3709	}
3710
3711	/**
3712	 * Encrypt the value before storing it in the database
3713	 *
3714	 * @param $value
3715	 *
3716	 * @return array
3717	 * @throws \Exception
3718	 */
3719	public function GetSQLValues($value)
3720	{
3721		$oSimpleCrypt = new SimpleCrypt(self::$sLibrary);
3722		$encryptedValue = $oSimpleCrypt->Encrypt(self::$sKey, $value);
3723
3724		$aValues = array();
3725		$aValues[$this->Get("sql")] = $encryptedValue;
3726
3727		return $aValues;
3728	}
3729}
3730
3731
3732/**
3733 * Wiki formatting - experimental
3734 *
3735 * [[<objClass>:<objName|objId>|<label>]]
3736 * <label> is optional
3737 *
3738 * Examples:
3739 * - [[Server:db1.tnut.com]]
3740 * - [[Server:123]]
3741 * - [[Server:db1.tnut.com|Production server]]
3742 * - [[Server:123|Production server]]
3743 */
3744define('WIKI_OBJECT_REGEXP', '/\[\[(.+):(.+)(\|(.+))?\]\]/U');
3745
3746
3747/**
3748 * Map a text column (size > ?) to an attribute
3749 *
3750 * @package     iTopORM
3751 */
3752class AttributeText extends AttributeString
3753{
3754	public function GetEditClass()
3755	{
3756		return ($this->GetFormat() == 'text') ? 'Text' : "HTML";
3757	}
3758
3759	protected function GetSQLCol($bFullSpec = false)
3760	{
3761		return "TEXT".CMDBSource::GetSqlStringColumnDefinition();
3762	}
3763
3764	public function GetSQLColumns($bFullSpec = false)
3765	{
3766		$aColumns = array();
3767		$aColumns[$this->Get('sql')] = $this->GetSQLCol($bFullSpec);
3768		if ($this->GetOptional('format', null) != null)
3769		{
3770			// Add the extra column only if the property 'format' is specified for the attribute
3771			$aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')".CMDBSource::GetSqlStringColumnDefinition();
3772			if ($bFullSpec)
3773			{
3774				$aColumns[$this->Get('sql').'_format'] .= " DEFAULT 'text'"; // default 'text' is for migrating old records
3775			}
3776		}
3777
3778		return $aColumns;
3779	}
3780
3781	public function GetSQLExpressions($sPrefix = '')
3782	{
3783		if ($sPrefix == '')
3784		{
3785			$sPrefix = $this->Get('sql');
3786		}
3787		$aColumns = array();
3788		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
3789		$aColumns[''] = $sPrefix;
3790		if ($this->GetOptional('format', null) != null)
3791		{
3792			// Add the extra column only if the property 'format' is specified for the attribute
3793			$aColumns['_format'] = $sPrefix.'_format';
3794		}
3795
3796		return $aColumns;
3797	}
3798
3799	public function GetMaxSize()
3800	{
3801		// Is there a way to know the current limitation for mysql?
3802		// See mysql_field_len()
3803		return 65535;
3804	}
3805
3806	static public function RenderWikiHtml($sText, $bWikiOnly = false)
3807	{
3808		if (!$bWikiOnly)
3809		{
3810			$sPattern = '/'.str_replace('/', '\/', utils::GetConfig()->Get('url_validation_pattern')).'/i';
3811			if (preg_match_all($sPattern, $sText, $aAllMatches,
3812				PREG_SET_ORDER /* important !*/ | PREG_OFFSET_CAPTURE /* important ! */))
3813			{
3814				$i = count($aAllMatches);
3815				// Replace the URLs by an actual hyperlink <a href="...">...</a>
3816				// Let's do it backwards so that the initial positions are not modified by the replacement
3817				// This works if the matches are captured: in the order they occur in the string  AND
3818				// with their offset (i.e. position) inside the string
3819				while ($i > 0)
3820				{
3821					$i--;
3822					$sUrl = $aAllMatches[$i][0][0]; // String corresponding to the main pattern
3823					$iPos = $aAllMatches[$i][0][1]; // Position of the main pattern
3824					$sText = substr_replace($sText, "<a href=\"$sUrl\">$sUrl</a>", $iPos, strlen($sUrl));
3825
3826				}
3827			}
3828		}
3829		if (preg_match_all(WIKI_OBJECT_REGEXP, $sText, $aAllMatches, PREG_SET_ORDER))
3830		{
3831			foreach($aAllMatches as $iPos => $aMatches)
3832			{
3833				$sClass = trim($aMatches[1]);
3834				$sName = trim($aMatches[2]);
3835				$sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null;
3836
3837				if (MetaModel::IsValidClass($sClass))
3838				{
3839				    $bFound = false;
3840
3841				    // Try to find by name, then by id
3842					if (is_object($oObj = MetaModel::GetObjectByName($sClass, $sName, false /* MustBeFound */)))
3843                    {
3844                        $bFound = true;
3845                    }
3846                    elseif(is_object($oObj = MetaModel::GetObject($sClass, (int) $sName, false /* MustBeFound */, true)))
3847                    {
3848                        $bFound = true;
3849                    }
3850
3851                    if($bFound === true)
3852                    {
3853						// Propose a std link to the object
3854                        $sHyperlinkLabel = (empty($sLabel)) ? $oObj->GetName() : $sLabel;
3855                        $sText = str_replace($aMatches[0], $oObj->GetHyperlink(null, true, $sHyperlinkLabel), $sText);
3856					}
3857					else
3858					{
3859						// Propose a std link to the object
3860						$sClassLabel = MetaModel::GetName($sClass);
3861						$sReplacement = "<span class=\"wiki_broken_link\">$sClassLabel:$sName" . (!empty($sLabel) ? " ($sLabel)" : "") . "</span>";
3862						$sText = str_replace($aMatches[0], $sReplacement, $sText);
3863						// Later: propose a link to create a new object
3864						// Anyhow... there is no easy way to suggest default values based on the given FRIENDLY name
3865						//$sText = preg_replace('/\[\[(.+):(.+)\]\]/', '<a href="'.utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=new&class='.$sClass.'&default[att1]=xxx&default[att2]=yyy">'.$sName.'</a>', $sText);
3866					}
3867				}
3868			}
3869		}
3870
3871		return $sText;
3872	}
3873
3874	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
3875	{
3876		$aStyles = array();
3877		if ($this->GetWidth() != '')
3878		{
3879			$aStyles[] = 'width:'.$this->GetWidth();
3880		}
3881		if ($this->GetHeight() != '')
3882		{
3883			$aStyles[] = 'height:'.$this->GetHeight();
3884		}
3885		$sStyle = '';
3886		if (count($aStyles) > 0)
3887		{
3888			$aStyles[] = 'overflow:auto';
3889			$sStyle = 'style="'.implode(';', $aStyles).'"';
3890		}
3891
3892		if ($this->GetFormat() == 'text')
3893		{
3894			$sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize);
3895			$sValue = self::RenderWikiHtml($sValue);
3896
3897			return "<div $sStyle>".str_replace("\n", "<br>\n", $sValue).'</div>';
3898		}
3899		else
3900		{
3901			$sValue = self::RenderWikiHtml($sValue, true /* wiki only */);
3902
3903			return "<div class=\"HTML\" $sStyle>".InlineImage::FixUrls($sValue).'</div>';
3904		}
3905
3906	}
3907
3908	public function GetEditValue($sValue, $oHostObj = null)
3909	{
3910		if ($this->GetFormat() == 'text')
3911		{
3912			if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER))
3913			{
3914				foreach($aAllMatches as $iPos => $aMatches)
3915				{
3916					$sClass = trim($aMatches[1]);
3917					$sName = trim($aMatches[2]);
3918					$sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null;
3919
3920					if (MetaModel::IsValidClass($sClass))
3921					{
3922						$sClassLabel = MetaModel::GetName($sClass);
3923						$sReplacement = "[[$sClassLabel:$sName" . (!empty($sLabel) ? " | $sLabel" : "") . "]]";
3924						$sValue = str_replace($aMatches[0], $sReplacement, $sValue);
3925					}
3926				}
3927			}
3928		}
3929		else
3930		{
3931			$sValue = str_replace('&', '&amp;', $sValue);
3932		}
3933
3934		return $sValue;
3935	}
3936
3937	/**
3938	 * For fields containing a potential markup, return the value without this markup
3939	 *
3940	 * @param string $sValue
3941	 * @param \DBObject $oHostObj
3942	 *
3943	 * @return string
3944	 */
3945	public function GetAsPlainText($sValue, $oHostObj = null)
3946	{
3947		if ($this->GetFormat() == 'html')
3948		{
3949			return (string)utils::HtmlToText($this->GetEditValue($sValue, $oHostObj));
3950		}
3951		else
3952		{
3953			return parent::GetAsPlainText($sValue, $oHostObj);
3954		}
3955	}
3956
3957	public function MakeRealValue($proposedValue, $oHostObj)
3958	{
3959		$sValue = $proposedValue;
3960		switch ($this->GetFormat())
3961		{
3962			case 'html':
3963				if (($sValue !== null) && ($sValue !== ''))
3964				{
3965					$sValue = HTMLSanitizer::Sanitize($sValue);
3966				}
3967				break;
3968
3969			case 'text':
3970			default:
3971				if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER))
3972				{
3973					foreach($aAllMatches as $iPos => $aMatches)
3974					{
3975						$sClassLabel = trim($aMatches[1]);
3976						$sName = trim($aMatches[2]);
3977                        $sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null;
3978
3979						if (!MetaModel::IsValidClass($sClassLabel))
3980						{
3981							$sClass = MetaModel::GetClassFromLabel($sClassLabel);
3982							if ($sClass)
3983							{
3984                                $sReplacement = "[[$sClassLabel:$sName" . (!empty($sLabel) ? " | $sLabel" : "") . "]]";
3985								$sValue = str_replace($aMatches[0], $sReplacement, $sValue);
3986							}
3987						}
3988					}
3989				}
3990		}
3991
3992		return $sValue;
3993	}
3994
3995	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
3996	{
3997		return Str::pure2xml($value);
3998	}
3999
4000	public function GetWidth()
4001	{
4002		return $this->GetOptional('width', '');
4003	}
4004
4005	public function GetHeight()
4006	{
4007		return $this->GetOptional('height', '');
4008	}
4009
4010	static public function GetFormFieldClass()
4011	{
4012		return '\\Combodo\\iTop\\Form\\Field\\TextAreaField';
4013	}
4014
4015	/**
4016	 * @param \DBObject $oObject
4017	 * @param \Combodo\iTop\Form\Field\TextAreaField $oFormField
4018	 *
4019	 * @return \Combodo\iTop\Form\Field\TextAreaField
4020	 * @throws \CoreException
4021	 */
4022	public function MakeFormField(DBObject $oObject, $oFormField = null)
4023	{
4024		if ($oFormField === null)
4025		{
4026			$sFormFieldClass = static::GetFormFieldClass();
4027			/** @var \Combodo\iTop\Form\Field\TextAreaField $oFormField */
4028			$oFormField = new $sFormFieldClass($this->GetCode(), null, $oObject);
4029			$oFormField->SetFormat($this->GetFormat());
4030		}
4031		parent::MakeFormField($oObject, $oFormField);
4032
4033		return $oFormField;
4034	}
4035
4036	/**
4037	 * The actual formatting of the field: either text (=plain text) or html (= text with HTML markup)
4038	 *
4039	 * @return string
4040	 */
4041	public function GetFormat()
4042	{
4043		return $this->GetOptional('format', 'text');
4044	}
4045
4046	/**
4047	 * Read the value from the row returned by the SQL query and transorms it to the appropriate
4048	 * internal format (either text or html)
4049	 *
4050	 * @see AttributeDBFieldVoid::FromSQLToValue()
4051	 *
4052	 * @param array $aCols
4053	 * @param string $sPrefix
4054	 *
4055	 * @return string
4056	 */
4057	public function FromSQLToValue($aCols, $sPrefix = '')
4058	{
4059		$value = $aCols[$sPrefix.''];
4060		if ($this->GetOptional('format', null) != null)
4061		{
4062			// Read from the extra column only if the property 'format' is specified for the attribute
4063			$sFormat = $aCols[$sPrefix.'_format'];
4064		}
4065		else
4066		{
4067			$sFormat = $this->GetFormat();
4068		}
4069
4070		switch ($sFormat)
4071		{
4072			case 'text':
4073				if ($this->GetFormat() == 'html')
4074				{
4075					$value = utils::TextToHtml($value);
4076				}
4077				break;
4078
4079			case 'html':
4080				if ($this->GetFormat() == 'text')
4081				{
4082					$value = utils::HtmlToText($value);
4083				}
4084				else
4085				{
4086					$value = InlineImage::FixUrls((string)$value);
4087				}
4088				break;
4089
4090			default:
4091				// unknown format ??
4092		}
4093
4094		return $value;
4095	}
4096
4097	public function GetSQLValues($value)
4098	{
4099		$aValues = array();
4100		$aValues[$this->Get("sql")] = $this->ScalarToSQL($value);
4101		if ($this->GetOptional('format', null) != null)
4102		{
4103			// Add the extra column only if the property 'format' is specified for the attribute
4104			$aValues[$this->Get("sql").'_format'] = $this->GetFormat();
4105		}
4106
4107		return $aValues;
4108	}
4109
4110	public function GetAsCSV(
4111		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
4112		$bConvertToPlainText = false
4113	) {
4114		switch ($this->GetFormat())
4115		{
4116			case 'html':
4117				if ($bConvertToPlainText)
4118				{
4119					$sValue = utils::HtmlToText((string)$sValue);
4120				}
4121				$sFrom = array("\r\n", $sTextQualifier);
4122				$sTo = array("\n", $sTextQualifier.$sTextQualifier);
4123				$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
4124
4125				return $sTextQualifier.$sEscaped.$sTextQualifier;
4126				break;
4127
4128			case 'text':
4129			default:
4130				return parent::GetAsCSV($sValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize,
4131					$bConvertToPlainText);
4132		}
4133	}
4134}
4135
4136/**
4137 * Map a log to an attribute
4138 *
4139 * @package     iTopORM
4140 */
4141class AttributeLongText extends AttributeText
4142{
4143	protected function GetSQLCol($bFullSpec = false)
4144	{
4145		return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition();
4146	}
4147
4148	public function GetMaxSize()
4149	{
4150		// Is there a way to know the current limitation for mysql?
4151		// See mysql_field_len()
4152		return 65535 * 1024; // Limited... still 64 Mb!
4153	}
4154}
4155
4156/**
4157 * An attibute that stores a case log (i.e journal)
4158 *
4159 * @package     iTopORM
4160 */
4161class AttributeCaseLog extends AttributeLongText
4162{
4163	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
4164
4165	public function GetNullValue()
4166	{
4167		return '';
4168	}
4169
4170	public function IsNull($proposedValue)
4171	{
4172		if (!($proposedValue instanceof ormCaseLog))
4173		{
4174			return ($proposedValue == '');
4175		}
4176
4177		return ($proposedValue->GetText() == '');
4178	}
4179
4180	public function ScalarToSQL($value)
4181	{
4182		if (!is_string($value) && !is_null($value))
4183		{
4184			throw new CoreWarning('Expected the attribute value to be a string', array(
4185				'found_type' => gettype($value),
4186				'value' => $value,
4187				'class' => $this->GetCode(),
4188				'attribute' => $this->GetHostClass()
4189			));
4190		}
4191
4192		return $value;
4193	}
4194
4195	public function GetEditClass()
4196	{
4197		return "CaseLog";
4198	}
4199
4200	public function GetEditValue($sValue, $oHostObj = null)
4201	{
4202		if (!($sValue instanceOf ormCaseLog))
4203		{
4204			return '';
4205		}
4206
4207		return $sValue->GetModifiedEntry();
4208	}
4209
4210	/**
4211	 * For fields containing a potential markup, return the value without this markup
4212	 *
4213	 * @param mixed $value
4214	 * @param \DBObject $oHostObj
4215	 *
4216	 * @return string
4217	 */
4218	public function GetAsPlainText($value, $oHostObj = null)
4219	{
4220		if ($value instanceOf ormCaseLog)
4221		{
4222			/** ormCaseLog $value */
4223			return $value->GetAsPlainText();
4224		}
4225		else
4226		{
4227			return (string)$value;
4228		}
4229	}
4230
4231	public function GetDefaultValue(DBObject $oHostObject = null)
4232	{
4233		return new ormCaseLog();
4234	}
4235
4236	public function Equals($val1, $val2)
4237	{
4238		return ($val1->GetText() == $val2->GetText());
4239	}
4240
4241
4242	/**
4243	 * Facilitate things: allow the user to Set the value from a string
4244	 *
4245	 * @param $proposedValue
4246	 * @param \DBObject $oHostObj
4247	 *
4248	 * @return mixed|null|\ormCaseLog|string
4249	 * @throws \Exception
4250	 */
4251	public function MakeRealValue($proposedValue, $oHostObj)
4252	{
4253		if ($proposedValue instanceof ormCaseLog)
4254		{
4255			// Passthrough
4256			$ret = clone $proposedValue;
4257		}
4258		else
4259		{
4260			// Append the new value if an instance of the object is supplied
4261			//
4262			$oPreviousLog = null;
4263			if ($oHostObj != null)
4264			{
4265				$oPreviousLog = $oHostObj->Get($this->GetCode());
4266				if (!is_object($oPreviousLog))
4267				{
4268					$oPreviousLog = $oHostObj->GetOriginal($this->GetCode());;
4269				}
4270
4271			}
4272			if (is_object($oPreviousLog))
4273			{
4274				$oCaseLog = clone($oPreviousLog);
4275			}
4276			else
4277			{
4278				$oCaseLog = new ormCaseLog();
4279			}
4280
4281			if ($proposedValue instanceof stdClass)
4282			{
4283				$oCaseLog->AddLogEntryFromJSON($proposedValue);
4284			}
4285			else
4286			{
4287				if (strlen($proposedValue) > 0)
4288				{
4289					$oCaseLog->AddLogEntry($proposedValue);
4290				}
4291			}
4292			$ret = $oCaseLog;
4293		}
4294
4295		return $ret;
4296	}
4297
4298	public function GetSQLExpressions($sPrefix = '')
4299	{
4300		if ($sPrefix == '')
4301		{
4302			$sPrefix = $this->Get('sql');
4303		}
4304		$aColumns = array();
4305		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
4306		$aColumns[''] = $sPrefix;
4307		$aColumns['_index'] = $sPrefix.'_index';
4308
4309		return $aColumns;
4310	}
4311
4312	/**
4313	 * @param array $aCols
4314	 * @param string $sPrefix
4315	 *
4316	 * @return \ormCaseLog
4317	 * @throws \MissingColumnException
4318	 */
4319	public function FromSQLToValue($aCols, $sPrefix = '')
4320	{
4321		if (!array_key_exists($sPrefix, $aCols))
4322		{
4323			$sAvailable = implode(', ', array_keys($aCols));
4324			throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
4325		}
4326		$sLog = $aCols[$sPrefix];
4327
4328		if (isset($aCols[$sPrefix.'_index']))
4329		{
4330			$sIndex = $aCols[$sPrefix.'_index'];
4331		}
4332		else
4333		{
4334			// For backward compatibility, allow the current state to be: 1 log, no index
4335			$sIndex = '';
4336		}
4337
4338		if (strlen($sIndex) > 0)
4339		{
4340			$aIndex = unserialize($sIndex);
4341			$value = new ormCaseLog($sLog, $aIndex);
4342		}
4343		else
4344		{
4345			$value = new ormCaseLog($sLog);
4346		}
4347
4348		return $value;
4349	}
4350
4351	public function GetSQLValues($value)
4352	{
4353		if (!($value instanceOf ormCaseLog))
4354		{
4355			$value = new ormCaseLog('');
4356		}
4357		$aValues = array();
4358		$aValues[$this->GetCode()] = $value->GetText();
4359		$aValues[$this->GetCode().'_index'] = serialize($value->GetIndex());
4360
4361		return $aValues;
4362	}
4363
4364	public function GetSQLColumns($bFullSpec = false)
4365	{
4366		$aColumns = array();
4367		$aColumns[$this->GetCode()] = 'LONGTEXT' // 2^32 (4 Gb)
4368			.CMDBSource::GetSqlStringColumnDefinition();
4369		$aColumns[$this->GetCode().'_index'] = 'BLOB';
4370
4371		return $aColumns;
4372	}
4373
4374	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
4375	{
4376		if ($value instanceOf ormCaseLog)
4377		{
4378			$sContent = $value->GetAsHTML(null, false, array(__class__, 'RenderWikiHtml'));
4379		}
4380		else
4381		{
4382			$sContent = '';
4383		}
4384		$aStyles = array();
4385		if ($this->GetWidth() != '')
4386		{
4387			$aStyles[] = 'width:'.$this->GetWidth();
4388		}
4389		if ($this->GetHeight() != '')
4390		{
4391			$aStyles[] = 'height:'.$this->GetHeight();
4392		}
4393		$sStyle = '';
4394		if (count($aStyles) > 0)
4395		{
4396			$sStyle = 'style="'.implode(';', $aStyles).'"';
4397		}
4398
4399		return "<div class=\"caselog\" $sStyle>".$sContent.'</div>';
4400	}
4401
4402
4403	public function GetAsCSV(
4404		$value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
4405		$bConvertToPlainText = false
4406	) {
4407		if ($value instanceOf ormCaseLog)
4408		{
4409			return parent::GetAsCSV($value->GetText($bConvertToPlainText), $sSeparator, $sTextQualifier, $oHostObject,
4410				$bLocalize, $bConvertToPlainText);
4411		}
4412		else
4413		{
4414			return '';
4415		}
4416	}
4417
4418	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
4419	{
4420		if ($value instanceOf ormCaseLog)
4421		{
4422			return parent::GetAsXML($value->GetText(), $oHostObject, $bLocalize);
4423		}
4424		else
4425		{
4426			return '';
4427		}
4428	}
4429
4430	/**
4431	 * List the available verbs for 'GetForTemplate'
4432	 */
4433	public function EnumTemplateVerbs()
4434	{
4435		return array(
4436			'' => 'Plain text representation of all the log entries',
4437			'head' => 'Plain text representation of the latest entry',
4438			'head_html' => 'HTML representation of the latest entry',
4439			'html' => 'HTML representation of all the log entries',
4440		);
4441	}
4442
4443	/**
4444	 * Get various representations of the value, for insertion into a template (e.g. in Notifications)
4445	 *
4446	 * @param $value mixed The current value of the field
4447	 * @param $sVerb string The verb specifying the representation of the value
4448	 * @param $oHostObject DBObject The object
4449	 * @param $bLocalize bool Whether or not to localize the value
4450	 *
4451	 * @return mixed
4452	 * @throws \Exception
4453	 */
4454	public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
4455	{
4456		switch ($sVerb)
4457		{
4458			case '':
4459				return $value->GetText(true);
4460
4461			case 'head':
4462				return $value->GetLatestEntry('text');
4463
4464			case 'head_html':
4465				return $value->GetLatestEntry('html');
4466
4467			case 'html':
4468				return $value->GetAsEmailHtml();
4469
4470			default:
4471				throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject));
4472		}
4473	}
4474
4475	/**
4476	 * Helper to get a value that will be JSON encoded
4477	 * The operation is the opposite to FromJSONToValue
4478	 */
4479	public function GetForJSON($value)
4480	{
4481		return $value->GetForJSON();
4482	}
4483
4484	/**
4485	 * Helper to form a value, given JSON decoded data
4486	 * The operation is the opposite to GetForJSON
4487	 */
4488	public function FromJSONToValue($json)
4489	{
4490		if (is_string($json))
4491		{
4492			// Will be correctly handled in MakeRealValue
4493			$ret = $json;
4494		}
4495		else
4496		{
4497			if (isset($json->add_item))
4498			{
4499				// Will be correctly handled in MakeRealValue
4500				$ret = $json->add_item;
4501				if (!isset($ret->message))
4502				{
4503					throw new Exception("Missing mandatory entry: 'message'");
4504				}
4505			}
4506			else
4507			{
4508				$ret = ormCaseLog::FromJSON($json);
4509			}
4510		}
4511
4512		return $ret;
4513	}
4514
4515	public function Fingerprint($value)
4516	{
4517		$sFingerprint = '';
4518		if ($value instanceOf ormCaseLog)
4519		{
4520			$sFingerprint = $value->GetText();
4521		}
4522
4523		return $sFingerprint;
4524	}
4525
4526	/**
4527	 * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup)
4528	 *
4529	 * @return string
4530	 */
4531	public function GetFormat()
4532	{
4533		return $this->GetOptional('format', 'html'); // default format for case logs is now HTML
4534	}
4535
4536	static public function GetFormFieldClass()
4537	{
4538		return '\\Combodo\\iTop\\Form\\Field\\CaseLogField';
4539	}
4540
4541	public function MakeFormField(DBObject $oObject, $oFormField = null)
4542	{
4543		// First we call the parent so the field is build
4544		$oFormField = parent::MakeFormField($oObject, $oFormField);
4545		// Then only we set the value
4546		$oFormField->SetCurrentValue($this->GetEditValue($oObject->Get($this->GetCode())));
4547		// And we set the entries
4548		$oFormField->SetEntries($oObject->Get($this->GetCode())->GetAsArray());
4549
4550		return $oFormField;
4551	}
4552}
4553
4554/**
4555 * Map a text column (size > ?), containing HTML code, to an attribute
4556 *
4557 * @package     iTopORM
4558 */
4559class AttributeHTML extends AttributeLongText
4560{
4561	public function GetSQLColumns($bFullSpec = false)
4562	{
4563		$aColumns = array();
4564		$aColumns[$this->Get('sql')] = $this->GetSQLCol();
4565		if ($this->GetOptional('format', null) != null)
4566		{
4567			// Add the extra column only if the property 'format' is specified for the attribute
4568			$aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')";
4569			if ($bFullSpec)
4570			{
4571				$aColumns[$this->Get('sql').'_format'] .= " DEFAULT 'html'"; // default 'html' is for migrating old records
4572			}
4573		}
4574
4575		return $aColumns;
4576	}
4577
4578	/**
4579	 * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup)
4580	 *
4581	 * @return string
4582	 */
4583	public function GetFormat()
4584	{
4585		return $this->GetOptional('format', 'html'); // Defaults to HTML
4586	}
4587}
4588
4589/**
4590 * Specialization of a string: email
4591 *
4592 * @package     iTopORM
4593 */
4594class AttributeEmailAddress extends AttributeString
4595{
4596	public function GetValidationPattern()
4597	{
4598		return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('email_validation_pattern').'$');
4599	}
4600
4601	static public function GetFormFieldClass()
4602	{
4603		return '\\Combodo\\iTop\\Form\\Field\\EmailField';
4604	}
4605
4606	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
4607	{
4608		if (empty($sValue))
4609		{
4610			return '';
4611		}
4612
4613		$sUrlDecorationClass = utils::GetConfig()->Get('email_decoration_class');
4614
4615		return '<a class="mailto" href="mailto:'.$sValue.'"><span class="text_decoration '.$sUrlDecorationClass.'"></span>'.parent::GetAsHTML($sValue).'</a>';
4616	}
4617}
4618
4619/**
4620 * Specialization of a string: IP address
4621 *
4622 * @package     iTopORM
4623 */
4624class AttributeIPAddress extends AttributeString
4625{
4626	public function GetValidationPattern()
4627	{
4628		$sNum = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])';
4629
4630		return "^($sNum\\.$sNum\\.$sNum\\.$sNum)$";
4631	}
4632
4633	public function GetOrderBySQLExpressions($sClassAlias)
4634	{
4635		// Note: This is the responsibility of this function to place backticks around column aliases
4636		return array('INET_ATON(`'.$sClassAlias.$this->GetCode().'`)');
4637	}
4638}
4639
4640/**
4641 * Specialization of a string: phone number
4642 *
4643 * @package     iTopORM
4644 */
4645class AttributePhoneNumber extends AttributeString
4646{
4647	public function GetValidationPattern()
4648	{
4649		return $this->GetOptional('validation_pattern',
4650			'^'.utils::GetConfig()->Get('phone_number_validation_pattern').'$');
4651	}
4652
4653	static public function GetFormFieldClass()
4654	{
4655		return '\\Combodo\\iTop\\Form\\Field\\PhoneField';
4656	}
4657
4658	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
4659	{
4660		if (empty($sValue))
4661		{
4662			return '';
4663		}
4664
4665		$sUrlDecorationClass = utils::GetConfig()->Get('phone_number_decoration_class');
4666		$sUrlPattern = utils::GetConfig()->Get('phone_number_url_pattern');
4667		$sUrl = sprintf($sUrlPattern, $sValue);
4668
4669		return '<a class="tel" href="'.$sUrl.'"><span class="text_decoration '.$sUrlDecorationClass.'"></span>'.parent::GetAsHTML($sValue).'</a>';
4670	}
4671}
4672
4673/**
4674 * Specialization of a string: OQL expression
4675 *
4676 * @package     iTopORM
4677 */
4678class AttributeOQL extends AttributeText
4679{
4680	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
4681
4682	public function GetEditClass()
4683	{
4684		return "OQLExpression";
4685	}
4686}
4687
4688/**
4689 * Specialization of a string: template (contains iTop placeholders like $current_contact_id$ or $this->name$)
4690 *
4691 * @package     iTopORM
4692 */
4693class AttributeTemplateString extends AttributeString
4694{
4695	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
4696}
4697
4698/**
4699 * Specialization of a text: template (contains iTop placeholders like $current_contact_id$ or $this->name$)
4700 *
4701 * @package     iTopORM
4702 */
4703class AttributeTemplateText extends AttributeText
4704{
4705	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
4706}
4707
4708/**
4709 * Specialization of a HTML: template (contains iTop placeholders like $current_contact_id$ or $this->name$)
4710 *
4711 * @package     iTopORM
4712 */
4713class AttributeTemplateHTML extends AttributeText
4714{
4715	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
4716
4717	public function GetSQLColumns($bFullSpec = false)
4718	{
4719		$aColumns = array();
4720		$aColumns[$this->Get('sql')] = $this->GetSQLCol();
4721		if ($this->GetOptional('format', null) != null)
4722		{
4723			// Add the extra column only if the property 'format' is specified for the attribute
4724			$aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')";
4725			if ($bFullSpec)
4726			{
4727				$aColumns[$this->Get('sql').'_format'] .= " DEFAULT 'html'"; // default 'html' is for migrating old records
4728			}
4729		}
4730
4731		return $aColumns;
4732	}
4733
4734	/**
4735	 * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup)
4736	 *
4737	 * @return string
4738	 */
4739	public function GetFormat()
4740	{
4741		return $this->GetOptional('format', 'html'); // Defaults to HTML
4742	}
4743}
4744
4745
4746/**
4747 * Map a enum column to an attribute
4748 *
4749 * @package     iTopORM
4750 */
4751class AttributeEnum extends AttributeString
4752{
4753	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM;
4754
4755	static public function ListExpectedParams()
4756	{
4757		return parent::ListExpectedParams();
4758		//return array_merge(parent::ListExpectedParams(), array());
4759	}
4760
4761	public function GetEditClass()
4762	{
4763		return "String";
4764	}
4765
4766	protected function GetSQLCol($bFullSpec = false)
4767	{
4768		$oValDef = $this->GetValuesDef();
4769		if ($oValDef)
4770		{
4771			$aValues = CMDBSource::Quote(array_keys($oValDef->GetValues(array(), "")), true);
4772		}
4773		else
4774		{
4775			$aValues = array();
4776		}
4777		if (count($aValues) > 0)
4778		{
4779			// The syntax used here do matters
4780			// In particular, I had to remove unnecessary spaces to
4781			// make sure that this string will match the field type returned by the DB
4782			// (used to perform a comparison between the current DB format and the data model)
4783			return "ENUM(".implode(",", $aValues).")"
4784				.CMDBSource::GetSqlStringColumnDefinition()
4785				.($bFullSpec ? $this->GetSQLColSpec() : '');
4786		}
4787		else
4788		{
4789			return "VARCHAR(255)"
4790				.CMDBSource::GetSqlStringColumnDefinition()
4791				.($bFullSpec ? " DEFAULT ''" : ""); // ENUM() is not an allowed syntax!
4792		}
4793	}
4794
4795	protected function GetSQLColSpec()
4796	{
4797		$default = $this->ScalarToSQL($this->GetDefaultValue());
4798		if (is_null($default))
4799		{
4800			$sRet = '';
4801		}
4802		else
4803		{
4804			// ENUMs values are strings so the default value must be a string as well,
4805			// otherwise MySQL interprets the number as the zero-based index of the value in the list (i.e. the nth value in the list)
4806			$sRet = " DEFAULT ".CMDBSource::Quote($default);
4807		}
4808
4809		return $sRet;
4810	}
4811
4812	public function ScalarToSQL($value)
4813	{
4814		// Note: for strings, the null value is an empty string and it is recorded as such in the DB
4815		//	   but that wasn't working for enums, because '' is NOT one of the allowed values
4816		//	   that's why a null value must be forced to a real null
4817		$value = parent::ScalarToSQL($value);
4818		if ($this->IsNull($value))
4819		{
4820			return null;
4821		}
4822		else
4823		{
4824			return $value;
4825		}
4826	}
4827
4828	public function RequiresIndex()
4829	{
4830		return false;
4831	}
4832
4833	public function GetBasicFilterOperators()
4834	{
4835		return parent::GetBasicFilterOperators();
4836	}
4837
4838	public function GetBasicFilterLooseOperator()
4839	{
4840		return '=';
4841	}
4842
4843	public function GetBasicFilterSQLExpr($sOpCode, $value)
4844	{
4845		return parent::GetBasicFilterSQLExpr($sOpCode, $value);
4846	}
4847
4848	public function GetValueLabel($sValue)
4849	{
4850		if (is_null($sValue))
4851		{
4852			// Unless a specific label is defined for the null value of this enum, use a generic "undefined" label
4853			$sLabel = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue,
4854				Dict::S('Enum:Undefined'));
4855		}
4856		else
4857		{
4858			$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, null, true /*user lang*/);
4859			if (is_null($sLabel))
4860			{
4861				$sDefault = str_replace('_', ' ', $sValue);
4862				// Browse the hierarchy again, accepting default (english) translations
4863				$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, false);
4864			}
4865		}
4866
4867		return $sLabel;
4868	}
4869
4870	public function GetValueDescription($sValue)
4871	{
4872		if (is_null($sValue))
4873		{
4874			// Unless a specific label is defined for the null value of this enum, use a generic "undefined" label
4875			$sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+',
4876				Dict::S('Enum:Undefined'));
4877		}
4878		else
4879		{
4880			$sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+',
4881				'', true /* user language only */);
4882			if (strlen($sDescription) == 0)
4883			{
4884				$sParentClass = MetaModel::GetParentClass($this->m_sHostClass);
4885				if ($sParentClass)
4886				{
4887					if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode))
4888					{
4889						$oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode);
4890						$sDescription = $oAttDef->GetValueDescription($sValue);
4891					}
4892				}
4893			}
4894		}
4895
4896		return $sDescription;
4897	}
4898
4899	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
4900	{
4901		if ($bLocalize)
4902		{
4903			$sLabel = $this->GetValueLabel($sValue);
4904			$sDescription = $this->GetValueDescription($sValue);
4905			// later, we could imagine a detailed description in the title
4906			$sRes = "<span title=\"$sDescription\">".parent::GetAsHtml($sLabel)."</span>";
4907		}
4908		else
4909		{
4910			$sRes = parent::GetAsHtml($sValue, $oHostObject, $bLocalize);
4911		}
4912
4913		return $sRes;
4914	}
4915
4916	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
4917	{
4918		if (is_null($value))
4919		{
4920			$sFinalValue = '';
4921		}
4922		elseif ($bLocalize)
4923		{
4924			$sFinalValue = $this->GetValueLabel($value);
4925		}
4926		else
4927		{
4928			$sFinalValue = $value;
4929		}
4930		$sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize);
4931
4932		return $sRes;
4933	}
4934
4935	public function GetAsCSV(
4936		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
4937		$bConvertToPlainText = false
4938	) {
4939		if (is_null($sValue))
4940		{
4941			$sFinalValue = '';
4942		}
4943		elseif ($bLocalize)
4944		{
4945			$sFinalValue = $this->GetValueLabel($sValue);
4946		}
4947		else
4948		{
4949			$sFinalValue = $sValue;
4950		}
4951		$sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize);
4952
4953		return $sRes;
4954	}
4955
4956	static public function GetFormFieldClass()
4957	{
4958		return '\\Combodo\\iTop\\Form\\Field\\SelectField';
4959	}
4960
4961	public function MakeFormField(DBObject $oObject, $oFormField = null)
4962	{
4963		if ($oFormField === null)
4964		{
4965			// Later : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value
4966			$sFormFieldClass = static::GetFormFieldClass();
4967			$oFormField = new $sFormFieldClass($this->GetCode());
4968		}
4969
4970		$oFormField->SetChoices($this->GetAllowedValues($oObject->ToArgsForQuery()));
4971		parent::MakeFormField($oObject, $oFormField);
4972
4973		return $oFormField;
4974	}
4975
4976	public function GetEditValue($sValue, $oHostObj = null)
4977	{
4978		if (is_null($sValue))
4979		{
4980			return '';
4981		}
4982		else
4983		{
4984			return $this->GetValueLabel($sValue);
4985		}
4986	}
4987
4988	/**
4989	 * Helper to get a value that will be JSON encoded
4990	 * The operation is the opposite to FromJSONToValue
4991	 */
4992	public function GetForJSON($value)
4993	{
4994		return $value;
4995	}
4996
4997	public function GetAllowedValues($aArgs = array(), $sContains = '')
4998	{
4999		$aRawValues = parent::GetAllowedValues($aArgs, $sContains);
5000		if (is_null($aRawValues))
5001		{
5002			return null;
5003		}
5004		$aLocalizedValues = array();
5005		foreach($aRawValues as $sKey => $sValue)
5006		{
5007			$aLocalizedValues[$sKey] = $this->GetValueLabel($sKey);
5008		}
5009
5010		return $aLocalizedValues;
5011	}
5012
5013	public function GetMaxSize()
5014	{
5015		return null;
5016	}
5017
5018	/**
5019	 * An enum can be localized
5020	 */
5021	public function MakeValueFromString(
5022		$sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
5023		$sAttributeQualifier = null
5024	) {
5025		if ($bLocalizedValue)
5026		{
5027			// Lookup for the value matching the input
5028			//
5029			$sFoundValue = null;
5030			$aRawValues = parent::GetAllowedValues();
5031			if (!is_null($aRawValues))
5032			{
5033				foreach($aRawValues as $sKey => $sValue)
5034				{
5035					$sRefValue = $this->GetValueLabel($sKey);
5036					if ($sProposedValue == $sRefValue)
5037					{
5038						$sFoundValue = $sKey;
5039						break;
5040					}
5041				}
5042			}
5043			if (is_null($sFoundValue))
5044			{
5045				return null;
5046			}
5047
5048			return $this->MakeRealValue($sFoundValue, null);
5049		}
5050		else
5051		{
5052			return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue,
5053				$sAttributeQualifier);
5054		}
5055	}
5056
5057	/**
5058	 * Processes the input value to align it with the values supported
5059	 * by this type of attribute. In this case: turns empty strings into nulls
5060	 *
5061	 * @param mixed $proposedValue The value to be set for the attribute
5062	 *
5063	 * @return mixed The actual value that will be set
5064	 */
5065	public function MakeRealValue($proposedValue, $oHostObj)
5066	{
5067		if ($proposedValue == '')
5068		{
5069			return null;
5070		}
5071
5072		return parent::MakeRealValue($proposedValue, $oHostObj);
5073	}
5074
5075	public function GetOrderByHint()
5076	{
5077		$aValues = $this->GetAllowedValues();
5078
5079		return Dict::Format('UI:OrderByHint_Values', implode(', ', $aValues));
5080	}
5081}
5082
5083/**
5084 * A meta enum is an aggregation of enum from subclasses into an enum of a base class
5085 * It has been designed is to cope with the fact that statuses must be defined in leaf classes, while it makes sense to
5086 * have a superstatus available on the root classe(s)
5087 *
5088 * @package     iTopORM
5089 */
5090class AttributeMetaEnum extends AttributeEnum
5091{
5092	static public function ListExpectedParams()
5093	{
5094		return array('allowed_values', 'sql', 'default_value', 'mapping');
5095	}
5096
5097	public function IsNullAllowed()
5098	{
5099		return false; // Well... this actually depends on the mapping
5100	}
5101
5102	public function IsWritable()
5103	{
5104		return false;
5105	}
5106
5107	public function RequiresIndex()
5108	{
5109		return true;
5110	}
5111
5112	public function GetPrerequisiteAttributes($sClass = null)
5113	{
5114		if (is_null($sClass))
5115		{
5116			$sClass = $this->GetHostClass();
5117		}
5118		$aMappingData = $this->GetMapRule($sClass);
5119		if ($aMappingData == null)
5120		{
5121			$aRet = array();
5122		}
5123		else
5124		{
5125			$aRet = array($aMappingData['attcode']);
5126		}
5127
5128		return $aRet;
5129	}
5130
5131	/**
5132	 * Overload the standard so as to leave the data unsorted
5133	 *
5134	 * @param array $aArgs
5135	 * @param string $sContains
5136	 *
5137	 * @return array|null
5138	 */
5139	public function GetAllowedValues($aArgs = array(), $sContains = '')
5140	{
5141		$oValSetDef = $this->GetValuesDef();
5142		if (!$oValSetDef)
5143		{
5144			return null;
5145		}
5146		$aRawValues = $oValSetDef->GetValueList();
5147
5148		if (is_null($aRawValues))
5149		{
5150			return null;
5151		}
5152		$aLocalizedValues = array();
5153		foreach($aRawValues as $sKey => $sValue)
5154		{
5155			$aLocalizedValues[$sKey] = Str::pure2html($this->GetValueLabel($sKey));
5156		}
5157
5158		return $aLocalizedValues;
5159	}
5160
5161	/**
5162	 * Returns the meta value for the given object.
5163	 * See also MetaModel::RebuildMetaEnums() that must be maintained when MapValue changes
5164	 *
5165	 * @param $oObject
5166	 *
5167	 * @return mixed
5168	 * @throws Exception
5169	 */
5170	public function MapValue($oObject)
5171	{
5172		$aMappingData = $this->GetMapRule(get_class($oObject));
5173		if ($aMappingData == null)
5174		{
5175			$sRet = $this->GetDefaultValue();
5176		}
5177		else
5178		{
5179			$sAttCode = $aMappingData['attcode'];
5180			$value = $oObject->Get($sAttCode);
5181			if (array_key_exists($value, $aMappingData['values']))
5182			{
5183				$sRet = $aMappingData['values'][$value];
5184			}
5185			elseif ($this->GetDefaultValue() != '')
5186			{
5187				$sRet = $this->GetDefaultValue();
5188			}
5189			else
5190			{
5191				throw new Exception('AttributeMetaEnum::MapValue(): mapping not found for value "'.$value.'" in '.get_class($oObject).', on attribute '.MetaModel::GetAttributeOrigin($this->GetHostClass(),
5192						$this->GetCode()).'::'.$this->GetCode());
5193			}
5194		}
5195
5196		return $sRet;
5197	}
5198
5199	public function GetMapRule($sClass)
5200	{
5201		$aMappings = $this->Get('mapping');
5202		if (array_key_exists($sClass, $aMappings))
5203		{
5204			$aMappingData = $aMappings[$sClass];
5205		}
5206		else
5207		{
5208			$sParent = MetaModel::GetParentClass($sClass);
5209			if (is_null($sParent))
5210			{
5211				$aMappingData = null;
5212			}
5213			else
5214			{
5215				$aMappingData = $this->GetMapRule($sParent);
5216			}
5217		}
5218
5219		return $aMappingData;
5220	}
5221}
5222
5223/**
5224 * Map a date+time column to an attribute
5225 *
5226 * @package     iTopORM
5227 */
5228class AttributeDateTime extends AttributeDBField
5229{
5230	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE_TIME;
5231
5232	static $oFormat = null;
5233
5234	/**
5235	 *
5236	 * @return DateTimeFormat
5237	 */
5238	static public function GetFormat()
5239	{
5240		if (self::$oFormat == null)
5241		{
5242			static::LoadFormatFromConfig();
5243		}
5244
5245		return self::$oFormat;
5246	}
5247
5248	/**
5249	 * Load the 3 settings: date format, time format and data_time format from the configuration
5250	 */
5251	protected static function LoadFormatFromConfig()
5252	{
5253		$aFormats = MetaModel::GetConfig()->Get('date_and_time_format');
5254		$sLang = Dict::GetUserLanguage();
5255		$sDateFormat = isset($aFormats[$sLang]['date']) ? $aFormats[$sLang]['date'] : (isset($aFormats['default']['date']) ? $aFormats['default']['date'] : 'Y-m-d');
5256		$sTimeFormat = isset($aFormats[$sLang]['time']) ? $aFormats[$sLang]['time'] : (isset($aFormats['default']['time']) ? $aFormats['default']['time'] : 'H:i:s');
5257		$sDateAndTimeFormat = isset($aFormats[$sLang]['date_time']) ? $aFormats[$sLang]['date_time'] : (isset($aFormats['default']['date_time']) ? $aFormats['default']['date_time'] : '$date $time');
5258
5259		$sFullFormat = str_replace(array('$date', '$time'), array($sDateFormat, $sTimeFormat), $sDateAndTimeFormat);
5260
5261		self::SetFormat(new DateTimeFormat($sFullFormat));
5262		AttributeDate::SetFormat(new DateTimeFormat($sDateFormat));
5263	}
5264
5265	/**
5266	 * Returns the format string used for the date & time stored in memory
5267	 *
5268	 * @return string
5269	 */
5270	static public function GetInternalFormat()
5271	{
5272		return 'Y-m-d H:i:s';
5273	}
5274
5275	/**
5276	 * Returns the format string used for the date & time written to MySQL
5277	 *
5278	 * @return string
5279	 */
5280	static public function GetSQLFormat()
5281	{
5282		return 'Y-m-d H:i:s';
5283	}
5284
5285	static public function SetFormat(DateTimeFormat $oDateTimeFormat)
5286	{
5287		self::$oFormat = $oDateTimeFormat;
5288	}
5289
5290	static public function GetSQLTimeFormat()
5291	{
5292		return 'H:i:s';
5293	}
5294
5295	/**
5296	 * Parses a search string coming from user input
5297	 *
5298	 * @param string $sSearchString
5299	 *
5300	 * @return string
5301	 */
5302	public function ParseSearchString($sSearchString)
5303	{
5304		try
5305		{
5306			$oDateTime = $this->GetFormat()->Parse($sSearchString);
5307			$sSearchString = $oDateTime->format($this->GetInternalFormat());
5308		} catch (Exception $e)
5309		{
5310			$sFormatString = '!'.(string)AttributeDate::GetFormat(); // BEWARE: ! is needed to set non-parsed fields to zero !!!
5311			$oDateTime = DateTime::createFromFormat($sFormatString, $sSearchString);
5312			if ($oDateTime !== false)
5313			{
5314				$sSearchString = $oDateTime->format($this->GetInternalFormat());
5315			}
5316		}
5317
5318		return $sSearchString;
5319	}
5320
5321	static public function GetFormFieldClass()
5322	{
5323		return '\\Combodo\\iTop\\Form\\Field\\DateTimeField';
5324	}
5325
5326	/**
5327	 * Override to specify Field class
5328	 *
5329	 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the
5330	 * $oFormField is passed, MakeFormField behave more like a Prepare.
5331	 */
5332	public function MakeFormField(DBObject $oObject, $oFormField = null)
5333	{
5334		if ($oFormField === null)
5335		{
5336			$sFormFieldClass = static::GetFormFieldClass();
5337			$oFormField = new $sFormFieldClass($this->GetCode());
5338		}
5339		$oFormField->SetPHPDateTimeFormat((string)$this->GetFormat());
5340		$oFormField->SetJSDateTimeFormat($this->GetFormat()->ToMomentJS());
5341
5342		$oFormField = parent::MakeFormField($oObject, $oFormField);
5343
5344		// After call to the parent as it sets the current value
5345		$oFormField->SetCurrentValue($this->GetFormat()->Format($oObject->Get($this->GetCode())));
5346
5347		return $oFormField;
5348	}
5349
5350	/**
5351	 * @inheritdoc
5352	 */
5353	public function EnumTemplateVerbs()
5354	{
5355		return array(
5356			'' => 'Formatted representation',
5357			'raw' => 'Not formatted representation',
5358		);
5359	}
5360
5361	/**
5362	 * @inheritdoc
5363	 */
5364	public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
5365	{
5366		switch ($sVerb)
5367		{
5368			case '':
5369			case 'text':
5370				return static::GetFormat()->format($value);
5371				break;
5372			case 'html':
5373				// Note: Not passing formatted value as the method will format it.
5374				return $this->GetAsHTML($value);
5375				break;
5376			case 'raw':
5377				return $value;
5378				break;
5379			default:
5380				return parent::GetForTemplate($value, $sVerb, $oHostObject, $bLocalize);
5381				break;
5382		}
5383	}
5384
5385	static public function ListExpectedParams()
5386	{
5387		return parent::ListExpectedParams();
5388		//return array_merge(parent::ListExpectedParams(), array());
5389	}
5390
5391	public function GetEditClass()
5392	{
5393		return "DateTime";
5394	}
5395
5396
5397	public function GetEditValue($sValue, $oHostObj = null)
5398	{
5399		return (string)static::GetFormat()->format($sValue);
5400	}
5401
5402	public function GetValueLabel($sValue, $oHostObj = null)
5403	{
5404		return (string)static::GetFormat()->format($sValue);
5405	}
5406
5407	protected function GetSQLCol($bFullSpec = false)
5408	{
5409		return "DATETIME";
5410	}
5411
5412	public function GetImportColumns()
5413	{
5414		// Allow an empty string to be a valid value (synonym for "reset")
5415		$aColumns = array();
5416		$aColumns[$this->GetCode()] = 'VARCHAR(19)';
5417
5418		return $aColumns;
5419	}
5420
5421	public static function GetAsUnixSeconds($value)
5422	{
5423		$oDeadlineDateTime = new DateTime($value);
5424		$iUnixSeconds = $oDeadlineDateTime->format('U');
5425
5426		return $iUnixSeconds;
5427	}
5428
5429	public function GetDefaultValue(DBObject $oHostObject = null)
5430	{
5431		// null value will be replaced by the current date, if not already set, in DoComputeValues
5432		return $this->GetNullValue();
5433	}
5434
5435	public function GetValidationPattern()
5436	{
5437		return static::GetFormat()->ToRegExpr();
5438	}
5439
5440	public function GetBasicFilterOperators()
5441	{
5442		return array(
5443			"=" => "equals",
5444			"!=" => "differs from",
5445			"<" => "before",
5446			"<=" => "before",
5447			">" => "after (strictly)",
5448			">=" => "after",
5449			"SameDay" => "same day (strip time)",
5450			"SameMonth" => "same year/month",
5451			"SameYear" => "same year",
5452			"Today" => "today",
5453			">|" => "after today + N days",
5454			"<|" => "before today + N days",
5455			"=|" => "equals today + N days",
5456		);
5457	}
5458
5459	public function GetBasicFilterLooseOperator()
5460	{
5461		// Unless we implement a "same xxx, depending on given precision" !
5462		return "=";
5463	}
5464
5465	public function GetBasicFilterSQLExpr($sOpCode, $value)
5466	{
5467		$sQValue = CMDBSource::Quote($value);
5468
5469		switch ($sOpCode)
5470		{
5471			case '=':
5472			case '!=':
5473			case '<':
5474			case '<=':
5475			case '>':
5476			case '>=':
5477				return $this->GetSQLExpr()." $sOpCode $sQValue";
5478			case 'SameDay':
5479				return "DATE(".$this->GetSQLExpr().") = DATE($sQValue)";
5480			case 'SameMonth':
5481				return "DATE_FORMAT(".$this->GetSQLExpr().", '%Y-%m') = DATE_FORMAT($sQValue, '%Y-%m')";
5482			case 'SameYear':
5483				return "MONTH(".$this->GetSQLExpr().") = MONTH($sQValue)";
5484			case 'Today':
5485				return "DATE(".$this->GetSQLExpr().") = CURRENT_DATE()";
5486			case '>|':
5487				return "DATE(".$this->GetSQLExpr().") > DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)";
5488			case '<|':
5489				return "DATE(".$this->GetSQLExpr().") < DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)";
5490			case '=|':
5491				return "DATE(".$this->GetSQLExpr().") = DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)";
5492			default:
5493				return $this->GetSQLExpr()." = $sQValue";
5494		}
5495	}
5496
5497	public function MakeRealValue($proposedValue, $oHostObj)
5498	{
5499		if (is_null($proposedValue))
5500		{
5501			return null;
5502		}
5503		if (is_string($proposedValue) && ($proposedValue == "") && $this->IsNullAllowed())
5504		{
5505			return null;
5506		}
5507		if (!is_numeric($proposedValue))
5508		{
5509			// Check the format
5510			try
5511			{
5512				$oFormat = new DateTimeFormat($this->GetInternalFormat());
5513				$oFormat->Parse($proposedValue);
5514			} catch (Exception $e)
5515			{
5516				throw new Exception('Wrong format for date attribute '.$this->GetCode().', expecting "'.$this->GetInternalFormat().'" and got "'.$proposedValue.'"');
5517			}
5518
5519			return $proposedValue;
5520		}
5521
5522		return date(static::GetInternalFormat(), $proposedValue);
5523	}
5524
5525	public function ScalarToSQL($value)
5526	{
5527		if (empty($value))
5528		{
5529			return null;
5530		}
5531
5532		return $value;
5533	}
5534
5535	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
5536	{
5537		return Str::pure2html(static::GetFormat()->format($value));
5538	}
5539
5540	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
5541	{
5542		return Str::pure2xml($value);
5543	}
5544
5545	public function GetAsCSV(
5546		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
5547		$bConvertToPlainText = false
5548	) {
5549		if (empty($sValue) || ($sValue === '0000-00-00 00:00:00') || ($sValue === '0000-00-00'))
5550		{
5551			return '';
5552		}
5553		else
5554		{
5555			if ((string)static::GetFormat() !== static::GetInternalFormat())
5556			{
5557				// Format conversion
5558				$oDate = new DateTime($sValue);
5559				if ($oDate !== false)
5560				{
5561					$sValue = static::GetFormat()->format($oDate);
5562				}
5563			}
5564		}
5565		$sFrom = array("\r\n", $sTextQualifier);
5566		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
5567		$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
5568
5569		return $sTextQualifier.$sEscaped.$sTextQualifier;
5570	}
5571
5572	/**
5573	 * Parses a string to find some smart search patterns and build the corresponding search/OQL condition
5574	 * Each derived class is reponsible for defining and processing their own smart patterns, the base class
5575	 * does nothing special, and just calls the default (loose) operator
5576	 *
5577	 * @param string $sSearchText The search string to analyze for smart patterns
5578	 * @param FieldExpression $oField The FieldExpression representing the atttribute code in this OQL query
5579	 * @param array $aParams Values of the query parameters
5580	 * @param bool $bParseSearchString
5581	 *
5582	 * @return Expression The search condition to be added (AND) to the current search
5583	 * @throws \CoreException
5584	 */
5585	public function GetSmartConditionExpression(
5586		$sSearchText, FieldExpression $oField, &$aParams, $bParseSearchString = false
5587	) {
5588		// Possible smart patterns
5589		$aPatterns = array(
5590			'between' => array('pattern' => '/^\[(.*),(.*)\]$/', 'operator' => 'n/a'),
5591			'greater than or equal' => array('pattern' => '/^>=(.*)$/', 'operator' => '>='),
5592			'greater than' => array('pattern' => '/^>(.*)$/', 'operator' => '>'),
5593			'less than or equal' => array('pattern' => '/^<=(.*)$/', 'operator' => '<='),
5594			'less than' => array('pattern' => '/^<(.*)$/', 'operator' => '<'),
5595		);
5596
5597		$sPatternFound = '';
5598		$aMatches = array();
5599		foreach($aPatterns as $sPatName => $sPattern)
5600		{
5601			if (preg_match($sPattern['pattern'], $sSearchText, $aMatches))
5602			{
5603				$sPatternFound = $sPatName;
5604				break;
5605			}
5606		}
5607
5608		switch ($sPatternFound)
5609		{
5610			case 'between':
5611
5612				$sParamName1 = $oField->GetParent().'_'.$oField->GetName().'_1';
5613				$oRightExpr = new VariableExpression($sParamName1);
5614				if ($bParseSearchString)
5615				{
5616					$aParams[$sParamName1] = $this->ParseSearchString($aMatches[1]);
5617				}
5618				else
5619				{
5620					$aParams[$sParamName1] = $aMatches[1];
5621				}
5622				$oCondition1 = new BinaryExpression($oField, '>=', $oRightExpr);
5623
5624				$sParamName2 = $oField->GetParent().'_'.$oField->GetName().'_2';
5625				$oRightExpr = new VariableExpression($sParamName2);
5626				if ($bParseSearchString)
5627				{
5628					$aParams[$sParamName2] = $this->ParseSearchString($aMatches[2]);
5629				}
5630				else
5631				{
5632					$aParams[$sParamName2] = $aMatches[2];
5633				}
5634				$oCondition2 = new BinaryExpression($oField, '<=', $oRightExpr);
5635
5636				$oNewCondition = new BinaryExpression($oCondition1, 'AND', $oCondition2);
5637				break;
5638
5639			case 'greater than':
5640			case 'greater than or equal':
5641			case 'less than':
5642			case 'less than or equal':
5643				$sSQLOperator = $aPatterns[$sPatternFound]['operator'];
5644				$sParamName = $oField->GetParent().'_'.$oField->GetName();
5645				$oRightExpr = new VariableExpression($sParamName);
5646				if ($bParseSearchString)
5647				{
5648					$aParams[$sParamName] = $this->ParseSearchString($aMatches[1]);
5649				}
5650				else
5651				{
5652					$aParams[$sParamName] = $aMatches[1];
5653				}
5654				$oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr);
5655
5656				break;
5657
5658			default:
5659				$oNewCondition = parent::GetSmartConditionExpression($sSearchText, $oField, $aParams);
5660
5661		}
5662
5663		return $oNewCondition;
5664	}
5665
5666
5667	public function GetHelpOnSmartSearch()
5668	{
5669		$sDict = parent::GetHelpOnSmartSearch();
5670
5671		$oFormat = static::GetFormat();
5672		$sExample = $oFormat->Format(new DateTime('2015-07-19 18:40:00'));
5673
5674		return vsprintf($sDict, array($oFormat->ToPlaceholder(), $sExample));
5675	}
5676}
5677
5678/**
5679 * Store a duration as a number of seconds
5680 *
5681 * @package     iTopORM
5682 */
5683class AttributeDuration extends AttributeInteger
5684{
5685	public function GetEditClass()
5686	{
5687		return "Duration";
5688	}
5689
5690	protected function GetSQLCol($bFullSpec = false)
5691	{
5692		return "INT(11) UNSIGNED";
5693	}
5694
5695	public function GetNullValue()
5696	{
5697		return '0';
5698	}
5699
5700	public function MakeRealValue($proposedValue, $oHostObj)
5701	{
5702		if (is_null($proposedValue))
5703		{
5704			return null;
5705		}
5706		if (!is_numeric($proposedValue))
5707		{
5708			return null;
5709		}
5710		if (((int)$proposedValue) < 0)
5711		{
5712			return null;
5713		}
5714
5715		return (int)$proposedValue;
5716	}
5717
5718	public function ScalarToSQL($value)
5719	{
5720		if (is_null($value))
5721		{
5722			return null;
5723		}
5724
5725		return $value;
5726	}
5727
5728	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
5729	{
5730		return Str::pure2html(self::FormatDuration($value));
5731	}
5732
5733	public static function FormatDuration($duration)
5734	{
5735		$aDuration = self::SplitDuration($duration);
5736
5737		if ($duration < 60)
5738		{
5739			// Less than 1 min
5740			$sResult = Dict::Format('Core:Duration_Seconds', $aDuration['seconds']);
5741		}
5742		else
5743		{
5744			if ($duration < 3600)
5745			{
5746				// less than 1 hour, display it in minutes/seconds
5747				$sResult = Dict::Format('Core:Duration_Minutes_Seconds', $aDuration['minutes'], $aDuration['seconds']);
5748			}
5749			else
5750			{
5751				if ($duration < 86400)
5752				{
5753					// Less than 1 day, display it in hours/minutes/seconds
5754					$sResult = Dict::Format('Core:Duration_Hours_Minutes_Seconds', $aDuration['hours'],
5755						$aDuration['minutes'], $aDuration['seconds']);
5756				}
5757				else
5758				{
5759					// more than 1 day, display it in days/hours/minutes/seconds
5760					$sResult = Dict::Format('Core:Duration_Days_Hours_Minutes_Seconds', $aDuration['days'],
5761						$aDuration['hours'], $aDuration['minutes'], $aDuration['seconds']);
5762				}
5763			}
5764		}
5765
5766		return $sResult;
5767	}
5768
5769	static function SplitDuration($duration)
5770	{
5771		$duration = (int)$duration;
5772		$days = floor($duration / 86400);
5773		$hours = floor(($duration - (86400 * $days)) / 3600);
5774		$minutes = floor(($duration - (86400 * $days + 3600 * $hours)) / 60);
5775		$seconds = ($duration % 60); // modulo
5776
5777		return array('days' => $days, 'hours' => $hours, 'minutes' => $minutes, 'seconds' => $seconds);
5778	}
5779
5780	static public function GetFormFieldClass()
5781	{
5782		return '\\Combodo\\iTop\\Form\\Field\\DurationField';
5783	}
5784
5785	public function MakeFormField(DBObject $oObject, $oFormField = null)
5786	{
5787		if ($oFormField === null)
5788		{
5789			$sFormFieldClass = static::GetFormFieldClass();
5790			$oFormField = new $sFormFieldClass($this->GetCode());
5791		}
5792		parent::MakeFormField($oObject, $oFormField);
5793
5794		// Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition
5795		$sAttCode = $this->GetCode();
5796		$oFormField->SetCurrentValue($oObject->Get($sAttCode));
5797		$oFormField->SetReadOnly(true);
5798
5799		return $oFormField;
5800	}
5801
5802}
5803
5804/**
5805 * Map a date+time column to an attribute
5806 *
5807 * @package     iTopORM
5808 */
5809class AttributeDate extends AttributeDateTime
5810{
5811	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE;
5812
5813	static $oDateFormat = null;
5814
5815	static public function GetFormat()
5816	{
5817		if (self::$oDateFormat == null)
5818		{
5819			AttributeDateTime::LoadFormatFromConfig();
5820		}
5821
5822		return self::$oDateFormat;
5823	}
5824
5825	static public function SetFormat(DateTimeFormat $oDateFormat)
5826	{
5827		self::$oDateFormat = $oDateFormat;
5828	}
5829
5830	/**
5831	 * Returns the format string used for the date & time stored in memory
5832	 *
5833	 * @return string
5834	 */
5835	static public function GetInternalFormat()
5836	{
5837		return 'Y-m-d';
5838	}
5839
5840	/**
5841	 * Returns the format string used for the date & time written to MySQL
5842	 *
5843	 * @return string
5844	 */
5845	static public function GetSQLFormat()
5846	{
5847		return 'Y-m-d';
5848	}
5849
5850	static public function ListExpectedParams()
5851	{
5852		return parent::ListExpectedParams();
5853		//return array_merge(parent::ListExpectedParams(), array());
5854	}
5855
5856	public function GetEditClass()
5857	{
5858		return "Date";
5859	}
5860
5861	protected function GetSQLCol($bFullSpec = false)
5862	{
5863		return "DATE";
5864	}
5865
5866	public function GetImportColumns()
5867	{
5868		// Allow an empty string to be a valid value (synonym for "reset")
5869		$aColumns = array();
5870		$aColumns[$this->GetCode()] = 'VARCHAR(10)';
5871
5872		return $aColumns;
5873	}
5874
5875
5876	/**
5877	 * Override to specify Field class
5878	 *
5879	 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the
5880	 * $oFormField is passed, MakeFormField behave more like a Prepare.
5881	 */
5882	public function MakeFormField(DBObject $oObject, $oFormField = null)
5883	{
5884		$oFormField = parent::MakeFormField($oObject, $oFormField);
5885		$oFormField->SetDateOnly(true);
5886
5887		return $oFormField;
5888	}
5889
5890}
5891
5892/**
5893 * A dead line stored as a date & time
5894 * The only difference with the DateTime attribute is the display:
5895 * relative to the current time
5896 */
5897class AttributeDeadline extends AttributeDateTime
5898{
5899	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
5900	{
5901		$sResult = self::FormatDeadline($value);
5902
5903		return $sResult;
5904	}
5905
5906	public static function FormatDeadline($value)
5907	{
5908		$sResult = '';
5909		if ($value !== null)
5910		{
5911			$iValue = AttributeDateTime::GetAsUnixSeconds($value);
5912			$sDate = AttributeDateTime::GetFormat()->Format($value);
5913			$difference = $iValue - time();
5914
5915			if ($difference >= 0)
5916			{
5917				$sDifference = self::FormatDuration($difference);
5918			}
5919			else
5920			{
5921				$sDifference = Dict::Format('UI:DeadlineMissedBy_duration', self::FormatDuration(-$difference));
5922			}
5923			$sFormat = MetaModel::GetConfig()->Get('deadline_format');
5924			$sResult = str_replace(array('$date$', '$difference$'), array($sDate, $sDifference), $sFormat);
5925		}
5926
5927		return $sResult;
5928	}
5929
5930	static function FormatDuration($duration)
5931	{
5932		$days = floor($duration / 86400);
5933		$hours = floor(($duration - (86400 * $days)) / 3600);
5934		$minutes = floor(($duration - (86400 * $days + 3600 * $hours)) / 60);
5935
5936		if ($duration < 60)
5937		{
5938			// Less than 1 min
5939			$sResult = Dict::S('UI:Deadline_LessThan1Min');
5940		}
5941		else
5942		{
5943			if ($duration < 3600)
5944			{
5945				// less than 1 hour, display it in minutes
5946				$sResult = Dict::Format('UI:Deadline_Minutes', $minutes);
5947			}
5948			else
5949			{
5950				if ($duration < 86400)
5951				{
5952					// Less that 1 day, display it in hours/minutes
5953					$sResult = Dict::Format('UI:Deadline_Hours_Minutes', $hours, $minutes);
5954				}
5955				else
5956				{
5957					// Less that 1 day, display it in hours/minutes
5958					$sResult = Dict::Format('UI:Deadline_Days_Hours_Minutes', $days, $hours, $minutes);
5959				}
5960			}
5961		}
5962
5963		return $sResult;
5964	}
5965}
5966
5967/**
5968 * Map a foreign key to an attribute
5969 *  AttributeExternalKey and AttributeExternalField may be an external key
5970 *  the difference is that AttributeExternalKey corresponds to a column into the defined table
5971 *  where an AttributeExternalField corresponds to a column into another table (class)
5972 *
5973 * @package     iTopORM
5974 */
5975class AttributeExternalKey extends AttributeDBFieldVoid
5976{
5977	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY;
5978
5979
5980	/**
5981	 * Return the search widget type corresponding to this attribute
5982	 *
5983	 * @return string
5984	 */
5985	public function GetSearchType()
5986	{
5987		try
5988		{
5989			$oRemoteAtt = $this->GetFinalAttDef();
5990			$sTargetClass = $oRemoteAtt->GetTargetClass();
5991			if (MetaModel::IsHierarchicalClass($sTargetClass))
5992			{
5993				return self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY;
5994			}
5995
5996			return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY;
5997		} catch (CoreException $e)
5998		{
5999		}
6000
6001		return self::SEARCH_WIDGET_TYPE_RAW;
6002	}
6003
6004	static public function ListExpectedParams()
6005	{
6006		return array_merge(parent::ListExpectedParams(), array("targetclass", "is_null_allowed", "on_target_delete"));
6007	}
6008
6009	public function GetEditClass()
6010	{
6011		return "ExtKey";
6012	}
6013
6014	protected function GetSQLCol($bFullSpec = false)
6015	{
6016		return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");
6017	}
6018
6019	public function RequiresIndex()
6020	{
6021		return true;
6022	}
6023
6024	public function IsExternalKey($iType = EXTKEY_RELATIVE)
6025	{
6026		return true;
6027	}
6028
6029	public function GetTargetClass($iType = EXTKEY_RELATIVE)
6030	{
6031		return $this->Get("targetclass");
6032	}
6033
6034	public function GetKeyAttDef($iType = EXTKEY_RELATIVE)
6035	{
6036		return $this;
6037	}
6038
6039	public function GetKeyAttCode()
6040	{
6041		return $this->GetCode();
6042	}
6043
6044	public function GetDisplayStyle()
6045	{
6046		return $this->GetOptional('display_style', 'select');
6047	}
6048
6049
6050	public function GetDefaultValue(DBObject $oHostObject = null)
6051	{
6052		return 0;
6053	}
6054
6055	public function IsNullAllowed()
6056	{
6057		if (MetaModel::GetConfig()->Get('disable_mandatory_ext_keys'))
6058		{
6059			return true;
6060		}
6061
6062		return $this->Get("is_null_allowed");
6063	}
6064
6065
6066	public function GetBasicFilterOperators()
6067	{
6068		return parent::GetBasicFilterOperators();
6069	}
6070
6071	public function GetBasicFilterLooseOperator()
6072	{
6073		return parent::GetBasicFilterLooseOperator();
6074	}
6075
6076	public function GetBasicFilterSQLExpr($sOpCode, $value)
6077	{
6078		return parent::GetBasicFilterSQLExpr($sOpCode, $value);
6079	}
6080
6081	// overloaded here so that an ext key always have the answer to
6082	// "what are your possible values?"
6083	public function GetValuesDef()
6084	{
6085		$oValSetDef = $this->Get("allowed_values");
6086		if (!$oValSetDef)
6087		{
6088			// Let's propose every existing value
6089			$oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass());
6090		}
6091
6092		return $oValSetDef;
6093	}
6094
6095	public function GetAllowedValues($aArgs = array(), $sContains = '')
6096	{
6097		//throw new Exception("GetAllowedValues on ext key has been deprecated");
6098		try
6099		{
6100			return parent::GetAllowedValues($aArgs, $sContains);
6101		} catch (Exception $e)
6102		{
6103			// Some required arguments could not be found, enlarge to any existing value
6104			$oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass());
6105
6106			return $oValSetDef->GetValues($aArgs, $sContains);
6107		}
6108	}
6109
6110	public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null)
6111	{
6112		$oValSetDef = $this->GetValuesDef();
6113		$oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue);
6114
6115		return $oSet;
6116	}
6117
6118	public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null)
6119	{
6120		return DBObjectSearch::FromOQL($this->GetValuesDef()->GetFilterExpression());
6121	}
6122
6123	public function GetDeletionPropagationOption()
6124	{
6125		return $this->Get("on_target_delete");
6126	}
6127
6128	public function GetNullValue()
6129	{
6130		return 0;
6131	}
6132
6133	public function IsNull($proposedValue)
6134	{
6135		return ($proposedValue == 0);
6136	}
6137
6138	public function MakeRealValue($proposedValue, $oHostObj)
6139	{
6140		if (is_null($proposedValue))
6141		{
6142			return 0;
6143		}
6144		if ($proposedValue === '')
6145		{
6146			return 0;
6147		}
6148		if (MetaModel::IsValidObject($proposedValue))
6149		{
6150			return $proposedValue->GetKey();
6151		}
6152
6153		return (int)$proposedValue;
6154	}
6155
6156	public function GetMaximumComboLength()
6157	{
6158		return $this->GetOptional('max_combo_length', MetaModel::GetConfig()->Get('max_combo_length'));
6159	}
6160
6161	public function GetMinAutoCompleteChars()
6162	{
6163		return $this->GetOptional('min_autocomplete_chars', MetaModel::GetConfig()->Get('min_autocomplete_chars'));
6164	}
6165
6166	public function AllowTargetCreation()
6167	{
6168		return $this->GetOptional('allow_target_creation', MetaModel::GetConfig()->Get('allow_target_creation'));
6169	}
6170
6171	/**
6172	 * Find the corresponding "link" attribute on the target class, if any
6173	 *
6174	 * @return null | AttributeDefinition
6175	 * @throws \CoreException
6176	 */
6177	public function GetMirrorLinkAttribute()
6178	{
6179		$oRet = null;
6180		$sRemoteClass = $this->GetTargetClass();
6181		foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef)
6182		{
6183			if (!$oRemoteAttDef->IsLinkSet())
6184			{
6185				continue;
6186			}
6187			if (!is_subclass_of($this->GetHostClass(),
6188					$oRemoteAttDef->GetLinkedClass()) && $oRemoteAttDef->GetLinkedClass() != $this->GetHostClass())
6189			{
6190				continue;
6191			}
6192			if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetCode())
6193			{
6194				continue;
6195			}
6196			$oRet = $oRemoteAttDef;
6197			break;
6198		}
6199
6200		return $oRet;
6201	}
6202
6203	static public function GetFormFieldClass()
6204	{
6205		return '\\Combodo\\iTop\\Form\\Field\\SelectObjectField';
6206	}
6207
6208	public function MakeFormField(DBObject $oObject, $oFormField = null)
6209	{
6210		if ($oFormField === null)
6211		{
6212			// Later : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value
6213			$sFormFieldClass = static::GetFormFieldClass();
6214			$oFormField = new $sFormFieldClass($this->GetCode());
6215		}
6216
6217		// Setting params
6218		$oFormField->SetMaximumComboLength($this->GetMaximumComboLength());
6219		$oFormField->SetMinAutoCompleteChars($this->GetMinAutoCompleteChars());
6220		$oFormField->SetHierarchical(MetaModel::IsHierarchicalClass($this->GetTargetClass()));
6221		// Setting choices regarding the field dependencies
6222		$aFieldDependencies = $this->GetPrerequisiteAttributes();
6223		if (!empty($aFieldDependencies))
6224		{
6225			$oTmpAttDef = $this;
6226			$oTmpField = $oFormField;
6227			$oFormField->SetOnFinalizeCallback(function () use ($oTmpField, $oTmpAttDef, $oObject) {
6228				/** @var $oTmpField \Combodo\iTop\Form\Field\Field */
6229				/** @var $oTmpAttDef \AttributeDefinition */
6230				/** @var $oObject \DBObject */
6231
6232				// We set search object only if it has not already been set (overrided)
6233				if ($oTmpField->GetSearch() === null)
6234				{
6235					$oSearch = DBSearch::FromOQL($oTmpAttDef->GetValuesDef()->GetFilterExpression());
6236					$oSearch->SetInternalParams(array('this' => $oObject));
6237					$oTmpField->SetSearch($oSearch);
6238				}
6239			});
6240		}
6241		else
6242		{
6243			$oSearch = DBSearch::FromOQL($this->GetValuesDef()->GetFilterExpression());
6244			$oSearch->SetInternalParams(array('this' => $oObject));
6245			$oFormField->SetSearch($oSearch);
6246		}
6247
6248		// If ExtKey is mandatory, we add a validator to ensure that the value 0 is not selected
6249		if ($oObject->GetAttributeFlags($this->GetCode()) & OPT_ATT_MANDATORY)
6250		{
6251			$oFormField->AddValidator(new \Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator());
6252		}
6253
6254		parent::MakeFormField($oObject, $oFormField);
6255
6256		return $oFormField;
6257	}
6258
6259	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
6260	{
6261		if (!is_null($oHostObject))
6262		{
6263			return $oHostObject->GetAsHTML($this->GetCode(), $oHostObject);
6264		}
6265
6266		return DBObject::MakeHyperLink($this->GetTargetClass(), $sValue);
6267	}
6268}
6269
6270/**
6271 * Special kind of External Key to manage a hierarchy of objects
6272 */
6273class AttributeHierarchicalKey extends AttributeExternalKey
6274{
6275	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY;
6276
6277	protected $m_sTargetClass;
6278
6279	static public function ListExpectedParams()
6280	{
6281		$aParams = parent::ListExpectedParams();
6282		$idx = array_search('targetclass', $aParams);
6283		unset($aParams[$idx]);
6284		$idx = array_search('jointype', $aParams);
6285		unset($aParams[$idx]);
6286
6287		return $aParams; // Later: mettre les bons parametres ici !!
6288	}
6289
6290	public function GetEditClass()
6291	{
6292		return "ExtKey";
6293	}
6294
6295	public function RequiresIndex()
6296	{
6297		return true;
6298	}
6299
6300	/*
6301	*  The target class is the class for which the attribute has been defined first
6302	*/
6303	public function SetHostClass($sHostClass)
6304	{
6305		if (!isset($this->m_sTargetClass))
6306		{
6307			$this->m_sTargetClass = $sHostClass;
6308		}
6309		parent::SetHostClass($sHostClass);
6310	}
6311
6312	static public function IsHierarchicalKey()
6313	{
6314		return true;
6315	}
6316
6317	public function GetTargetClass($iType = EXTKEY_RELATIVE)
6318	{
6319		return $this->m_sTargetClass;
6320	}
6321
6322	public function GetKeyAttDef($iType = EXTKEY_RELATIVE)
6323	{
6324		return $this;
6325	}
6326
6327	public function GetKeyAttCode()
6328	{
6329		return $this->GetCode();
6330	}
6331
6332	public function GetBasicFilterOperators()
6333	{
6334		return parent::GetBasicFilterOperators();
6335	}
6336
6337	public function GetBasicFilterLooseOperator()
6338	{
6339		return parent::GetBasicFilterLooseOperator();
6340	}
6341
6342	public function GetSQLColumns($bFullSpec = false)
6343	{
6344		$aColumns = array();
6345		$aColumns[$this->GetCode()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : '');
6346		$aColumns[$this->GetSQLLeft()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : '');
6347		$aColumns[$this->GetSQLRight()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : '');
6348
6349		return $aColumns;
6350	}
6351
6352	public function GetSQLRight()
6353	{
6354		return $this->GetCode().'_right';
6355	}
6356
6357	public function GetSQLLeft()
6358	{
6359		return $this->GetCode().'_left';
6360	}
6361
6362	public function GetSQLValues($value)
6363	{
6364		if (!is_array($value))
6365		{
6366			$aValues[$this->GetCode()] = $value;
6367		}
6368		else
6369		{
6370			$aValues = array();
6371			$aValues[$this->GetCode()] = $value[$this->GetCode()];
6372			$aValues[$this->GetSQLRight()] = $value[$this->GetSQLRight()];
6373			$aValues[$this->GetSQLLeft()] = $value[$this->GetSQLLeft()];
6374		}
6375
6376		return $aValues;
6377	}
6378
6379	public function GetAllowedValues($aArgs = array(), $sContains = '')
6380	{
6381		$oFilter = $this->GetHierachicalFilter($aArgs, $sContains);
6382		if ($oFilter)
6383		{
6384			$oValSetDef = $this->GetValuesDef();
6385			$oValSetDef->AddCondition($oFilter);
6386
6387			return $oValSetDef->GetValues($aArgs, $sContains);
6388		}
6389		else
6390		{
6391			return parent::GetAllowedValues($aArgs, $sContains);
6392		}
6393	}
6394
6395	public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null)
6396	{
6397		$oValSetDef = $this->GetValuesDef();
6398		$oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue);
6399		if ($oFilter)
6400		{
6401			$oValSetDef->AddCondition($oFilter);
6402		}
6403		$oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue);
6404
6405		return $oSet;
6406	}
6407
6408	public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null)
6409	{
6410		$oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue);
6411		if ($oFilter)
6412		{
6413			return $oFilter;
6414		}
6415
6416		return parent::GetAllowedValuesAsFilter($aArgs, $sContains, $iAdditionalValue);
6417	}
6418
6419	private function GetHierachicalFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null)
6420	{
6421		if (array_key_exists('this', $aArgs))
6422		{
6423			// Hierarchical keys have one more constraint: the "parent value" cannot be
6424			// "under" themselves
6425			$iRootId = $aArgs['this']->GetKey();
6426			if ($iRootId > 0) // ignore objects that do no exist in the database...
6427			{
6428				$sClass = $this->m_sTargetClass;
6429
6430				return DBObjectSearch::FromOQL("SELECT $sClass AS node JOIN $sClass AS root ON node.".$this->GetCode()." NOT BELOW root.id WHERE root.id = $iRootId");
6431			}
6432		}
6433
6434		return false;
6435	}
6436
6437	/**
6438	 * Find the corresponding "link" attribute on the target class, if any
6439	 *
6440	 * @return null | AttributeDefinition
6441	 */
6442	public function GetMirrorLinkAttribute()
6443	{
6444		return null;
6445	}
6446}
6447
6448/**
6449 * An attribute which corresponds to an external key (direct or indirect)
6450 *
6451 * @package     iTopORM
6452 */
6453class AttributeExternalField extends AttributeDefinition
6454{
6455	/**
6456	 * Return the search widget type corresponding to this attribute
6457	 *
6458	 * @return string
6459	 * @throws \CoreException
6460	 */
6461	public function GetSearchType()
6462	{
6463		// Not necessary the external key is already present
6464		if ($this->IsFriendlyName())
6465		{
6466			return self::SEARCH_WIDGET_TYPE_RAW;
6467		}
6468
6469		try
6470		{
6471			$oRemoteAtt = $this->GetFinalAttDef();
6472			switch (true)
6473			{
6474				case ($oRemoteAtt instanceof AttributeString):
6475					return self::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD;
6476				case ($oRemoteAtt instanceof AttributeExternalKey):
6477					return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY;
6478			}
6479		} catch (CoreException $e)
6480		{
6481		}
6482
6483		return self::SEARCH_WIDGET_TYPE_RAW;
6484	}
6485
6486
6487	static public function ListExpectedParams()
6488	{
6489		return array_merge(parent::ListExpectedParams(), array("extkey_attcode", "target_attcode"));
6490	}
6491
6492	public function GetEditClass()
6493	{
6494		return "ExtField";
6495	}
6496
6497	/**
6498	 * @return \AttributeDefinition
6499	 * @throws \CoreException
6500	 */
6501	public function GetFinalAttDef()
6502	{
6503		$oExtAttDef = $this->GetExtAttDef();
6504
6505		return $oExtAttDef->GetFinalAttDef();
6506	}
6507
6508	protected function GetSQLCol($bFullSpec = false)
6509	{
6510		// throw new CoreException("external attribute: does it make any sense to request its type ?");
6511		$oExtAttDef = $this->GetExtAttDef();
6512
6513		return $oExtAttDef->GetSQLCol($bFullSpec);
6514	}
6515
6516	public function GetSQLExpressions($sPrefix = '')
6517	{
6518		if ($sPrefix == '')
6519		{
6520			return array('' => $this->GetCode()); // Warning: Use GetCode() since AttributeExternalField does not have any 'sql' property
6521		}
6522		else
6523		{
6524			return $sPrefix;
6525		}
6526	}
6527
6528	public function GetLabel($sDefault = null)
6529	{
6530		if ($this->IsFriendlyName())
6531		{
6532			$sKeyAttCode = $this->Get("extkey_attcode");
6533			$oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode);
6534			$sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode);
6535		}
6536		else
6537		{
6538			$sLabel = parent::GetLabel('');
6539			if (strlen($sLabel) == 0)
6540			{
6541				$oRemoteAtt = $this->GetExtAttDef();
6542				$sLabel = $oRemoteAtt->GetLabel($this->m_sCode);
6543				$oKeyAtt = $this->GetKeyAttDef();
6544				$sKeyLabel = $oKeyAtt->GetLabel($this->GetKeyAttCode());
6545				$sLabel = "{$sKeyLabel}->{$sLabel}";
6546			}
6547		}
6548
6549		return $sLabel;
6550	}
6551
6552	public function GetLabelForSearchField()
6553	{
6554		$sLabel = parent::GetLabel('');
6555		if (strlen($sLabel) == 0)
6556		{
6557			$sKeyAttCode = $this->Get("extkey_attcode");
6558			$oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode);
6559			$sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode);
6560
6561			$oRemoteAtt = $this->GetExtAttDef();
6562			$sLabel .= '->'.$oRemoteAtt->GetLabel($this->m_sCode);
6563		}
6564
6565		return $sLabel;
6566	}
6567
6568	public function GetDescription($sDefault = null)
6569	{
6570		$sLabel = parent::GetDescription('');
6571		if (strlen($sLabel) == 0)
6572		{
6573			$oRemoteAtt = $this->GetExtAttDef();
6574			$sLabel = $oRemoteAtt->GetDescription('');
6575		}
6576
6577		return $sLabel;
6578	}
6579
6580	public function GetHelpOnEdition($sDefault = null)
6581	{
6582		$sLabel = parent::GetHelpOnEdition('');
6583		if (strlen($sLabel) == 0)
6584		{
6585			$oRemoteAtt = $this->GetExtAttDef();
6586			$sLabel = $oRemoteAtt->GetHelpOnEdition('');
6587		}
6588
6589		return $sLabel;
6590	}
6591
6592	public function IsExternalKey($iType = EXTKEY_RELATIVE)
6593	{
6594		switch ($iType)
6595		{
6596			case EXTKEY_ABSOLUTE:
6597				// see further
6598				$oRemoteAtt = $this->GetExtAttDef();
6599
6600				return $oRemoteAtt->IsExternalKey($iType);
6601
6602			case EXTKEY_RELATIVE:
6603				return false;
6604
6605			default:
6606				throw new CoreException("Unexpected value for argument iType: '$iType'");
6607		}
6608	}
6609
6610	/**
6611	 * @return bool
6612	 * @throws \CoreException
6613	 */
6614	public function IsFriendlyName()
6615	{
6616		$oRemoteAtt = $this->GetExtAttDef();
6617		if ($oRemoteAtt instanceof AttributeExternalField)
6618		{
6619			$bRet = $oRemoteAtt->IsFriendlyName();
6620		}
6621		elseif ($oRemoteAtt instanceof AttributeFriendlyName)
6622		{
6623			$bRet = true;
6624		}
6625		else
6626		{
6627			$bRet = false;
6628		}
6629
6630		return $bRet;
6631	}
6632
6633	public function GetTargetClass($iType = EXTKEY_RELATIVE)
6634	{
6635		return $this->GetKeyAttDef($iType)->GetTargetClass();
6636	}
6637
6638	static public function IsExternalField()
6639	{
6640		return true;
6641	}
6642
6643	public function GetKeyAttCode()
6644	{
6645		return $this->Get("extkey_attcode");
6646	}
6647
6648	public function GetExtAttCode()
6649	{
6650		return $this->Get("target_attcode");
6651	}
6652
6653	/**
6654	 * @param int $iType
6655	 *
6656	 * @return \AttributeExternalKey
6657	 * @throws \CoreException
6658	 * @throws \Exception
6659	 */
6660	public function GetKeyAttDef($iType = EXTKEY_RELATIVE)
6661	{
6662		switch ($iType)
6663		{
6664			case EXTKEY_ABSOLUTE:
6665				// see further
6666				/** @var \AttributeExternalKey $oRemoteAtt */
6667				$oRemoteAtt = $this->GetExtAttDef();
6668				if ($oRemoteAtt->IsExternalField())
6669				{
6670					return $oRemoteAtt->GetKeyAttDef(EXTKEY_ABSOLUTE);
6671				}
6672				else
6673				{
6674					if ($oRemoteAtt->IsExternalKey())
6675					{
6676						return $oRemoteAtt;
6677					}
6678				}
6679
6680				return $this->GetKeyAttDef(EXTKEY_RELATIVE); // which corresponds to the code hereafter !
6681
6682			case EXTKEY_RELATIVE:
6683				/** @var \AttributeExternalKey $oAttDef */
6684				$oAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $this->Get("extkey_attcode"));
6685
6686				return $oAttDef;
6687
6688			default:
6689				throw new CoreException("Unexpected value for argument iType: '$iType'");
6690		}
6691	}
6692
6693	public function GetPrerequisiteAttributes($sClass = null)
6694	{
6695		return array($this->Get("extkey_attcode"));
6696	}
6697
6698
6699	/**
6700	 * @return \AttributeExternalField
6701	 * @throws \CoreException
6702	 * @throws \Exception
6703	 */
6704	public function GetExtAttDef()
6705	{
6706		$oKeyAttDef = $this->GetKeyAttDef();
6707		/** @var \AttributeExternalField $oExtAttDef */
6708		$oExtAttDef = MetaModel::GetAttributeDef($oKeyAttDef->GetTargetClass(), $this->Get("target_attcode"));
6709		if (!is_object($oExtAttDef))
6710		{
6711			throw new CoreException("Invalid external field ".$this->GetCode()." in class ".$this->GetHostClass().". The class ".$oKeyAttDef->GetTargetClass()." has no attribute ".$this->Get("target_attcode"));
6712		}
6713
6714		return $oExtAttDef;
6715	}
6716
6717	/**
6718	 * @return mixed
6719	 * @throws \CoreException
6720	 */
6721	public function GetSQLExpr()
6722	{
6723		$oExtAttDef = $this->GetExtAttDef();
6724
6725		return $oExtAttDef->GetSQLExpr();
6726	}
6727
6728	public function GetDefaultValue(DBObject $oHostObject = null)
6729	{
6730		$oExtAttDef = $this->GetExtAttDef();
6731
6732		return $oExtAttDef->GetDefaultValue();
6733	}
6734
6735	public function IsNullAllowed()
6736	{
6737		$oExtAttDef = $this->GetExtAttDef();
6738
6739		return $oExtAttDef->IsNullAllowed();
6740	}
6741
6742	static public function IsScalar()
6743	{
6744		return true;
6745	}
6746
6747	public function GetFilterDefinitions()
6748	{
6749		return array($this->GetCode() => new FilterFromAttribute($this));
6750	}
6751
6752	public function GetBasicFilterOperators()
6753	{
6754		$oExtAttDef = $this->GetExtAttDef();
6755
6756		return $oExtAttDef->GetBasicFilterOperators();
6757	}
6758
6759	public function GetBasicFilterLooseOperator()
6760	{
6761		$oExtAttDef = $this->GetExtAttDef();
6762
6763		return $oExtAttDef->GetBasicFilterLooseOperator();
6764	}
6765
6766	public function GetBasicFilterSQLExpr($sOpCode, $value)
6767	{
6768		$oExtAttDef = $this->GetExtAttDef();
6769
6770		return $oExtAttDef->GetBasicFilterSQLExpr($sOpCode, $value);
6771	}
6772
6773	public function GetNullValue()
6774	{
6775		$oExtAttDef = $this->GetExtAttDef();
6776
6777		return $oExtAttDef->GetNullValue();
6778	}
6779
6780	public function IsNull($proposedValue)
6781	{
6782		$oExtAttDef = $this->GetExtAttDef();
6783
6784		return $oExtAttDef->IsNull($proposedValue);
6785	}
6786
6787	public function MakeRealValue($proposedValue, $oHostObj)
6788	{
6789		$oExtAttDef = $this->GetExtAttDef();
6790
6791		return $oExtAttDef->MakeRealValue($proposedValue, $oHostObj);
6792	}
6793
6794	public function ScalarToSQL($value)
6795	{
6796		// This one could be used in case of filtering only
6797		$oExtAttDef = $this->GetExtAttDef();
6798
6799		return $oExtAttDef->ScalarToSQL($value);
6800	}
6801
6802
6803	// Do not overload GetSQLExpression here because this is handled in the joins
6804	//public function GetSQLExpressions($sPrefix = '') {return array();}
6805
6806	// Here, we get the data...
6807	public function FromSQLToValue($aCols, $sPrefix = '')
6808	{
6809		$oExtAttDef = $this->GetExtAttDef();
6810
6811		return $oExtAttDef->FromSQLToValue($aCols, $sPrefix);
6812	}
6813
6814	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
6815	{
6816		$oExtAttDef = $this->GetExtAttDef();
6817
6818		return $oExtAttDef->GetAsHTML($value, null, $bLocalize);
6819	}
6820
6821	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
6822	{
6823		$oExtAttDef = $this->GetExtAttDef();
6824
6825		return $oExtAttDef->GetAsXML($value, null, $bLocalize);
6826	}
6827
6828	public function GetAsCSV(
6829		$value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null, $bLocalize = true,
6830		$bConvertToPlainText = false
6831	) {
6832		$oExtAttDef = $this->GetExtAttDef();
6833
6834		return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier, null, $bLocalize, $bConvertToPlainText);
6835	}
6836
6837	static public function GetFormFieldClass()
6838	{
6839		return '\\Combodo\\iTop\\Form\\Field\\LabelField';
6840	}
6841
6842	/**
6843	 * @param \DBObject $oObject
6844	 * @param \Combodo\iTop\Form\Field\Field $oFormField
6845	 *
6846	 * @return null
6847	 * @throws \CoreException
6848	 */
6849	public function MakeFormField(DBObject $oObject, $oFormField = null)
6850	{
6851		// Retrieving AttDef from the remote attribute
6852		$oRemoteAttDef = $this->GetExtAttDef();
6853
6854		if ($oFormField === null)
6855		{
6856			// ExternalField's FormField are actually based on the FormField from the target attribute.
6857			// Except for the AttributeExternalKey because we have no OQL and stuff
6858			if ($oRemoteAttDef instanceof AttributeExternalKey)
6859			{
6860				$sFormFieldClass = static::GetFormFieldClass();
6861			}
6862			else
6863			{
6864				$sFormFieldClass = $oRemoteAttDef::GetFormFieldClass();
6865			}
6866			$oFormField = new $sFormFieldClass($this->GetCode());
6867		}
6868		parent::MakeFormField($oObject, $oFormField);
6869
6870		// Manually setting for remote ExternalKey, otherwise, the id would be displayed.
6871		if ($oRemoteAttDef instanceof AttributeExternalKey)
6872		{
6873			$oFormField->SetCurrentValue($oObject->Get($this->GetCode().'_friendlyname'));
6874		}
6875
6876		// Readonly field because we can't update external fields
6877		$oFormField->SetReadOnly(true);
6878
6879		return $oFormField;
6880	}
6881
6882	public function IsPartOfFingerprint()
6883	{
6884		return false;
6885	}
6886}
6887
6888
6889/**
6890 * Map a varchar column to an URL (formats the ouput in HMTL)
6891 *
6892 * @package     iTopORM
6893 */
6894class AttributeURL extends AttributeString
6895{
6896	static public function ListExpectedParams()
6897	{
6898		//return parent::ListExpectedParams();
6899		return array_merge(parent::ListExpectedParams(), array("target"));
6900	}
6901
6902	protected function GetSQLCol($bFullSpec = false)
6903	{
6904		return "VARCHAR(2048)"
6905			.CMDBSource::GetSqlStringColumnDefinition()
6906			.($bFullSpec ? $this->GetSQLColSpec() : '');
6907	}
6908
6909	public function GetMaxSize()
6910	{
6911		return 2048;
6912	}
6913
6914	public function GetEditClass()
6915	{
6916		return "String";
6917	}
6918
6919	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
6920	{
6921		$sTarget = $this->Get("target");
6922		if (empty($sTarget))
6923		{
6924			$sTarget = "_blank";
6925		}
6926		$sLabel = Str::pure2html($sValue);
6927		if (strlen($sLabel) > 128)
6928		{
6929			// Truncate the length to 128 characters, by removing the middle
6930			$sLabel = substr($sLabel, 0, 100).'.....'.substr($sLabel, -20);
6931		}
6932
6933		return "<a target=\"$sTarget\" href=\"$sValue\">$sLabel</a>";
6934	}
6935
6936	public function GetValidationPattern()
6937	{
6938		return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('url_validation_pattern').'$');
6939	}
6940
6941	static public function GetFormFieldClass()
6942	{
6943		return '\\Combodo\\iTop\\Form\\Field\\UrlField';
6944	}
6945
6946	/**
6947	 * @param \DBObject $oObject
6948	 * @param  \Combodo\iTop\Form\Field\UrlField $oFormField
6949	 *
6950	 * @return null
6951	 * @throws \CoreException
6952	 */
6953	public function MakeFormField(DBObject $oObject, $oFormField = null)
6954	{
6955		if ($oFormField === null)
6956		{
6957			$sFormFieldClass = static::GetFormFieldClass();
6958			$oFormField = new $sFormFieldClass($this->GetCode());
6959		}
6960		parent::MakeFormField($oObject, $oFormField);
6961
6962		$oFormField->SetTarget($this->Get('target'));
6963
6964		return $oFormField;
6965	}
6966}
6967
6968/**
6969 * A blob is an ormDocument, it is stored as several columns in the database
6970 *
6971 * @package     iTopORM
6972 */
6973class AttributeBlob extends AttributeDefinition
6974{
6975	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
6976
6977	static public function ListExpectedParams()
6978	{
6979		return array_merge(parent::ListExpectedParams(), array("depends_on"));
6980	}
6981
6982	public function GetEditClass()
6983	{
6984		return "Document";
6985	}
6986
6987	static public function IsBasedOnDBColumns()
6988	{
6989		return true;
6990	}
6991
6992	static public function IsScalar()
6993	{
6994		return true;
6995	}
6996
6997	public function IsWritable()
6998	{
6999		return true;
7000	}
7001
7002	public function GetDefaultValue(DBObject $oHostObject = null)
7003	{
7004		return "";
7005	}
7006
7007	public function IsNullAllowed(DBObject $oHostObject = null)
7008	{
7009		return $this->GetOptional("is_null_allowed", false);
7010	}
7011
7012	public function GetEditValue($sValue, $oHostObj = null)
7013	{
7014		return '';
7015	}
7016
7017	/**
7018	 * Users can provide the document from an URL (including an URL on iTop itself)
7019	 * for CSV import. Administrators can even provide the path to a local file
7020	 * {@inheritDoc}
7021	 *
7022	 * @see AttributeDefinition::MakeRealValue()
7023	 */
7024	public function MakeRealValue($proposedValue, $oHostObj)
7025	{
7026		if ($proposedValue === null)
7027		{
7028			return null;
7029		}
7030
7031		if (is_object($proposedValue))
7032		{
7033			$proposedValue = clone $proposedValue;
7034		}
7035		else
7036		{
7037			try
7038			{
7039				// Read the file from iTop, an URL (or the local file system - for admins only)
7040				$proposedValue = Utils::FileGetContentsAndMIMEType($proposedValue);
7041			} catch (Exception $e)
7042			{
7043				IssueLog::Warning(get_class($this)."::MakeRealValue - ".$e->getMessage());
7044				// Not a real document !! store is as text !!! (This was the default behavior before)
7045				$proposedValue = new ormDocument($e->getMessage()." \n".$proposedValue, 'text/plain');
7046			}
7047		}
7048
7049		return $proposedValue;
7050	}
7051
7052	public function GetSQLExpressions($sPrefix = '')
7053	{
7054		if ($sPrefix == '')
7055		{
7056			$sPrefix = $this->GetCode();
7057		}
7058		$aColumns = array();
7059		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
7060		$aColumns[''] = $sPrefix.'_mimetype';
7061		$aColumns['_data'] = $sPrefix.'_data';
7062		$aColumns['_filename'] = $sPrefix.'_filename';
7063
7064		return $aColumns;
7065	}
7066
7067	public function FromSQLToValue($aCols, $sPrefix = '')
7068	{
7069		if (!array_key_exists($sPrefix, $aCols))
7070		{
7071			$sAvailable = implode(', ', array_keys($aCols));
7072			throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
7073		}
7074		$sMimeType = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : '';
7075
7076		if (!array_key_exists($sPrefix.'_data', $aCols))
7077		{
7078			$sAvailable = implode(', ', array_keys($aCols));
7079			throw new MissingColumnException("Missing column '".$sPrefix."_data' from {$sAvailable}");
7080		}
7081		$data = isset($aCols[$sPrefix.'_data']) ? $aCols[$sPrefix.'_data'] : null;
7082
7083		if (!array_key_exists($sPrefix.'_filename', $aCols))
7084		{
7085			$sAvailable = implode(', ', array_keys($aCols));
7086			throw new MissingColumnException("Missing column '".$sPrefix."_filename' from {$sAvailable}");
7087		}
7088		$sFileName = isset($aCols[$sPrefix.'_filename']) ? $aCols[$sPrefix.'_filename'] : '';
7089
7090		$value = new ormDocument($data, $sMimeType, $sFileName);
7091
7092		return $value;
7093	}
7094
7095	public function GetSQLValues($value)
7096	{
7097		// #@# Optimization: do not load blobs anytime
7098		//	 As per mySQL doc, selecting blob columns will prevent mySQL from
7099		//	 using memory in case a temporary table has to be created
7100		//	 (temporary tables created on disk)
7101		//	 We will have to remove the blobs from the list of attributes when doing the select
7102		//	 then the use of Get() should finalize the load
7103		if ($value instanceOf ormDocument && !$value->IsEmpty())
7104		{
7105			$aValues = array();
7106			$aValues[$this->GetCode().'_data'] = $value->GetData();
7107			$aValues[$this->GetCode().'_mimetype'] = $value->GetMimeType();
7108			$aValues[$this->GetCode().'_filename'] = $value->GetFileName();
7109		}
7110		else
7111		{
7112			$aValues = array();
7113			$aValues[$this->GetCode().'_data'] = '';
7114			$aValues[$this->GetCode().'_mimetype'] = '';
7115			$aValues[$this->GetCode().'_filename'] = '';
7116		}
7117
7118		return $aValues;
7119	}
7120
7121	public function GetSQLColumns($bFullSpec = false)
7122	{
7123		$aColumns = array();
7124		$aColumns[$this->GetCode().'_data'] = 'LONGBLOB'; // 2^32 (4 Gb)
7125		$aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition();
7126		$aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition();
7127
7128		return $aColumns;
7129	}
7130
7131	public function GetFilterDefinitions()
7132	{
7133		return array();
7134	}
7135
7136	public function GetBasicFilterOperators()
7137	{
7138		return array();
7139	}
7140
7141	public function GetBasicFilterLooseOperator()
7142	{
7143		return '=';
7144	}
7145
7146	public function GetBasicFilterSQLExpr($sOpCode, $value)
7147	{
7148		return 'true';
7149	}
7150
7151	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
7152	{
7153		if (is_object($value))
7154		{
7155			return $value->GetAsHTML();
7156		}
7157
7158		return '';
7159	}
7160
7161	/**
7162	 * @param string $sValue
7163	 * @param string $sSeparator
7164	 * @param string $sTextQualifier
7165	 * @param \DBObject $oHostObject
7166	 * @param bool $bLocalize
7167	 * @param bool $bConvertToPlainText
7168	 *
7169	 * @return string
7170	 */
7171	public function GetAsCSV(
7172		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
7173		$bConvertToPlainText = false
7174	) {
7175		$sAttCode = $this->GetCode();
7176		if ($sValue instanceof ormDocument && !$sValue->IsEmpty())
7177		{
7178			return $sValue->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $sAttCode);
7179		}
7180
7181		return ''; // Not exportable in CSV !
7182	}
7183
7184	/**
7185	 * @param $value
7186	 * @param \DBObject $oHostObject
7187	 * @param bool $bLocalize
7188	 *
7189	 * @return mixed|string
7190	 */
7191	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
7192	{
7193		$sRet = '';
7194		if (is_object($value))
7195		{
7196			if (!$value->IsEmpty())
7197			{
7198				$sRet = '<mimetype>'.$value->GetMimeType().'</mimetype>';
7199				$sRet .= '<filename>'.$value->GetFileName().'</filename>';
7200				$sRet .= '<data>'.base64_encode($value->GetData()).'</data>';
7201			}
7202		}
7203
7204		return $sRet;
7205	}
7206
7207	/**
7208	 * Helper to get a value that will be JSON encoded
7209	 * The operation is the opposite to FromJSONToValue
7210	 */
7211	public function GetForJSON($value)
7212	{
7213		if ($value instanceOf ormDocument)
7214		{
7215			$aValues = array();
7216			$aValues['data'] = base64_encode($value->GetData());
7217			$aValues['mimetype'] = $value->GetMimeType();
7218			$aValues['filename'] = $value->GetFileName();
7219		}
7220		else
7221		{
7222			$aValues = null;
7223		}
7224
7225		return $aValues;
7226	}
7227
7228	/**
7229	 * Helper to form a value, given JSON decoded data
7230	 * The operation is the opposite to GetForJSON
7231	 */
7232	public function FromJSONToValue($json)
7233	{
7234		if (isset($json->data))
7235		{
7236			$data = base64_decode($json->data);
7237			$value = new ormDocument($data, $json->mimetype, $json->filename);
7238		}
7239		else
7240		{
7241			$value = null;
7242		}
7243
7244		return $value;
7245	}
7246
7247	public function Fingerprint($value)
7248	{
7249		$sFingerprint = '';
7250		if ($value instanceOf ormDocument)
7251		{
7252			$sFingerprint = md5($value->GetData());
7253		}
7254
7255		return $sFingerprint;
7256	}
7257
7258	static public function GetFormFieldClass()
7259	{
7260		return '\\Combodo\\iTop\\Form\\Field\\BlobField';
7261	}
7262
7263	public function MakeFormField(DBObject $oObject, $oFormField = null)
7264	{
7265		if ($oFormField === null)
7266		{
7267			$sFormFieldClass = static::GetFormFieldClass();
7268			$oFormField = new $sFormFieldClass($this->GetCode());
7269		}
7270
7271		// Note: As of today we want this field to always be read-only
7272		$oFormField->SetReadOnly(true);
7273
7274		// Generating urls
7275		$value = $oObject->Get($this->GetCode());
7276		$oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode()));
7277		$oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode()));
7278
7279		parent::MakeFormField($oObject, $oFormField);
7280
7281		return $oFormField;
7282	}
7283
7284}
7285
7286/**
7287 * An image is a specific type of document, it is stored as several columns in the database
7288 *
7289 * @package     iTopORM
7290 */
7291class AttributeImage extends AttributeBlob
7292{
7293	public function GetEditClass()
7294	{
7295		return "Image";
7296	}
7297
7298	/**
7299	 * {@inheritDoc}
7300	 * @see AttributeBlob::MakeRealValue()
7301	 */
7302	public function MakeRealValue($proposedValue, $oHostObj)
7303	{
7304		$oDoc = parent::MakeRealValue($proposedValue, $oHostObj);
7305
7306		// The validation of the MIME Type is done by CheckFormat below
7307		return $oDoc;
7308	}
7309
7310	/**
7311	 * Check that the supplied ormDocument actually contains an image
7312	 * {@inheritDoc}
7313	 *
7314	 * @see AttributeDefinition::CheckFormat()
7315	 */
7316	public function CheckFormat($value)
7317	{
7318		if ($value instanceof ormDocument && !$value->IsEmpty())
7319		{
7320			return ($value->GetMainMimeType() == 'image');
7321		}
7322
7323		return true;
7324	}
7325
7326	/**
7327	 * @param \ormDocument $value
7328	 * @param \DBObject $oHostObject
7329	 * @param bool $bLocalize
7330	 *
7331	 * @return string
7332	 *
7333	 * @see edit_image.js for JS generated markup in form edition
7334	 */
7335	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
7336	{
7337		$sRet = '';
7338		$bIsCustomImage = false;
7339
7340		$iMaxWidthPx = $this->Get('display_max_width').'px';
7341		$iMaxHeightPx = $this->Get('display_max_height').'px';
7342
7343		$sDefaultImageUrl = $this->Get('default_image');
7344		if ($sDefaultImageUrl !== null) {
7345			$sRet = $this->GetHtmlForImageUrl($sDefaultImageUrl, $iMaxWidthPx, $iMaxHeightPx);
7346		}
7347
7348		$sCustomImageUrl = $this->GetAttributeImageFileUrl($value, $oHostObject);
7349		if ($sCustomImageUrl !== null) {
7350			$bIsCustomImage = true;
7351			$sRet = $this->GetHtmlForImageUrl($sCustomImageUrl, $iMaxWidthPx, $iMaxHeightPx);
7352		}
7353
7354		$sCssClasses = 'view-image attribute-image';
7355		$sCssClasses .= ' '.(($bIsCustomImage) ? 'attribute-image-custom' : 'attribute-image-default');
7356
7357		return '<div class="'.$sCssClasses.'" style="width: '.$iMaxWidthPx.'; height: '.$iMaxHeightPx.';"><span class="helper-middle"></span>'.$sRet.'</div>';
7358	}
7359
7360	private function GetHtmlForImageUrl($sUrl, $iMaxWidthPx, $iMaxHeightPx) {
7361		return  '<img src="'.$sUrl.'" style="max-width: '.$iMaxWidthPx.'; max-height: '.$iMaxHeightPx.'">';
7362	}
7363
7364	/**
7365	 * @param \ormDocument $value
7366	 * @param \DBObject $oHostObject
7367	 *
7368	 * @return null|string
7369	 */
7370	private function GetAttributeImageFileUrl($value, $oHostObject) {
7371		if (!is_object($value)) {
7372			return null;
7373		}
7374		if ($value->IsEmpty()) {
7375			return null;
7376		}
7377
7378		$bExistingImageModified = ($oHostObject->IsModified() && (array_key_exists($this->GetCode(), $oHostObject->ListChanges())));
7379		if ($oHostObject->IsNew() || ($bExistingImageModified))
7380		{
7381			// If the object is modified (or not yet stored in the database) we must serve the content of the image directly inline
7382			// otherwise (if we just give an URL) the browser will be given the wrong content... and may cache it
7383			return 'data:'.$value->GetMimeType().';base64,'.base64_encode($value->GetData());
7384		}
7385
7386		return $value->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $this->GetCode());
7387	}
7388
7389	static public function GetFormFieldClass()
7390	{
7391		return '\\Combodo\\iTop\\Form\\Field\\ImageField';
7392	}
7393
7394	public function MakeFormField(DBObject $oObject, $oFormField = null)
7395	{
7396		if ($oFormField === null)
7397		{
7398			$sFormFieldClass = static::GetFormFieldClass();
7399			$oFormField = new $sFormFieldClass($this->GetCode());
7400		}
7401
7402		parent::MakeFormField($oObject, $oFormField);
7403
7404		// Generating urls
7405		$value = $oObject->Get($this->GetCode());
7406		if (is_object($value) && !$value->IsEmpty())
7407		{
7408			$oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode()));
7409			$oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(),	$this->GetCode()));
7410		}
7411		else
7412		{
7413			$oFormField->SetDownloadUrl($this->Get('default_image'));
7414			$oFormField->SetDisplayUrl($this->Get('default_image'));
7415		}
7416
7417		return $oFormField;
7418	}
7419}
7420
7421/**
7422 * A stop watch is an ormStopWatch object, it is stored as several columns in the database
7423 *
7424 * @package     iTopORM
7425 */
7426class AttributeStopWatch extends AttributeDefinition
7427{
7428	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
7429
7430	static public function ListExpectedParams()
7431	{
7432		// The list of thresholds must be an array of iPercent => array of 'option' => value
7433		return array_merge(parent::ListExpectedParams(),
7434			array("states", "goal_computing", "working_time_computing", "thresholds"));
7435	}
7436
7437	public function GetEditClass()
7438	{
7439		return "StopWatch";
7440	}
7441
7442	static public function IsBasedOnDBColumns()
7443	{
7444		return true;
7445	}
7446
7447	static public function IsScalar()
7448	{
7449		return true;
7450	}
7451
7452	public function IsWritable()
7453	{
7454		return true;
7455	}
7456
7457	public function GetDefaultValue(DBObject $oHostObject = null)
7458	{
7459		return $this->NewStopWatch();
7460	}
7461
7462	/**
7463	 * @param \ormStopWatch $value
7464	 * @param \DBObject $oHostObj
7465	 *
7466	 * @return string
7467	 */
7468	public function GetEditValue($value, $oHostObj = null)
7469	{
7470		return $value->GetTimeSpent();
7471	}
7472
7473	public function GetStates()
7474	{
7475		return $this->Get('states');
7476	}
7477
7478	public function AlwaysLoadInTables()
7479	{
7480		// Each and every stop watch is accessed for computing the highlight code (DBObject::GetHighlightCode())
7481		return true;
7482	}
7483
7484	/**
7485	 * Construct a brand new (but configured) stop watch
7486	 */
7487	public function NewStopWatch()
7488	{
7489		$oSW = new ormStopWatch();
7490		foreach($this->ListThresholds() as $iThreshold => $aFoo)
7491		{
7492			$oSW->DefineThreshold($iThreshold);
7493		}
7494
7495		return $oSW;
7496	}
7497
7498	// Facilitate things: allow the user to Set the value from a string
7499	public function MakeRealValue($proposedValue, $oHostObj)
7500	{
7501		if (!$proposedValue instanceof ormStopWatch)
7502		{
7503			return $this->NewStopWatch();
7504		}
7505
7506		return $proposedValue;
7507	}
7508
7509	public function GetSQLExpressions($sPrefix = '')
7510	{
7511		if ($sPrefix == '')
7512		{
7513			$sPrefix = $this->GetCode(); // Warning: a stopwatch does not have any 'sql' property, so its SQL column is equal to its attribute code !!
7514		}
7515		$aColumns = array();
7516		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
7517		$aColumns[''] = $sPrefix.'_timespent';
7518		$aColumns['_started'] = $sPrefix.'_started';
7519		$aColumns['_laststart'] = $sPrefix.'_laststart';
7520		$aColumns['_stopped'] = $sPrefix.'_stopped';
7521		foreach($this->ListThresholds() as $iThreshold => $aFoo)
7522		{
7523			$sThPrefix = '_'.$iThreshold;
7524			$aColumns[$sThPrefix.'_deadline'] = $sPrefix.$sThPrefix.'_deadline';
7525			$aColumns[$sThPrefix.'_passed'] = $sPrefix.$sThPrefix.'_passed';
7526			$aColumns[$sThPrefix.'_triggered'] = $sPrefix.$sThPrefix.'_triggered';
7527			$aColumns[$sThPrefix.'_overrun'] = $sPrefix.$sThPrefix.'_overrun';
7528		}
7529
7530		return $aColumns;
7531	}
7532
7533	public static function DateToSeconds($sDate)
7534	{
7535		if (is_null($sDate))
7536		{
7537			return null;
7538		}
7539		$oDateTime = new DateTime($sDate);
7540		$iSeconds = $oDateTime->format('U');
7541
7542		return $iSeconds;
7543	}
7544
7545	public static function SecondsToDate($iSeconds)
7546	{
7547		if (is_null($iSeconds))
7548		{
7549			return null;
7550		}
7551
7552		return date("Y-m-d H:i:s", $iSeconds);
7553	}
7554
7555	public function FromSQLToValue($aCols, $sPrefix = '')
7556	{
7557		$aExpectedCols = array($sPrefix, $sPrefix.'_started', $sPrefix.'_laststart', $sPrefix.'_stopped');
7558		foreach($this->ListThresholds() as $iThreshold => $aFoo)
7559		{
7560			$sThPrefix = '_'.$iThreshold;
7561			$aExpectedCols[] = $sPrefix.$sThPrefix.'_deadline';
7562			$aExpectedCols[] = $sPrefix.$sThPrefix.'_passed';
7563			$aExpectedCols[] = $sPrefix.$sThPrefix.'_triggered';
7564			$aExpectedCols[] = $sPrefix.$sThPrefix.'_overrun';
7565		}
7566		foreach($aExpectedCols as $sExpectedCol)
7567		{
7568			if (!array_key_exists($sExpectedCol, $aCols))
7569			{
7570				$sAvailable = implode(', ', array_keys($aCols));
7571				throw new MissingColumnException("Missing column '$sExpectedCol' from {$sAvailable}");
7572			}
7573		}
7574
7575		$value = new ormStopWatch(
7576			$aCols[$sPrefix],
7577			self::DateToSeconds($aCols[$sPrefix.'_started']),
7578			self::DateToSeconds($aCols[$sPrefix.'_laststart']),
7579			self::DateToSeconds($aCols[$sPrefix.'_stopped'])
7580		);
7581
7582		foreach($this->ListThresholds() as $iThreshold => $aDefinition)
7583		{
7584			$sThPrefix = '_'.$iThreshold;
7585			$value->DefineThreshold(
7586				$iThreshold,
7587				self::DateToSeconds($aCols[$sPrefix.$sThPrefix.'_deadline']),
7588				(bool)($aCols[$sPrefix.$sThPrefix.'_passed'] == 1),
7589				(bool)($aCols[$sPrefix.$sThPrefix.'_triggered'] == 1),
7590				$aCols[$sPrefix.$sThPrefix.'_overrun'],
7591				array_key_exists('highlight', $aDefinition) ? $aDefinition['highlight'] : null
7592			);
7593		}
7594
7595		return $value;
7596	}
7597
7598	public function GetSQLValues($value)
7599	{
7600		if ($value instanceOf ormStopWatch)
7601		{
7602			$aValues = array();
7603			$aValues[$this->GetCode().'_timespent'] = $value->GetTimeSpent();
7604			$aValues[$this->GetCode().'_started'] = self::SecondsToDate($value->GetStartDate());
7605			$aValues[$this->GetCode().'_laststart'] = self::SecondsToDate($value->GetLastStartDate());
7606			$aValues[$this->GetCode().'_stopped'] = self::SecondsToDate($value->GetStopDate());
7607
7608			foreach($this->ListThresholds() as $iThreshold => $aFoo)
7609			{
7610				$sPrefix = $this->GetCode().'_'.$iThreshold;
7611				$aValues[$sPrefix.'_deadline'] = self::SecondsToDate($value->GetThresholdDate($iThreshold));
7612				$aValues[$sPrefix.'_passed'] = $value->IsThresholdPassed($iThreshold) ? '1' : '0';
7613				$aValues[$sPrefix.'_triggered'] = $value->IsThresholdTriggered($iThreshold) ? '1' : '0';
7614				$aValues[$sPrefix.'_overrun'] = $value->GetOverrun($iThreshold);
7615			}
7616		}
7617		else
7618		{
7619			$aValues = array();
7620			$aValues[$this->GetCode().'_timespent'] = '';
7621			$aValues[$this->GetCode().'_started'] = '';
7622			$aValues[$this->GetCode().'_laststart'] = '';
7623			$aValues[$this->GetCode().'_stopped'] = '';
7624		}
7625
7626		return $aValues;
7627	}
7628
7629	public function GetSQLColumns($bFullSpec = false)
7630	{
7631		$aColumns = array();
7632		$aColumns[$this->GetCode().'_timespent'] = 'INT(11) UNSIGNED';
7633		$aColumns[$this->GetCode().'_started'] = 'DATETIME';
7634		$aColumns[$this->GetCode().'_laststart'] = 'DATETIME';
7635		$aColumns[$this->GetCode().'_stopped'] = 'DATETIME';
7636		foreach($this->ListThresholds() as $iThreshold => $aFoo)
7637		{
7638			$sPrefix = $this->GetCode().'_'.$iThreshold;
7639			$aColumns[$sPrefix.'_deadline'] = 'DATETIME';
7640			$aColumns[$sPrefix.'_passed'] = 'TINYINT(1) UNSIGNED';
7641			$aColumns[$sPrefix.'_triggered'] = 'TINYINT(1)';
7642			$aColumns[$sPrefix.'_overrun'] = 'INT(11) UNSIGNED';
7643		}
7644
7645		return $aColumns;
7646	}
7647
7648	public function GetFilterDefinitions()
7649	{
7650		$aRes = array(
7651			$this->GetCode() => new FilterFromAttribute($this),
7652			$this->GetCode().'_started' => new FilterFromAttribute($this, '_started'),
7653			$this->GetCode().'_laststart' => new FilterFromAttribute($this, '_laststart'),
7654			$this->GetCode().'_stopped' => new FilterFromAttribute($this, '_stopped')
7655		);
7656		foreach($this->ListThresholds() as $iThreshold => $aFoo)
7657		{
7658			$sPrefix = $this->GetCode().'_'.$iThreshold;
7659			$aRes[$sPrefix.'_deadline'] = new FilterFromAttribute($this, '_deadline');
7660			$aRes[$sPrefix.'_passed'] = new FilterFromAttribute($this, '_passed');
7661			$aRes[$sPrefix.'_triggered'] = new FilterFromAttribute($this, '_triggered');
7662			$aRes[$sPrefix.'_overrun'] = new FilterFromAttribute($this, '_overrun');
7663		}
7664
7665		return $aRes;
7666	}
7667
7668	public function GetBasicFilterOperators()
7669	{
7670		return array();
7671	}
7672
7673	public function GetBasicFilterLooseOperator()
7674	{
7675		return '=';
7676	}
7677
7678	public function GetBasicFilterSQLExpr($sOpCode, $value)
7679	{
7680		return 'true';
7681	}
7682
7683	/**
7684	 * @param \ormStopWatch $value
7685	 * @param \DBObject $oHostObject
7686	 * @param bool $bLocalize
7687	 *
7688	 * @return string
7689	 */
7690	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
7691	{
7692		if (is_object($value))
7693		{
7694			return $value->GetAsHTML($this, $oHostObject);
7695		}
7696
7697		return '';
7698	}
7699
7700	/**
7701	 * @param ormStopWatch $value
7702	 * @param string $sSeparator
7703	 * @param string $sTextQualifier
7704	 * @param null $oHostObject
7705	 * @param bool $bLocalize
7706	 * @param bool $bConvertToPlainText
7707	 *
7708	 * @return string
7709	 */
7710	public function GetAsCSV(
7711		$value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
7712		$bConvertToPlainText = false
7713	) {
7714		return $value->GetTimeSpent();
7715	}
7716
7717	/**
7718	 * @param \ormStopWatch $value
7719	 * @param \DBObject $oHostObject
7720	 * @param bool $bLocalize
7721	 *
7722	 * @return mixed
7723	 */
7724	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
7725	{
7726		return $value->GetTimeSpent();
7727	}
7728
7729	public function ListThresholds()
7730	{
7731		return $this->Get('thresholds');
7732	}
7733
7734	public function Fingerprint($value)
7735	{
7736		$sFingerprint = '';
7737		if (is_object($value))
7738		{
7739			$sFingerprint = $value->GetAsHTML($this);
7740		}
7741
7742		return $sFingerprint;
7743	}
7744
7745	/**
7746	 * To expose internal values: Declare an attribute AttributeSubItem
7747	 * and implement the GetSubItemXXXX verbs
7748	 *
7749	 * @param string $sItemCode
7750	 *
7751	 * @return array
7752	 * @throws \CoreException
7753	 */
7754	public function GetSubItemSQLExpression($sItemCode)
7755	{
7756		$sPrefix = $this->GetCode();
7757		switch ($sItemCode)
7758		{
7759			case 'timespent':
7760				return array('' => $sPrefix.'_timespent');
7761			case 'started':
7762				return array('' => $sPrefix.'_started');
7763			case 'laststart':
7764				return array('' => $sPrefix.'_laststart');
7765			case 'stopped':
7766				return array('' => $sPrefix.'_stopped');
7767		}
7768
7769		foreach($this->ListThresholds() as $iThreshold => $aFoo)
7770		{
7771			$sThPrefix = $iThreshold.'_';
7772			if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
7773			{
7774				// The current threshold is concerned
7775				$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
7776				switch ($sThresholdCode)
7777				{
7778					case 'deadline':
7779						return array('' => $sPrefix.'_'.$iThreshold.'_deadline');
7780					case 'passed':
7781						return array('' => $sPrefix.'_'.$iThreshold.'_passed');
7782					case 'triggered':
7783						return array('' => $sPrefix.'_'.$iThreshold.'_triggered');
7784					case 'overrun':
7785						return array('' => $sPrefix.'_'.$iThreshold.'_overrun');
7786				}
7787			}
7788		}
7789		throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode());
7790	}
7791
7792	/**
7793	 * @param string $sItemCode
7794	 * @param \ormStopWatch $value
7795	 * @param \DBObject $oHostObject
7796	 *
7797	 * @return mixed
7798	 * @throws \CoreException
7799	 */
7800	public function GetSubItemValue($sItemCode, $value, $oHostObject = null)
7801	{
7802		$oStopWatch = $value;
7803		switch ($sItemCode)
7804		{
7805			case 'timespent':
7806				return $oStopWatch->GetTimeSpent();
7807			case 'started':
7808				return $oStopWatch->GetStartDate();
7809			case 'laststart':
7810				return $oStopWatch->GetLastStartDate();
7811			case 'stopped':
7812				return $oStopWatch->GetStopDate();
7813		}
7814
7815		foreach($this->ListThresholds() as $iThreshold => $aFoo)
7816		{
7817			$sThPrefix = $iThreshold.'_';
7818			if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
7819			{
7820				// The current threshold is concerned
7821				$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
7822				switch ($sThresholdCode)
7823				{
7824					case 'deadline':
7825						return $oStopWatch->GetThresholdDate($iThreshold);
7826					case 'passed':
7827						return $oStopWatch->IsThresholdPassed($iThreshold);
7828					case 'triggered':
7829						return $oStopWatch->IsThresholdTriggered($iThreshold);
7830					case 'overrun':
7831						return $oStopWatch->GetOverrun($iThreshold);
7832				}
7833			}
7834		}
7835
7836		throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode());
7837	}
7838
7839	protected function GetBooleanLabel($bValue)
7840	{
7841		$sDictKey = $bValue ? 'yes' : 'no';
7842
7843		return Dict::S('BooleanLabel:'.$sDictKey, 'def:'.$sDictKey);
7844	}
7845
7846	public function GetSubItemAsHTMLForHistory($sItemCode, $sValue)
7847	{
7848		$sHtml = null;
7849		switch ($sItemCode)
7850		{
7851			case 'timespent':
7852				$sHtml = (int)$sValue ? Str::pure2html(AttributeDuration::FormatDuration($sValue)) : null;
7853				break;
7854			case 'started':
7855			case 'laststart':
7856			case 'stopped':
7857				$sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(), (int)$sValue) : null;
7858				break;
7859
7860			default:
7861				foreach($this->ListThresholds() as $iThreshold => $aFoo)
7862				{
7863					$sThPrefix = $iThreshold.'_';
7864					if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
7865					{
7866						// The current threshold is concerned
7867						$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
7868						switch ($sThresholdCode)
7869						{
7870							case 'deadline':
7871								$sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(),
7872									(int)$sValue) : null;
7873								break;
7874							case 'passed':
7875								$sHtml = $this->GetBooleanLabel((int)$sValue);
7876								break;
7877							case 'triggered':
7878								$sHtml = $this->GetBooleanLabel((int)$sValue);
7879								break;
7880							case 'overrun':
7881								$sHtml = (int)$sValue > 0 ? Str::pure2html(AttributeDuration::FormatDuration((int)$sValue)) : '';
7882						}
7883					}
7884				}
7885		}
7886
7887		return $sHtml;
7888	}
7889
7890	public function GetSubItemAsPlainText($sItemCode, $value)
7891	{
7892		$sRet = $value;
7893
7894		switch ($sItemCode)
7895		{
7896			case 'timespent':
7897				$sRet = AttributeDuration::FormatDuration($value);
7898				break;
7899			case 'started':
7900			case 'laststart':
7901			case 'stopped':
7902				if (is_null($value))
7903				{
7904					$sRet = ''; // Undefined
7905				}
7906				else
7907				{
7908					$oDateTime = new DateTime();
7909					$oDateTime->setTimestamp($value);
7910					$oDateTimeFormat = AttributeDateTime::GetFormat();
7911					$sRet = $oDateTimeFormat->Format($oDateTime);
7912				}
7913				break;
7914
7915			default:
7916				foreach($this->ListThresholds() as $iThreshold => $aFoo)
7917				{
7918					$sThPrefix = $iThreshold.'_';
7919					if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
7920					{
7921						// The current threshold is concerned
7922						$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
7923						switch ($sThresholdCode)
7924						{
7925							case 'deadline':
7926								if ($value)
7927								{
7928									$sDate = date(AttributeDateTime::GetInternalFormat(), $value);
7929									$sRet = AttributeDeadline::FormatDeadline($sDate);
7930								}
7931								else
7932								{
7933									$sRet = '';
7934								}
7935								break;
7936							case 'passed':
7937							case 'triggered':
7938								$sRet = $this->GetBooleanLabel($value);
7939								break;
7940							case 'overrun':
7941								$sRet = AttributeDuration::FormatDuration($value);
7942								break;
7943						}
7944					}
7945				}
7946		}
7947
7948		return $sRet;
7949	}
7950
7951	public function GetSubItemAsHTML($sItemCode, $value)
7952	{
7953		$sHtml = $value;
7954
7955		switch ($sItemCode)
7956		{
7957			case 'timespent':
7958				$sHtml = Str::pure2html(AttributeDuration::FormatDuration($value));
7959				break;
7960			case 'started':
7961			case 'laststart':
7962			case 'stopped':
7963				if (is_null($value))
7964				{
7965					$sHtml = ''; // Undefined
7966				}
7967				else
7968				{
7969					$oDateTime = new DateTime();
7970					$oDateTime->setTimestamp($value);
7971					$oDateTimeFormat = AttributeDateTime::GetFormat();
7972					$sHtml = Str::pure2html($oDateTimeFormat->Format($oDateTime));
7973				}
7974				break;
7975
7976			default:
7977				foreach($this->ListThresholds() as $iThreshold => $aFoo)
7978				{
7979					$sThPrefix = $iThreshold.'_';
7980					if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
7981					{
7982						// The current threshold is concerned
7983						$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
7984						switch ($sThresholdCode)
7985						{
7986							case 'deadline':
7987								if ($value)
7988								{
7989									$sDate = date(AttributeDateTime::GetInternalFormat(), $value);
7990									$sHtml = Str::pure2html(AttributeDeadline::FormatDeadline($sDate));
7991								}
7992								else
7993								{
7994									$sHtml = '';
7995								}
7996								break;
7997							case 'passed':
7998							case 'triggered':
7999								$sHtml = $this->GetBooleanLabel($value);
8000								break;
8001							case 'overrun':
8002								$sHtml = Str::pure2html(AttributeDuration::FormatDuration($value));
8003								break;
8004						}
8005					}
8006				}
8007		}
8008
8009		return $sHtml;
8010	}
8011
8012	public function GetSubItemAsCSV(
8013		$sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"', $bConvertToPlainText = false
8014	) {
8015		$sFrom = array("\r\n", $sTextQualifier);
8016		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
8017		$sEscaped = str_replace($sFrom, $sTo, (string)$value);
8018		$sRet = $sTextQualifier.$sEscaped.$sTextQualifier;
8019
8020		switch ($sItemCode)
8021		{
8022			case 'timespent':
8023				$sRet = $sTextQualifier.AttributeDuration::FormatDuration($value).$sTextQualifier;
8024				break;
8025			case 'started':
8026			case 'laststart':
8027			case 'stopped':
8028				if ($value !== null)
8029				{
8030					$oDateTime = new DateTime();
8031					$oDateTime->setTimestamp($value);
8032					$oDateTimeFormat = AttributeDateTime::GetFormat();
8033					$sRet = $sTextQualifier.$oDateTimeFormat->Format($oDateTime).$sTextQualifier;
8034				}
8035				break;
8036
8037			default:
8038				foreach($this->ListThresholds() as $iThreshold => $aFoo)
8039				{
8040					$sThPrefix = $iThreshold.'_';
8041					if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
8042					{
8043						// The current threshold is concerned
8044						$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
8045						switch ($sThresholdCode)
8046						{
8047							case 'deadline':
8048								if ($value != '')
8049								{
8050									$oDateTime = new DateTime();
8051									$oDateTime->setTimestamp($value);
8052									$oDateTimeFormat = AttributeDateTime::GetFormat();
8053									$sRet = $sTextQualifier.$oDateTimeFormat->Format($oDateTime).$sTextQualifier;
8054								}
8055								break;
8056
8057							case 'passed':
8058							case 'triggered':
8059								$sRet = $sTextQualifier.$this->GetBooleanLabel($value).$sTextQualifier;
8060								break;
8061
8062							case 'overrun':
8063								$sRet = $sTextQualifier.AttributeDuration::FormatDuration($value).$sTextQualifier;
8064								break;
8065						}
8066					}
8067				}
8068		}
8069
8070		return $sRet;
8071	}
8072
8073	public function GetSubItemAsXML($sItemCode, $value)
8074	{
8075		$sRet = Str::pure2xml((string)$value);
8076
8077		switch ($sItemCode)
8078		{
8079			case 'timespent':
8080			case 'started':
8081			case 'laststart':
8082			case 'stopped':
8083				break;
8084
8085			default:
8086				foreach($this->ListThresholds() as $iThreshold => $aFoo)
8087				{
8088					$sThPrefix = $iThreshold.'_';
8089					if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
8090					{
8091						// The current threshold is concerned
8092						$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
8093						switch ($sThresholdCode)
8094						{
8095							case 'deadline':
8096								break;
8097
8098							case 'passed':
8099							case 'triggered':
8100								$sRet = $this->GetBooleanLabel($value);
8101								break;
8102
8103							case 'overrun':
8104								break;
8105						}
8106					}
8107				}
8108		}
8109
8110		return $sRet;
8111	}
8112
8113	/**
8114	 * Implemented for the HTML spreadsheet format!
8115	 *
8116	 * @param string $sItemCode
8117	 * @param \ormStopWatch $value
8118	 *
8119	 * @return false|string
8120	 */
8121	public function GetSubItemAsEditValue($sItemCode, $value)
8122	{
8123		$sRet = $value;
8124
8125		switch ($sItemCode)
8126		{
8127			case 'timespent':
8128				break;
8129
8130			case 'started':
8131			case 'laststart':
8132			case 'stopped':
8133				if (is_null($value))
8134				{
8135					$sRet = ''; // Undefined
8136				}
8137				else
8138				{
8139					$sRet = date((string)AttributeDateTime::GetFormat(), $value);
8140				}
8141				break;
8142
8143			default:
8144				foreach($this->ListThresholds() as $iThreshold => $aFoo)
8145				{
8146					$sThPrefix = $iThreshold.'_';
8147					if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
8148					{
8149						// The current threshold is concerned
8150						$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
8151						switch ($sThresholdCode)
8152						{
8153							case 'deadline':
8154								if ($value)
8155								{
8156									$sRet = date((string)AttributeDateTime::GetFormat(), $value);
8157								}
8158								else
8159								{
8160									$sRet = '';
8161								}
8162								break;
8163							case 'passed':
8164							case 'triggered':
8165								$sRet = $this->GetBooleanLabel($value);
8166								break;
8167							case 'overrun':
8168								break;
8169						}
8170					}
8171				}
8172		}
8173
8174		return $sRet;
8175	}
8176}
8177
8178/**
8179 * View of a subvalue of another attribute
8180 * If an attribute implements the verbs GetSubItem.... then it can expose
8181 * internal values, each of them being an attribute and therefore they
8182 * can be displayed at different times in the object lifecycle, and used for
8183 * reporting (as a condition in OQL, or as an additional column in an export)
8184 * Known usages: Stop Watches can expose threshold statuses
8185 */
8186class AttributeSubItem extends AttributeDefinition
8187{
8188	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
8189
8190	static public function ListExpectedParams()
8191	{
8192		return array_merge(parent::ListExpectedParams(), array('target_attcode', 'item_code'));
8193	}
8194
8195	public function GetParentAttCode()
8196	{
8197		return $this->Get("target_attcode");
8198	}
8199
8200	/**
8201	 * Helper : get the attribute definition to which the execution will be forwarded
8202	 */
8203	public function GetTargetAttDef()
8204	{
8205		$sClass = $this->GetHostClass();
8206		$oParentAttDef = MetaModel::GetAttributeDef($sClass, $this->Get('target_attcode'));
8207
8208		return $oParentAttDef;
8209	}
8210
8211	public function GetEditClass()
8212	{
8213		return "";
8214	}
8215
8216	public function GetValuesDef()
8217	{
8218		return null;
8219	}
8220
8221	static public function IsBasedOnDBColumns()
8222	{
8223		return true;
8224	}
8225
8226	static public function IsScalar()
8227	{
8228		return true;
8229	}
8230
8231	public function IsWritable()
8232	{
8233		return false;
8234	}
8235
8236	public function GetDefaultValue(DBObject $oHostObject = null)
8237	{
8238		return null;
8239	}
8240
8241//	public function IsNullAllowed() {return false;}
8242
8243	static public function LoadInObject()
8244	{
8245		return false;
8246	} // if this verb returns false, then GetValues must be implemented
8247
8248	/**
8249	 * Used by DBOBject::Get()
8250	 *
8251	 * @param \DBObject $oHostObject
8252	 *
8253	 * @return \AttributeSubItem
8254	 * @throws \CoreException
8255	 */
8256	public function GetValue($oHostObject)
8257	{
8258		/** @var \AttributeStopWatch $oParent */
8259		$oParent = $this->GetTargetAttDef();
8260		$parentValue = $oHostObject->GetStrict($oParent->GetCode());
8261		$res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject);
8262
8263		return $res;
8264	}
8265
8266	//
8267//	protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
8268
8269	public function FromSQLToValue($aCols, $sPrefix = '')
8270	{
8271	}
8272
8273	public function GetSQLColumns($bFullSpec = false)
8274	{
8275		return array();
8276	}
8277
8278	public function GetFilterDefinitions()
8279	{
8280		return array($this->GetCode() => new FilterFromAttribute($this));
8281	}
8282
8283	public function GetBasicFilterOperators()
8284	{
8285		return array();
8286	}
8287
8288	public function GetBasicFilterLooseOperator()
8289	{
8290		return "=";
8291	}
8292
8293	public function GetBasicFilterSQLExpr($sOpCode, $value)
8294	{
8295		$sQValue = CMDBSource::Quote($value);
8296		switch ($sOpCode)
8297		{
8298			case '!=':
8299				return $this->GetSQLExpr()." != $sQValue";
8300				break;
8301			case '=':
8302			default:
8303				return $this->GetSQLExpr()." = $sQValue";
8304		}
8305	}
8306
8307	public function GetSQLExpressions($sPrefix = '')
8308	{
8309		$oParent = $this->GetTargetAttDef();
8310		$res = $oParent->GetSubItemSQLExpression($this->Get('item_code'));
8311
8312		return $res;
8313	}
8314
8315	public function GetAsPlainText($value, $oHostObject = null, $bLocalize = true)
8316	{
8317		$oParent = $this->GetTargetAttDef();
8318		$res = $oParent->GetSubItemAsPlainText($this->Get('item_code'), $value);
8319
8320		return $res;
8321	}
8322
8323	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
8324	{
8325		$oParent = $this->GetTargetAttDef();
8326		$res = $oParent->GetSubItemAsHTML($this->Get('item_code'), $value);
8327
8328		return $res;
8329	}
8330
8331	public function GetAsHTMLForHistory($value, $oHostObject = null, $bLocalize = true)
8332	{
8333		$oParent = $this->GetTargetAttDef();
8334		$res = $oParent->GetSubItemAsHTMLForHistory($this->Get('item_code'), $value);
8335
8336		return $res;
8337	}
8338
8339	public function GetAsCSV(
8340		$value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
8341		$bConvertToPlainText = false
8342	) {
8343		$oParent = $this->GetTargetAttDef();
8344		$res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator, $sTextQualifier,
8345			$bConvertToPlainText);
8346
8347		return $res;
8348	}
8349
8350	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
8351	{
8352		$oParent = $this->GetTargetAttDef();
8353		$res = $oParent->GetSubItemAsXML($this->Get('item_code'), $value);
8354
8355		return $res;
8356	}
8357
8358	/**
8359	 * As of now, this function must be implemented to have the value in spreadsheet format
8360	 */
8361	public function GetEditValue($value, $oHostObj = null)
8362	{
8363		$oParent = $this->GetTargetAttDef();
8364		$res = $oParent->GetSubItemAsEditValue($this->Get('item_code'), $value);
8365
8366		return $res;
8367	}
8368
8369	public function IsPartOfFingerprint()
8370	{
8371		return false;
8372	}
8373
8374	static public function GetFormFieldClass()
8375	{
8376		return '\\Combodo\\iTop\\Form\\Field\\LabelField';
8377	}
8378
8379	public function MakeFormField(DBObject $oObject, $oFormField = null)
8380	{
8381		if ($oFormField === null)
8382		{
8383			$sFormFieldClass = static::GetFormFieldClass();
8384			$oFormField = new $sFormFieldClass($this->GetCode());
8385		}
8386		parent::MakeFormField($oObject, $oFormField);
8387
8388		// Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition
8389		$sAttCode = $this->GetCode();
8390		$oFormField->SetCurrentValue(html_entity_decode($oObject->GetAsHTML($sAttCode), ENT_QUOTES, 'UTF-8'));
8391		$oFormField->SetReadOnly(true);
8392
8393		return $oFormField;
8394	}
8395
8396}
8397
8398/**
8399 * One way encrypted (hashed) password
8400 */
8401class AttributeOneWayPassword extends AttributeDefinition
8402{
8403	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
8404
8405	static public function ListExpectedParams()
8406	{
8407		return array_merge(parent::ListExpectedParams(), array("depends_on"));
8408	}
8409
8410	public function GetEditClass()
8411	{
8412		return "One Way Password";
8413	}
8414
8415	static public function IsBasedOnDBColumns()
8416	{
8417		return true;
8418	}
8419
8420	static public function IsScalar()
8421	{
8422		return true;
8423	}
8424
8425	public function IsWritable()
8426	{
8427		return true;
8428	}
8429
8430	public function GetDefaultValue(DBObject $oHostObject = null)
8431	{
8432		return "";
8433	}
8434
8435	public function IsNullAllowed()
8436	{
8437		return $this->GetOptional("is_null_allowed", false);
8438	}
8439
8440	// Facilitate things: allow the user to Set the value from a string or from an ormPassword (already encrypted)
8441	public function MakeRealValue($proposedValue, $oHostObj)
8442	{
8443		$oPassword = $proposedValue;
8444		if (is_object($oPassword))
8445		{
8446			$oPassword = clone $proposedValue;
8447		}
8448		else
8449		{
8450			$oPassword = new ormPassword('', '');
8451			$oPassword->SetPassword($proposedValue);
8452		}
8453
8454		return $oPassword;
8455	}
8456
8457	public function GetSQLExpressions($sPrefix = '')
8458	{
8459		if ($sPrefix == '')
8460		{
8461			$sPrefix = $this->GetCode(); // Warning: AttributeOneWayPassword does not have any sql property so code = sql !
8462		}
8463		$aColumns = array();
8464		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
8465		$aColumns[''] = $sPrefix.'_hash';
8466		$aColumns['_salt'] = $sPrefix.'_salt';
8467
8468		return $aColumns;
8469	}
8470
8471	public function FromSQLToValue($aCols, $sPrefix = '')
8472	{
8473		if (!array_key_exists($sPrefix, $aCols))
8474		{
8475			$sAvailable = implode(', ', array_keys($aCols));
8476			throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
8477		}
8478		$hashed = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : '';
8479
8480		if (!array_key_exists($sPrefix.'_salt', $aCols))
8481		{
8482			$sAvailable = implode(', ', array_keys($aCols));
8483			throw new MissingColumnException("Missing column '".$sPrefix."_salt' from {$sAvailable}");
8484		}
8485		$sSalt = isset($aCols[$sPrefix.'_salt']) ? $aCols[$sPrefix.'_salt'] : '';
8486
8487		$value = new ormPassword($hashed, $sSalt);
8488
8489		return $value;
8490	}
8491
8492	public function GetSQLValues($value)
8493	{
8494		// #@# Optimization: do not load blobs anytime
8495		//	 As per mySQL doc, selecting blob columns will prevent mySQL from
8496		//	 using memory in case a temporary table has to be created
8497		//	 (temporary tables created on disk)
8498		//	 We will have to remove the blobs from the list of attributes when doing the select
8499		//	 then the use of Get() should finalize the load
8500		if ($value instanceOf ormPassword)
8501		{
8502			$aValues = array();
8503			$aValues[$this->GetCode().'_hash'] = $value->GetHash();
8504			$aValues[$this->GetCode().'_salt'] = $value->GetSalt();
8505		}
8506		else
8507		{
8508			$aValues = array();
8509			$aValues[$this->GetCode().'_hash'] = '';
8510			$aValues[$this->GetCode().'_salt'] = '';
8511		}
8512
8513		return $aValues;
8514	}
8515
8516	public function GetSQLColumns($bFullSpec = false)
8517	{
8518		$aColumns = array();
8519		$aColumns[$this->GetCode().'_hash'] = 'TINYBLOB';
8520		$aColumns[$this->GetCode().'_salt'] = 'TINYBLOB';
8521
8522		return $aColumns;
8523	}
8524
8525	public function GetImportColumns()
8526	{
8527		$aColumns = array();
8528		$aColumns[$this->GetCode()] = 'TINYTEXT'.CMDBSource::GetSqlStringColumnDefinition();
8529
8530		return $aColumns;
8531	}
8532
8533	public function FromImportToValue($aCols, $sPrefix = '')
8534	{
8535		if (!isset($aCols[$sPrefix]))
8536		{
8537			$sAvailable = implode(', ', array_keys($aCols));
8538			throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
8539		}
8540		$sClearPwd = $aCols[$sPrefix];
8541
8542		$oPassword = new ormPassword('', '');
8543		$oPassword->SetPassword($sClearPwd);
8544
8545		return $oPassword;
8546	}
8547
8548	public function GetFilterDefinitions()
8549	{
8550		return array();
8551		// still not working... see later...
8552	}
8553
8554	public function GetBasicFilterOperators()
8555	{
8556		return array();
8557	}
8558
8559	public function GetBasicFilterLooseOperator()
8560	{
8561		return '=';
8562	}
8563
8564	public function GetBasicFilterSQLExpr($sOpCode, $value)
8565	{
8566		return 'true';
8567	}
8568
8569	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
8570	{
8571		if (is_object($value))
8572		{
8573			return $value->GetAsHTML();
8574		}
8575
8576		return '';
8577	}
8578
8579	public function GetAsCSV(
8580		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
8581		$bConvertToPlainText = false
8582	) {
8583		return ''; // Not exportable in CSV
8584	}
8585
8586	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
8587	{
8588		return ''; // Not exportable in XML
8589	}
8590
8591	public function GetValueLabel($sValue, $oHostObj = null)
8592	{
8593		// Don't display anything in "group by" reports
8594		return '*****';
8595	}
8596
8597}
8598
8599// Indexed array having two dimensions
8600class AttributeTable extends AttributeDBField
8601{
8602	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
8603
8604	public function GetEditClass()
8605	{
8606		return "Table";
8607	}
8608
8609	protected function GetSQLCol($bFullSpec = false)
8610	{
8611		return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition();
8612	}
8613
8614	public function GetMaxSize()
8615	{
8616		return null;
8617	}
8618
8619	public function GetNullValue()
8620	{
8621		return array();
8622	}
8623
8624	public function IsNull($proposedValue)
8625	{
8626		return (count($proposedValue) == 0);
8627	}
8628
8629	public function GetEditValue($sValue, $oHostObj = null)
8630	{
8631		return '';
8632	}
8633
8634	// Facilitate things: allow the user to Set the value from a string
8635	public function MakeRealValue($proposedValue, $oHostObj)
8636	{
8637		if (is_null($proposedValue))
8638		{
8639			return array();
8640		}
8641		else
8642		{
8643			if (!is_array($proposedValue))
8644			{
8645				return array(0 => array(0 => $proposedValue));
8646			}
8647		}
8648
8649		return $proposedValue;
8650	}
8651
8652	public function FromSQLToValue($aCols, $sPrefix = '')
8653	{
8654		try
8655		{
8656			$value = @unserialize($aCols[$sPrefix.'']);
8657			if ($value === false)
8658			{
8659				$value = $this->MakeRealValue($aCols[$sPrefix.''], null);
8660			}
8661		} catch (Exception $e)
8662		{
8663			$value = $this->MakeRealValue($aCols[$sPrefix.''], null);
8664		}
8665
8666		return $value;
8667	}
8668
8669	public function GetSQLValues($value)
8670	{
8671		$aValues = array();
8672		$aValues[$this->Get("sql")] = serialize($value);
8673
8674		return $aValues;
8675	}
8676
8677	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
8678	{
8679		if (!is_array($value))
8680		{
8681			throw new CoreException('Expecting an array', array('found' => get_class($value)));
8682		}
8683		if (count($value) == 0)
8684		{
8685			return "";
8686		}
8687
8688		$sRes = "<TABLE class=\"listResults\">";
8689		$sRes .= "<TBODY>";
8690		foreach($value as $iRow => $aRawData)
8691		{
8692			$sRes .= "<TR>";
8693			foreach($aRawData as $iCol => $cell)
8694			{
8695				// Note: avoid the warning in case the cell is made of an array
8696				$sCell = @Str::pure2html((string)$cell);
8697				$sCell = str_replace("\n", "<br>\n", $sCell);
8698				$sRes .= "<TD>$sCell</TD>";
8699			}
8700			$sRes .= "</TR>";
8701		}
8702		$sRes .= "</TBODY>";
8703		$sRes .= "</TABLE>";
8704
8705		return $sRes;
8706	}
8707
8708	public function GetAsCSV(
8709		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
8710		$bConvertToPlainText = false
8711	) {
8712		// Not implemented
8713		return '';
8714	}
8715
8716	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
8717	{
8718		if (!is_array($value) || count($value) == 0)
8719		{
8720			return "";
8721		}
8722
8723		$sRes = "";
8724		foreach($value as $iRow => $aRawData)
8725		{
8726			$sRes .= "<row>";
8727			foreach($aRawData as $iCol => $cell)
8728			{
8729				$sCell = Str::pure2xml((string)$cell);
8730				$sRes .= "<cell icol=\"$iCol\">$sCell</cell>";
8731			}
8732			$sRes .= "</row>";
8733		}
8734
8735		return $sRes;
8736	}
8737}
8738
8739// The PHP value is a hash array, it is stored as a TEXT column
8740class AttributePropertySet extends AttributeTable
8741{
8742	public function GetEditClass()
8743	{
8744		return "PropertySet";
8745	}
8746
8747	// Facilitate things: allow the user to Set the value from a string
8748	public function MakeRealValue($proposedValue, $oHostObj)
8749	{
8750		if (!is_array($proposedValue))
8751		{
8752			return array('?' => (string)$proposedValue);
8753		}
8754
8755		return $proposedValue;
8756	}
8757
8758	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
8759	{
8760		if (!is_array($value))
8761		{
8762			throw new CoreException('Expecting an array', array('found' => get_class($value)));
8763		}
8764		if (count($value) == 0)
8765		{
8766			return "";
8767		}
8768
8769		$sRes = "<TABLE class=\"listResults\">";
8770		$sRes .= "<TBODY>";
8771		foreach($value as $sProperty => $sValue)
8772		{
8773			if ($sProperty == 'auth_pwd')
8774			{
8775				$sValue = '*****';
8776			}
8777			$sRes .= "<TR>";
8778			$sCell = str_replace("\n", "<br>\n", Str::pure2html((string)$sValue));
8779			$sRes .= "<TD class=\"label\">$sProperty</TD><TD>$sCell</TD>";
8780			$sRes .= "</TR>";
8781		}
8782		$sRes .= "</TBODY>";
8783		$sRes .= "</TABLE>";
8784
8785		return $sRes;
8786	}
8787
8788	public function GetAsCSV(
8789		$value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
8790		$bConvertToPlainText = false
8791	) {
8792		if (!is_array($value) || count($value) == 0)
8793		{
8794			return "";
8795		}
8796
8797		$aRes = array();
8798		foreach($value as $sProperty => $sValue)
8799		{
8800			if ($sProperty == 'auth_pwd')
8801			{
8802				$sValue = '*****';
8803			}
8804			$sFrom = array(',', '=');
8805			$sTo = array('\,', '\=');
8806			$aRes[] = $sProperty.'='.str_replace($sFrom, $sTo, (string)$sValue);
8807		}
8808		$sRaw = implode(',', $aRes);
8809
8810		$sFrom = array("\r\n", $sTextQualifier);
8811		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
8812		$sEscaped = str_replace($sFrom, $sTo, $sRaw);
8813
8814		return $sTextQualifier.$sEscaped.$sTextQualifier;
8815	}
8816
8817	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
8818	{
8819		if (!is_array($value) || count($value) == 0)
8820		{
8821			return "";
8822		}
8823
8824		$sRes = "";
8825		foreach($value as $sProperty => $sValue)
8826		{
8827			if ($sProperty == 'auth_pwd')
8828			{
8829				$sValue = '*****';
8830			}
8831			$sRes .= "<property id=\"$sProperty\">";
8832			$sRes .= Str::pure2xml((string)$sValue);
8833			$sRes .= "</property>";
8834		}
8835
8836		return $sRes;
8837	}
8838}
8839
8840/**
8841 * An unordered multi values attribute
8842 * Allowed values are mandatory for this attribute to be modified
8843 *
8844 * Class AttributeSet
8845 */
8846abstract class AttributeSet extends AttributeDBFieldVoid
8847{
8848	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
8849	const EDITABLE_INPUT_ID_SUFFIX = '-setwidget-values'; // used client side, see js/jquery.itop-set-widget.js
8850
8851	public function __construct($sCode, array $aParams)
8852	{
8853		parent::__construct($sCode, $aParams);
8854		$this->aCSSClasses[] = 'attribute-set';
8855	}
8856
8857	static public function ListExpectedParams()
8858	{
8859		return array_merge(parent::ListExpectedParams(), array('is_null_allowed', 'max_items'));
8860	}
8861
8862	/**
8863	 * Allowed values are mandatory for this attribute to be modified
8864	 *
8865	 * @param array $aArgs
8866	 * @param string $sContains
8867	 *
8868	 * @return array|null
8869	 * @throws \CoreException
8870	 * @throws \OQLException
8871	 */
8872	public function GetAllowedValues($aArgs = array(), $sContains = '')
8873	{
8874		return parent::GetAllowedValues($aArgs, $sContains);
8875	}
8876
8877	/**
8878	 * @param \ormSet $oValue
8879	 *
8880	 * @param $aArgs
8881	 *
8882	 * @return string JSON to be used in the itop.set_widget JQuery widget
8883	 * @throws \CoreException
8884	 * @throws \OQLException
8885	 */
8886	public function GetJsonForWidget($oValue, $aArgs = array())
8887	{
8888		$aJson = array();
8889
8890		// possible_values
8891		$aAllowedValues = $this->GetAllowedValues($aArgs);
8892		$aSetKeyValData = array();
8893		foreach($aAllowedValues as $sCode => $sLabel)
8894		{
8895			$aSetKeyValData[] = [
8896				'code' => $sCode,
8897				'label' => $sLabel,
8898			];
8899		}
8900		$aJson['possible_values'] = $aSetKeyValData;
8901		$aRemoved = array();
8902		if (is_null($oValue))
8903		{
8904			$aJson['partial_values'] = array();
8905			$aJson['orig_value'] = array();
8906		}
8907		else
8908		{
8909			$aPartialValues = $oValue->GetModified();
8910			foreach ($aPartialValues as $key => $value)
8911			{
8912				if (!isset($aAllowedValues[$value]))
8913				{
8914					unset($aPartialValues[$key]);
8915				}
8916			}
8917			$aJson['partial_values'] = array_values($aPartialValues);
8918			$aOrigValues = array_merge($oValue->GetValues(), $oValue->GetModified());
8919			foreach ($aOrigValues as $key => $value)
8920			{
8921				if (!isset($aAllowedValues[$value]))
8922				{
8923					// Remove unwanted values
8924					$aRemoved[] = $value;
8925					unset($aOrigValues[$key]);
8926				}
8927			}
8928			$aJson['orig_value'] = array_values($aOrigValues);
8929		}
8930		$aJson['added'] = array();
8931		$aJson['removed'] = $aRemoved;
8932
8933		$iMaxTags = $this->GetMaxItems();
8934		$aJson['max_items_allowed'] = $iMaxTags;
8935
8936		return json_encode($aJson);
8937	}
8938
8939	public function RequiresIndex()
8940	{
8941		return true;
8942	}
8943
8944	public function RequiresFullTextIndex()
8945	{
8946		return true;
8947	}
8948
8949	public function GetDefaultValue(DBObject $oHostObject = null)
8950	{
8951		return null;
8952	}
8953
8954	public function IsNullAllowed()
8955	{
8956		return $this->Get("is_null_allowed");
8957	}
8958
8959	public function GetEditClass()
8960	{
8961		return "Set";
8962	}
8963
8964	public function GetEditValue($value, $oHostObj = null)
8965	{
8966		if (is_string($value))
8967		{
8968			return $value;
8969		}
8970		if ($value instanceof ormSet)
8971		{
8972			$value = $value->GetValues();
8973		}
8974		if (is_array($value))
8975		{
8976			return implode(', ', $value);
8977		}
8978		return '';
8979	}
8980
8981	protected function GetSQLCol($bFullSpec = false)
8982	{
8983		$iLen = $this->GetMaxSize();
8984		return "VARCHAR($iLen)"
8985			.CMDBSource::GetSqlStringColumnDefinition()
8986			.($bFullSpec ? $this->GetSQLColSpec() : '');
8987	}
8988
8989	public function GetMaxSize()
8990	{
8991		return 255;
8992	}
8993
8994	public function FromStringToArray($proposedValue)
8995	{
8996		$aValues = array();
8997		if (!empty($proposedValue))
8998		{
8999			foreach(explode(',', $proposedValue) as $sCode)
9000			{
9001				$sValue = trim($sCode);
9002				$aValues[] = $sValue;
9003			}
9004		}
9005		return $aValues;
9006	}
9007
9008	/**
9009	 * @param array $aCols
9010	 * @param string $sPrefix
9011	 *
9012	 * @return mixed
9013	 * @throws \Exception
9014	 */
9015	public function FromSQLToValue($aCols, $sPrefix = '')
9016	{
9017		$sValue = $aCols["$sPrefix"];
9018
9019		return $this->MakeRealValue($sValue, null, true);
9020	}
9021
9022	/**
9023	 * @param $aCols
9024	 * @param string $sPrefix
9025	 *
9026	 * @return mixed
9027	 * @throws \Exception
9028	 */
9029	public function FromImportToValue($aCols, $sPrefix = '')
9030	{
9031		$sValue = $aCols["$sPrefix"];
9032
9033		return $this->MakeRealValue($sValue, null);
9034	}
9035
9036	/**
9037	 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
9038	 *
9039	 * @param $proposedValue
9040	 * @param \DBObject $oHostObj
9041	 *
9042	 * @param bool $bIgnoreErrors
9043	 *
9044	 * @return mixed
9045	 * @throws \CoreException
9046	 * @throws \CoreUnexpectedValue
9047	 */
9048	public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false)
9049	{
9050		$oSet = new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
9051		if (is_string($proposedValue) && !empty($proposedValue))
9052		{
9053			$proposedValue = trim("$proposedValue");
9054			$aValues = $this->FromStringToArray($proposedValue);
9055			$oSet->SetValues($aValues);
9056		}
9057		elseif ($proposedValue instanceof ormSet)
9058		{
9059			$oSet = $proposedValue;
9060		}
9061
9062		return $oSet;
9063	}
9064
9065	/**
9066	 * Get the value from a given string (plain text, CSV import)
9067	 *
9068	 * @param string $sProposedValue
9069	 * @param bool $bLocalizedValue
9070	 * @param string $sSepItem
9071	 * @param string $sSepAttribute
9072	 * @param string $sSepValue
9073	 * @param string $sAttributeQualifier
9074	 *
9075	 * @return mixed null if no match could be found
9076	 * @throws \Exception
9077	 */
9078	public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null)
9079	{
9080		return $this->MakeRealValue($sProposedValue, null);
9081	}
9082
9083	/**
9084	 * @return null|\ormSet
9085	 * @throws \CoreException
9086	 * @throws \Exception
9087	 */
9088	public function GetNullValue()
9089	{
9090		return new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
9091	}
9092
9093	public function IsNull($proposedValue)
9094	{
9095		if (empty($proposedValue))
9096		{
9097			return true;
9098		}
9099
9100		/** @var \ormSet $proposedValue */
9101		return $proposedValue->Count() == 0;
9102	}
9103
9104	/**
9105	 * To be overloaded for localized enums
9106	 *
9107	 * @param $sValue
9108	 *
9109	 * @return string label corresponding to the given value (in plain text)
9110	 * @throws \Exception
9111	 */
9112	public function GetValueLabel($sValue)
9113	{
9114		if ($sValue instanceof ormSet)
9115		{
9116			$sValue = $sValue->GetValues();
9117		}
9118		if (is_array($sValue))
9119		{
9120			return implode(', ', $sValue);
9121		}
9122		return $sValue;
9123	}
9124
9125	/**
9126	 * @param string $sValue
9127	 * @param null $oHostObj
9128	 *
9129	 * @return string
9130	 * @throws \Exception
9131	 */
9132	public function GetAsPlainText($sValue, $oHostObj = null)
9133	{
9134		return $this->GetValueLabel($sValue);
9135	}
9136
9137	/**
9138	 * @param $value
9139	 *
9140	 * @return string
9141	 */
9142	public function ScalarToSQL($value)
9143	{
9144		if (empty($value))
9145		{
9146			return '';
9147		}
9148		if ($value instanceof ormSet)
9149		{
9150			$value = $value->GetValues();
9151		}
9152		if (is_array($value))
9153		{
9154			return implode(', ', $value);
9155		}
9156		return $value;
9157	}
9158
9159	/**
9160	 * @param $value
9161	 * @param \DBObject $oHostObject
9162	 * @param bool $bLocalize
9163	 *
9164	 * @return string|null
9165	 *
9166	 * @throws \Exception
9167	 */
9168	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
9169	{
9170		if ($value instanceof ormSet)
9171		{
9172			$value = $value->GetValues();
9173		}
9174		if (is_array($value))
9175		{
9176			return implode(', ', $value);
9177		}
9178		return $value;
9179	}
9180
9181	public function GetMaxItems()
9182	{
9183		return $this->Get('max_items');
9184	}
9185
9186	static public function GetFormFieldClass()
9187	{
9188		return '\\Combodo\\iTop\\Form\\Field\\SetField';
9189	}
9190}
9191
9192class AttributeClassAttCodeSet extends AttributeSet
9193{
9194	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
9195
9196	const DEFAULT_PARAM_INCLUDE_CHILD_CLASSES_ATTRIBUTES = false;
9197
9198	public function __construct($sCode, array $aParams)
9199	{
9200		parent::__construct($sCode, $aParams);
9201		$this->aCSSClasses[] = 'attribute-class-attcode-set';
9202	}
9203
9204	static public function ListExpectedParams()
9205	{
9206		return array_merge(parent::ListExpectedParams(), array('class_field', 'attribute_definition_list', 'attribute_definition_exclusion_list'));
9207	}
9208
9209	public function GetMaxSize()
9210	{
9211		return max(255, 15 * $this->GetMaxItems());
9212	}
9213
9214	/**
9215	 * @param array $aArgs
9216	 * @param string $sContains
9217	 *
9218	 * @return array|null
9219	 * @throws \CoreException
9220	 */
9221	public function GetAllowedValues($aArgs = array(), $sContains = '')
9222	{
9223		if (!isset($aArgs['this']))
9224		{
9225			return null;
9226		}
9227
9228		$oHostObj = $aArgs['this'];
9229		$sTargetClass = $this->Get('class_field');
9230		$sRootClass = $oHostObj->Get($sTargetClass);
9231		$bIncludeChildClasses = $this->GetOptional('include_child_classes_attributes', static::DEFAULT_PARAM_INCLUDE_CHILD_CLASSES_ATTRIBUTES);
9232
9233		$aExcludeDefs = array();
9234		$sAttDefExclusionList = $this->Get('attribute_definition_exclusion_list');
9235		if (!empty($sAttDefExclusionList))
9236		{
9237			foreach(explode(',', $sAttDefExclusionList) as $sAttDefName)
9238			{
9239				$sAttDefName = trim($sAttDefName);
9240				$aExcludeDefs[$sAttDefName] = $sAttDefName;
9241			}
9242		}
9243
9244		$aAllowedDefs = array();
9245		$sAttDefList = $this->Get('attribute_definition_list');
9246		if (!empty($sAttDefList))
9247		{
9248			foreach(explode(',', $sAttDefList) as $sAttDefName)
9249			{
9250				$sAttDefName = trim($sAttDefName);
9251				$aAllowedDefs[$sAttDefName] = $sAttDefName;
9252			}
9253		}
9254
9255		$aAllAttributes = array();
9256		if (!empty($sRootClass))
9257		{
9258			$aClasses = array($sRootClass);
9259			if($bIncludeChildClasses === true)
9260			{
9261				$aClasses = $aClasses + MetaModel::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_EXCLUDETOP);
9262			}
9263
9264			foreach($aClasses as $sClass)
9265			{
9266				foreach(MetaModel::GetAttributesList($sClass) as $sAttCode)
9267				{
9268					// Add attribute only if not already there (can be in leaf classes but not the root)
9269					if(!array_key_exists($sAttCode, $aAllAttributes))
9270					{
9271						$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
9272						$sAttDefClass = get_class($oAttDef);
9273
9274						// Skip excluded attdefs
9275						if(isset($aExcludeDefs[$sAttDefClass]))
9276						{
9277							continue;
9278						}
9279						// Skip not allowed attdefs only if list specified
9280						if(!empty($aAllowedDefs) && !isset($aAllowedDefs[$sAttDefClass]))
9281						{
9282							continue;
9283						}
9284
9285						$aAllAttributes[$sAttCode] = array(
9286							'classes' => array($sClass),
9287						);
9288					}
9289					else
9290					{
9291						$aAllAttributes[$sAttCode]['classes'][] = $sClass;
9292					}
9293				}
9294			}
9295		}
9296
9297		$aAllowedAttributes = array();
9298		foreach($aAllAttributes as $sAttCode => $aAttData)
9299		{
9300			$iAttClassesCount = count($aAttData['classes']);
9301			$sAttFirstClass = $aAttData['classes'][0];
9302			$sAttLabel = MetaModel::GetLabel($sAttFirstClass, $sAttCode);
9303
9304			if($sAttFirstClass === $sRootClass)
9305			{
9306				$sLabel = Dict::Format('Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass', $sAttCode, $sAttLabel);
9307			}
9308			elseif($iAttClassesCount === 1)
9309			{
9310				$sLabel = Dict::Format('Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass', $sAttCode, $sAttLabel, MetaModel::GetName($sAttFirstClass));
9311			}
9312			else
9313			{
9314				$sLabel = Dict::Format('Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromSeveralChildClasses', $sAttCode, $sAttLabel);
9315			}
9316			$aAllowedAttributes[$sAttCode] = $sLabel;
9317		}
9318		return $aAllowedAttributes;
9319	}
9320
9321	/**
9322	 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
9323	 *
9324	 * @param $proposedValue
9325	 * @param \DBObject $oHostObj
9326	 *
9327	 * @param bool $bIgnoreErrors
9328	 *
9329	 * @return mixed
9330	 * @throws \CoreException
9331	 * @throws \CoreUnexpectedValue
9332	 * @throws \OQLException
9333	 * @throws \Exception
9334	 */
9335	public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false)
9336	{
9337		$oSet = new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
9338		$aArgs = array();
9339		if (!empty($oHostObj))
9340		{
9341			$aArgs['this'] = $oHostObj;
9342		}
9343		$aAllowedAttributes = $this->GetAllowedValues($aArgs);
9344		$aInvalidAttCodes = array();
9345		if (is_string($proposedValue) && !empty($proposedValue))
9346		{
9347			$aJsonFromWidget = json_decode($proposedValue, true);
9348			if (is_null($aJsonFromWidget))
9349			{
9350				$proposedValue = trim($proposedValue);
9351				$aValues = array();
9352				foreach(explode(',', $proposedValue) as $sValue)
9353				{
9354					$sAttCode = trim($sValue);
9355					if (empty($aAllowedAttributes) || isset($aAllowedAttributes[$sAttCode]))
9356					{
9357						$aValues[$sAttCode] = $sAttCode;
9358					}
9359					else
9360					{
9361						$aInvalidAttCodes[] = $sAttCode;
9362					}
9363				}
9364				$oSet->SetValues($aValues);
9365			}
9366		}
9367		elseif ($proposedValue instanceof ormSet)
9368		{
9369			$oSet = $proposedValue;
9370		}
9371		if (!empty($aInvalidAttCodes) && !$bIgnoreErrors)
9372		{
9373			$sTargetClass = $this->Get('class_field');
9374			$sClass = $oHostObj->Get($sTargetClass);
9375			throw new CoreUnexpectedValue("The attribute(s) ".implode(', ', $aInvalidAttCodes)." are invalid for class {$sClass}");
9376		}
9377
9378		return $oSet;
9379	}
9380
9381	/**
9382	 * @param $value
9383	 * @param \DBObject $oHostObject
9384	 * @param bool $bLocalize
9385	 *
9386	 * @return string|null
9387	 *
9388	 * @throws \Exception
9389	 */
9390	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
9391	{
9392		if ($value instanceof ormSet)
9393		{
9394			$value = $value->GetValues();
9395		}
9396		if (is_array($value))
9397		{
9398			if (!empty($oHostObject) && $bLocalize)
9399			{
9400				$sTargetClass = $this->Get('class_field');
9401				$sClass = $oHostObject->Get($sTargetClass);
9402
9403				$aLocalizedValues = array();
9404				foreach($value as $sAttCode)
9405				{
9406					try
9407					{
9408						$sAttClass = $sClass;
9409
9410						// Look for the first class (current or children) that have this attcode
9411						foreach(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass)
9412						{
9413							if(MetaModel::IsValidAttCode($sChildClass, $sAttCode))
9414							{
9415								$sAttClass = $sChildClass;
9416								break;
9417							}
9418						}
9419
9420						$aLocalizedValues[] = '<span class="attribute-set-item" data-code="'.$sAttCode.'" data-label="'.MetaModel::GetLabel($sAttClass, $sAttCode)." ($sAttCode)".'" data-description="">'.$sAttCode.'</span>';
9421					} catch (Exception $e)
9422					{
9423						// Ignore bad values
9424					}
9425				}
9426				$value = $aLocalizedValues;
9427			}
9428			$value = implode('', $value);
9429		}
9430		return '<span class="'.implode(' ', $this->aCSSClasses).'">'.$value.'</span>';
9431	}
9432}
9433
9434class AttributeQueryAttCodeSet extends AttributeSet
9435{
9436	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
9437
9438	public function __construct($sCode, array $aParams)
9439	{
9440		parent::__construct($sCode, $aParams);
9441		$this->aCSSClasses[] = 'attribute-query-attcode-set';
9442	}
9443
9444	static public function ListExpectedParams()
9445	{
9446		return array_merge(parent::ListExpectedParams(), array('query_field'));
9447	}
9448
9449	protected function GetSQLCol($bFullSpec = false)
9450	{
9451		return "TEXT".CMDBSource::GetSqlStringColumnDefinition();
9452	}
9453
9454	public function GetMaxSize()
9455	{
9456		return 65535;
9457	}
9458
9459	/**
9460	 * Get a class array indexed by alias
9461	 * @param $oHostObj
9462	 *
9463	 * @return array
9464	 */
9465	private function GetClassList($oHostObj)
9466	{
9467		try
9468		{
9469			$sQueryField = $this->Get('query_field');
9470			$sQuery = $oHostObj->Get($sQueryField);
9471			if (empty($sQuery))
9472			{
9473				return array();
9474			}
9475			$oFilter = DBSearch::FromOQL($sQuery);
9476			return $oFilter->GetSelectedClasses();
9477
9478		} catch (OQLException $e)
9479		{
9480			IssueLog::Warning($e->getMessage());
9481		}
9482		return array();
9483	}
9484
9485	public function GetAllowedValues($aArgs = array(), $sContains = '')
9486	{
9487		if (isset($aArgs['this']))
9488		{
9489			$oHostObj = $aArgs['this'];
9490			$aClasses = $this->GetClassList($oHostObj);
9491
9492			$aAllowedAttributes = array();
9493			$aAllAttributes = array();
9494
9495			if ((count($aClasses) == 1) && (array_keys($aClasses)[0] == array_values($aClasses)[0]))
9496			{
9497				$sClass = reset($aClasses);
9498				$aAttributes = MetaModel::GetAttributesList($sClass);
9499				foreach($aAttributes as $sAttCode)
9500				{
9501					$aAllowedAttributes[$sAttCode] = "$sAttCode (".MetaModel::GetLabel($sClass, $sAttCode).')';
9502				}
9503			}
9504			else
9505			{
9506				if (!empty($aClasses))
9507				{
9508					ksort($aClasses);
9509					foreach($aClasses as $sAlias => $sClass)
9510					{
9511						$aAttributes = MetaModel::GetAttributesList($sClass);
9512						foreach($aAttributes as $sAttCode)
9513						{
9514							$aAllAttributes[] = array('alias' => $sAlias, 'class' => $sClass, 'att_code' => $sAttCode);
9515						}
9516					}
9517				}
9518				foreach($aAllAttributes as $aFullAttCode)
9519				{
9520					$sAttCode = $aFullAttCode['alias'].'.'.$aFullAttCode['att_code'];
9521					$sClass = $aFullAttCode['class'];
9522					$sLabel = "$sAttCode (".MetaModel::GetLabel($sClass, $aFullAttCode['att_code']).')';
9523					$aAllowedAttributes[$sAttCode] = $sLabel;
9524				}
9525			}
9526			return $aAllowedAttributes;
9527		}
9528
9529		return null;
9530	}
9531
9532	/**
9533	 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
9534	 *
9535	 * @param $proposedValue
9536	 * @param \DBObject $oHostObj
9537	 *
9538	 * @param bool $bIgnoreErrors
9539	 *
9540	 * @return mixed
9541	 * @throws \CoreException
9542	 * @throws \CoreUnexpectedValue
9543	 * @throws \OQLException
9544	 * @throws \Exception
9545	 */
9546	public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false)
9547	{
9548		$oSet = new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
9549		$aArgs = array();
9550		if (!empty($oHostObj))
9551		{
9552			$aArgs['this'] = $oHostObj;
9553		}
9554		$aAllowedAttributes = $this->GetAllowedValues($aArgs);
9555		$aInvalidAttCodes = array();
9556		if (is_string($proposedValue) && !empty($proposedValue))
9557		{
9558			$proposedValue = trim($proposedValue);
9559			$aValues = array();
9560			foreach(explode(',', $proposedValue) as $sValue)
9561			{
9562				$sAttCode = trim($sValue);
9563				if (empty($aAllowedAttributes) || isset($aAllowedAttributes[$sAttCode]))
9564				{
9565					$aValues[$sAttCode] = $sAttCode;
9566				}
9567				else
9568				{
9569					$aInvalidAttCodes[] = $sAttCode;
9570				}
9571			}
9572			$oSet->SetValues($aValues);
9573		}
9574		elseif ($proposedValue instanceof ormSet)
9575		{
9576			$oSet = $proposedValue;
9577		}
9578		if (!empty($aInvalidAttCodes) && !$bIgnoreErrors)
9579		{
9580			throw new CoreUnexpectedValue("The attribute(s) ".implode(', ', $aInvalidAttCodes)." are invalid");
9581		}
9582
9583		return $oSet;
9584	}
9585
9586	/**
9587	 * @param $value
9588	 * @param \DBObject $oHostObject
9589	 * @param bool $bLocalize
9590	 *
9591	 * @return string|null
9592	 *
9593	 * @throws \Exception
9594	 */
9595	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
9596	{
9597
9598		if ($value instanceof ormSet)
9599		{
9600			$value = $value->GetValues();
9601		}
9602		if (is_array($value))
9603		{
9604			if (!empty($oHostObject) && $bLocalize)
9605			{
9606				$aArgs['this'] = $oHostObject;
9607				$aAllowedAttributes = $this->GetAllowedValues($aArgs);
9608
9609				$aLocalizedValues = array();
9610				foreach($value as $sAttCode)
9611				{
9612					if (isset($aAllowedAttributes[$sAttCode]))
9613					{
9614						$aLocalizedValues[] = '<span class="attribute-set-item" data-code="'.$sAttCode.'" data-label="'.$aAllowedAttributes[$sAttCode].'" data-description="">'.$sAttCode.'</span>';
9615					}
9616				}
9617				$value = $aLocalizedValues;
9618			}
9619			$value = implode('', $value);
9620		}
9621
9622		return '<span class="'.implode(' ', $this->aCSSClasses).'">'.$value.'</span>';
9623	}
9624}
9625
9626/**
9627 * Multi value list of tags
9628 *
9629 * @see TagSetFieldData
9630 * @since 2.6 N°931 tag fields
9631 */
9632class AttributeTagSet extends AttributeSet
9633{
9634	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_TAG_SET;
9635
9636	public function __construct($sCode, array $aParams)
9637	{
9638		parent::__construct($sCode, $aParams);
9639		$this->aCSSClasses[] = 'attribute-tag-set';
9640	}
9641
9642	public function GetEditClass()
9643	{
9644		return 'TagSet';
9645	}
9646
9647	static public function ListExpectedParams()
9648	{
9649		return array_merge(parent::ListExpectedParams(), array('tag_code_max_len'));
9650	}
9651
9652	/**
9653	 * @param \ormTagSet $oValue
9654	 *
9655	 * @param $aArgs
9656	 *
9657	 * @return string JSON to be used in the itop.tagset_widget JQuery widget
9658	 * @throws \CoreException
9659	 * @throws \OQLException
9660	 */
9661	public function GetJsonForWidget($oValue, $aArgs = array())
9662	{
9663		$aJson = array();
9664
9665		// possible_values
9666		$aTagSetObjectData = $this->GetAllowedValues($aArgs);
9667		$aTagSetKeyValData = array();
9668		foreach($aTagSetObjectData as $sTagCode => $sTagLabel)
9669		{
9670			$aTagSetKeyValData[] = [
9671				'code' => $sTagCode,
9672				'label' => $sTagLabel,
9673			];
9674		}
9675		$aJson['possible_values'] = $aTagSetKeyValData;
9676
9677		if (is_null($oValue))
9678		{
9679			$aJson['partial_values'] = array();
9680			$aJson['orig_value'] = array();
9681			$aJson['added'] = array();
9682			$aJson['removed'] = array();
9683		}
9684		else
9685		{
9686			$aJson['orig_value'] = array_merge($oValue->GetValues(), $oValue->GetModified());
9687			$aJson['added'] = $oValue->GetAdded();
9688			$aJson['removed'] = $oValue->GetRemoved();
9689
9690			if ($oValue->DisplayPartial())
9691			{
9692				// For bulk updates
9693				$aJson['partial_values'] = $oValue->GetModified();
9694			}
9695			else
9696			{
9697				// For simple updates
9698				$aJson['partial_values'] = array();
9699			}
9700		}
9701
9702
9703		$iMaxTags = $this->GetMaxItems();
9704		$aJson['max_items_allowed'] = $iMaxTags;
9705
9706		return json_encode($aJson);
9707	}
9708
9709	public function FromStringToArray($proposedValue)
9710	{
9711		$aValues = array();
9712		if (!empty($proposedValue))
9713		{
9714			foreach(explode(' ', $proposedValue) as $sCode)
9715			{
9716				$sValue = trim($sCode);
9717				$aValues[] = $sValue;
9718			}
9719		}
9720		return $aValues;
9721	}
9722
9723	/**
9724	 * Extract all existing tags from a string and ignore bad tags
9725	 *
9726	 * @param $sValue
9727	 * @param bool $bNoLimit : don't apply the maximum tag limit
9728	 *
9729	 * @return \ormTagSet
9730	 * @throws \CoreException
9731	 * @throws \CoreUnexpectedValue
9732	 */
9733	public function GetExistingTagsFromString($sValue, $bNoLimit = false)
9734	{
9735		$aTagCodes = $this->FromStringToArray("$sValue");
9736		$sAttCode = $this->GetCode();
9737		$sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode);
9738		if ($bNoLimit)
9739		{
9740			$oTagSet = new ormTagSet($sClass, $sAttCode, 0);
9741		}
9742		else
9743		{
9744			$oTagSet = new ormTagSet($sClass, $sAttCode, $this->GetMaxItems());
9745		}
9746		$aGoodTags = array();
9747		foreach($aTagCodes as $sTagCode)
9748		{
9749			if ($sTagCode === '')
9750			{
9751				continue;
9752			}
9753			if ($oTagSet->IsValidTag($sTagCode))
9754			{
9755				$aGoodTags[] = $sTagCode;
9756				if (!$bNoLimit && (count($aGoodTags) === $this->GetMaxItems()))
9757				{
9758					// extra and bad tags are ignored
9759					break;
9760				}
9761			}
9762		}
9763		$oTagSet->SetValues($aGoodTags);
9764
9765		return $oTagSet;
9766	}
9767
9768	public function GetTagCodeMaxLength()
9769	{
9770		return $this->Get('tag_code_max_len');
9771	}
9772
9773	public function GetEditValue($value, $oHostObj = null)
9774	{
9775		if (empty($value))
9776		{
9777			return '';
9778		}
9779		if ($value instanceof ormTagSet)
9780		{
9781			$aValues = $value->GetValues();
9782
9783			return implode(' ', $aValues);
9784		}
9785
9786		return '';
9787	}
9788
9789	public function GetMaxSize()
9790	{
9791		return max(255, ($this->GetMaxItems() * $this->GetTagCodeMaxLength()) + 1);
9792	}
9793
9794	public function Equals($val1, $val2)
9795	{
9796		if (($val1 instanceof ormTagSet) && ($val2 instanceof ormTagSet))
9797		{
9798			return $val1->Equals($val2);
9799		}
9800
9801		return ($val1 == $val2);
9802	}
9803
9804	public function GetAllowedValues($aArgs = array(), $sContains = '')
9805	{
9806		$sAttCode = $this->GetCode();
9807		$sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode);
9808		$aAllowedTags = TagSetFieldData::GetAllowedValues($sClass, $sAttCode);
9809		$aAllowedValues = array();
9810		foreach($aAllowedTags as $oAllowedTag)
9811		{
9812			$aAllowedValues[$oAllowedTag->Get('code')] = $oAllowedTag->Get('label');
9813		}
9814
9815		return $aAllowedValues;
9816	}
9817
9818	/**
9819	 * @param array $aCols
9820	 * @param string $sPrefix
9821	 *
9822	 * @return mixed
9823	 * @throws \CoreException
9824	 * @throws \Exception
9825	 */
9826	public function FromSQLToValue($aCols, $sPrefix = '')
9827	{
9828		$sValue = $aCols["$sPrefix"];
9829
9830		return $this->GetExistingTagsFromString($sValue);
9831	}
9832
9833	/**
9834	 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
9835	 *
9836	 * @param $proposedValue
9837	 * @param $oHostObj
9838	 *
9839	 * @param bool $bIgnoreErrors
9840	 *
9841	 * @return mixed
9842	 * @throws \CoreException
9843	 * @throws \CoreUnexpectedValue
9844	 */
9845	public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false)
9846	{
9847		$oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
9848		if (is_string($proposedValue) && !empty($proposedValue))
9849		{
9850			$sJsonFromWidget = json_decode($proposedValue, true);
9851			if (is_null($sJsonFromWidget))
9852			{
9853				$proposedValue = trim("$proposedValue");
9854				$aTagCodes = $this->FromStringToArray($proposedValue);
9855				$oTagSet->SetValues($aTagCodes);
9856			}
9857		}
9858		elseif ($proposedValue instanceof ormTagSet)
9859		{
9860			$oTagSet = $proposedValue;
9861		}
9862
9863		return $oTagSet;
9864	}
9865
9866	/**
9867	 * Get the value from a given string (plain text, CSV import)
9868	 *
9869	 * @param string $sProposedValue
9870	 * @param bool $bLocalizedValue
9871	 * @param string $sSepItem
9872	 * @param string $sSepAttribute
9873	 * @param string $sSepValue
9874	 * @param string $sAttributeQualifier
9875	 *
9876	 * @return mixed null if no match could be found
9877	 * @throws \Exception
9878	 */
9879	public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null)
9880	{
9881		if (is_null($sSepItem) || empty($sSepItem))
9882		{
9883			$sSepItem = MetaModel::GetConfig()->Get('tag_set_item_separator');
9884		}
9885		if (!empty($sProposedValue))
9886		{
9887			$oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()),
9888				$this->GetCode(), $this->GetMaxItems());
9889			$aLabels = explode($sSepItem, $sProposedValue);
9890			$aCodes = array();
9891			foreach($aLabels as $sTagLabel)
9892			{
9893				if (!empty($sTagLabel))
9894				{
9895					$aCodes[] = ($bLocalizedValue) ? $oTagSet->GetTagFromLabel($sTagLabel) : $sTagLabel;
9896				}
9897			}
9898			$sProposedValue = implode(' ', $aCodes);
9899		}
9900
9901		return $this->MakeRealValue($sProposedValue, null);
9902	}
9903
9904	public function GetNullValue()
9905	{
9906		return new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
9907	}
9908
9909	public function IsNull($proposedValue)
9910	{
9911		if (is_null($proposedValue))
9912		{
9913			return true;
9914		}
9915
9916		/** @var \ormTagSet $proposedValue */
9917		return count($proposedValue->GetValues()) == 0;
9918	}
9919
9920	/**
9921	 * To be overloaded for localized enums
9922	 *
9923	 * @param $sValue
9924	 *
9925	 * @return string label corresponding to the given value (in plain text)
9926	 * @throws \CoreWarning
9927	 * @throws \Exception
9928	 */
9929	public function GetValueLabel($sValue)
9930	{
9931		if (empty($sValue))
9932		{
9933			return '';
9934		}
9935		if (is_string($sValue))
9936		{
9937			$sValue = $this->GetExistingTagsFromString($sValue);
9938		}
9939		if ($sValue instanceof ormTagSet)
9940		{
9941			$aValues = $sValue->GetLabels();
9942
9943			return implode(', ', $aValues);
9944		}
9945		throw new CoreWarning('Expected the attribute value to be a TagSet', array(
9946			'found_type' => gettype($sValue),
9947			'value' => $sValue,
9948			'class' => $this->GetHostClass(),
9949			'attribute' => $this->GetCode()
9950		));
9951	}
9952
9953	/**
9954	 * @param $value
9955	 *
9956	 * @return string
9957	 * @throws \CoreWarning
9958	 */
9959	public function ScalarToSQL($value)
9960	{
9961		if (empty($value))
9962		{
9963			return '';
9964		}
9965		if ($value instanceof ormTagSet)
9966		{
9967			$aValues = $value->GetValues();
9968
9969			return implode(' ', $aValues);
9970		}
9971		throw new CoreWarning('Expected the attribute value to be a TagSet', array(
9972			'found_type' => gettype($value),
9973			'value' => $value,
9974			'class' => $this->GetHostClass(),
9975			'attribute' => $this->GetCode()
9976		));
9977	}
9978
9979	/**
9980	 * @param $value
9981	 * @param \DBObject $oHostObject
9982	 * @param bool $bLocalize
9983	 *
9984	 * @return string|null
9985	 *
9986	 * @throws \CoreException
9987	 * @throws \Exception
9988	 */
9989	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
9990	{
9991		if ($value instanceof ormTagSet)
9992		{
9993			if ($bLocalize)
9994			{
9995				$aValues = $value->GetTags();
9996			}
9997			else
9998			{
9999				$aValues = $value->GetValues();
10000			}
10001			if (empty($aValues))
10002			{
10003				return '';
10004			}
10005
10006			return $this->GenerateViewHtmlForValues($aValues);
10007		}
10008		if (is_string($value))
10009		{
10010			try
10011			{
10012				$oValue = $this->MakeRealValue($value, $oHostObject);
10013
10014				return $this->GetAsHTML($oValue, $oHostObject, $bLocalize);
10015			} catch (Exception $e)
10016			{
10017				// unknown tags are present display the code instead
10018			}
10019			$aTagCodes = $this->FromStringToArray($value);
10020			$aValues = array();
10021			$oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()),
10022				$this->GetCode(), $this->GetMaxItems());
10023			foreach($aTagCodes as $sTagCode)
10024			{
10025				try
10026				{
10027					$oTagSet->Add($sTagCode);
10028				} catch (Exception $e)
10029				{
10030					$aValues[] = $sTagCode;
10031				}
10032			}
10033			$sHTML = '';
10034			if (!empty($aValues))
10035			{
10036				$sHTML .= $this->GenerateViewHtmlForValues($aValues, 'attribute-set-item-undefined');
10037			}
10038			$aValues = $oTagSet->GetTags();
10039			if (!empty($aValues))
10040			{
10041				$sHTML .= $this->GenerateViewHtmlForValues($aValues);
10042			}
10043
10044			return $sHTML;
10045		}
10046
10047		return parent::GetAsHTML($value, $oHostObject, $bLocalize);
10048	}
10049
10050	// Do not display friendly names in the history of change
10051	public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null)
10052	{
10053		$sResult = Dict::Format('Change:AttName_Changed', $this->GetLabel()).", ";
10054
10055		$aNewValues = $this->FromStringToArray($sNewValue);
10056		$aOldValues = $this->FromStringToArray($sOldValue);
10057
10058		$aDelta['removed'] = array_diff($aOldValues, $aNewValues);
10059		$aDelta['added'] = array_diff($aNewValues, $aOldValues);
10060
10061		$aAllowedTags = TagSetFieldData::GetAllowedValues(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode());
10062
10063		if (!empty($aDelta['removed']))
10064		{
10065			$aRemoved = array();
10066			foreach($aDelta['removed'] as $idx => $sTagCode)
10067			{
10068				if (empty($sTagCode)) {continue;}
10069				$sTagLabel = $sTagCode;
10070				foreach($aAllowedTags as $oTag)
10071				{
10072					if ($sTagCode === $oTag->Get('code'))
10073					{
10074						$sTagLabel = $oTag->Get('label');
10075					}
10076				}
10077				$aRemoved[] = $sTagLabel;
10078			}
10079
10080			$sRemoved = $this->GenerateViewHtmlForValues($aRemoved, 'history-removed');
10081			if (!empty($sRemoved))
10082			{
10083				$sResult .= Dict::Format('Change:LinkSet:Removed', $sRemoved);
10084			}
10085		}
10086
10087		if (!empty($aDelta['added']))
10088		{
10089			if (!empty($sRemoved))
10090			{
10091				$sResult .= ', ';
10092			}
10093
10094			$aAdded = array();
10095			foreach($aDelta['added'] as $idx => $sTagCode)
10096			{
10097				if (empty($sTagCode)) {continue;}
10098				$sTagLabel = $sTagCode;
10099				foreach($aAllowedTags as $oTag)
10100				{
10101					if ($sTagCode === $oTag->Get('code'))
10102					{
10103						$sTagLabel = $oTag->Get('label');
10104					}
10105				}
10106				$aAdded[] = $sTagLabel;
10107			}
10108
10109			$sAdded = $this->GenerateViewHtmlForValues($aAdded, 'history-added');
10110			if (!empty($sAdded))
10111			{
10112				$sResult .= Dict::Format('Change:LinkSet:Added', $sAdded);
10113			}
10114		}
10115
10116		return $sResult;
10117	}
10118
10119	/**
10120	 * HTML representation of a list of tags (read-only)
10121	 * accept a list of strings or a list of TagSetFieldData
10122	 *
10123	 * @param array $aValues
10124	 * @param string $sCssClass
10125	 * @param bool $bWithLink if true will generate a link, otherwise just a "a" tag without href
10126	 *
10127	 * @return string
10128	 * @throws \CoreException
10129	 * @throws \OQLException
10130	 */
10131	public function GenerateViewHtmlForValues($aValues, $sCssClass = '', $bWithLink = true)
10132	{
10133		if (empty($aValues)) {return '';}
10134		$sHtml = '<span class="'.$sCssClass.' '.implode(' ', $this->aCSSClasses).'">';
10135		foreach($aValues as $oTag)
10136		{
10137			if ($oTag instanceof TagSetFieldData)
10138			{
10139				$sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode());
10140				$sAttCode = $this->GetCode();
10141				$sTagCode = $oTag->Get('code');
10142				$sTagLabel = $oTag->Get('label');
10143				$sTagDescription = $oTag->Get('description');
10144				$oFilter = DBSearch::FromOQL("SELECT $sClass WHERE $sAttCode MATCHES '$sTagCode'");
10145				$oAppContext = new ApplicationContext();
10146				$sContext = $oAppContext->GetForLink();
10147				$sUIPage = cmdbAbstractObject::ComputeStandardUIPage($oFilter->GetClass());
10148				$sFilter = rawurlencode($oFilter->serialize());
10149
10150				$sLink = '';
10151				if ($bWithLink)
10152				{
10153					$sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}";
10154					$sLink = ' href="'.$sUrl.'"';
10155				}
10156
10157				$sHtml .= '<a'.$sLink.' class="attribute-set-item attribute-set-item-'.$sTagCode.'" data-code="'.$sTagCode.'" data-label="'.htmlentities($sTagLabel,
10158						ENT_QUOTES, 'UTF-8').'" data-description="'.htmlentities($sTagDescription, ENT_QUOTES,
10159						'UTF-8').'">'.htmlentities($sTagLabel, ENT_QUOTES, 'UTF-8').'</a>';
10160			}
10161			else
10162			{
10163				$sHtml .= '<span class="attribute-set-item">'.$oTag.'</span>';
10164			}
10165		}
10166		$sHtml .= '</span>';
10167
10168		return $sHtml;
10169	}
10170
10171	/**
10172	 * @param $value
10173	 * @param \DBObject $oHostObject
10174	 * @param bool $bLocalize
10175	 *
10176	 * @return string
10177	 *
10178	 */
10179	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
10180	{
10181		if (is_object($value) && ($value instanceof ormTagSet))
10182		{
10183			$sRes = "<Set>\n";
10184			if ($bLocalize)
10185			{
10186				$aValues = $value->GetLabels();
10187			}
10188			else
10189			{
10190				$aValues = $value->GetValues();
10191			}
10192			if (!empty($aValues))
10193			{
10194				$sRes .= '<Tag>'.implode('</Tag><Tag>', $aValues).'</Tag>';
10195			}
10196			$sRes .= "</Set>\n";
10197		}
10198		else
10199		{
10200			$sRes = '';
10201		}
10202
10203		return $sRes;
10204	}
10205
10206	/**
10207	 * @param $value
10208	 * @param string $sSeparator
10209	 * @param string $sTextQualifier
10210	 * @param \DBObject $oHostObject
10211	 * @param bool $bLocalize
10212	 * @param bool $bConvertToPlainText
10213	 *
10214	 * @return mixed|string
10215	 */
10216	public function GetAsCSV(
10217		$value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
10218		$bConvertToPlainText = false
10219	) {
10220		$sSepItem = MetaModel::GetConfig()->Get('tag_set_item_separator');
10221		if (is_object($value) && ($value instanceof ormTagSet))
10222		{
10223			if ($bLocalize)
10224			{
10225				$aValues = $value->GetLabels();
10226			}
10227			else
10228			{
10229				$aValues = $value->GetValues();
10230			}
10231			$sRes = implode($sSepItem, $aValues);
10232		}
10233		else
10234		{
10235			$sRes = '';
10236		}
10237
10238		return "{$sTextQualifier}{$sRes}{$sTextQualifier}";
10239	}
10240
10241	/**
10242	 * List the available verbs for 'GetForTemplate'
10243	 */
10244	public function EnumTemplateVerbs()
10245	{
10246		return array(
10247			'' => 'Plain text representation',
10248			'html' => 'HTML representation (unordered list)',
10249		);
10250	}
10251
10252	/**
10253	 * Get various representations of the value, for insertion into a template (e.g. in Notifications)
10254	 *
10255	 * @param mixed $value The current value of the field
10256	 * @param string $sVerb The verb specifying the representation of the value
10257	 * @param DBObject $oHostObject The object
10258	 * @param bool $bLocalize Whether or not to localize the value
10259	 *
10260	 * @return string
10261	 * @throws \Exception
10262	 */
10263	public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
10264	{
10265		if (is_object($value) && ($value instanceof ormTagSet))
10266		{
10267			if ($bLocalize)
10268			{
10269				$aValues = $value->GetLabels();
10270				$sSep = ', ';
10271			}
10272			else
10273			{
10274				$aValues = $value->GetValues();
10275				$sSep = ' ';
10276			}
10277
10278			switch ($sVerb)
10279			{
10280				case '':
10281					return implode($sSep, $aValues);
10282
10283				case 'html':
10284					return '<ul><li>'.implode("</li><li>", $aValues).'</li></ul>';
10285
10286				default:
10287					throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject));
10288			}
10289		}
10290		throw new CoreUnexpectedValue("Bad value '$value' for attribute ".$this->GetCode().' in class '.get_class($oHostObject));
10291	}
10292
10293	/**
10294	 * Helper to get a value that will be JSON encoded
10295	 * The operation is the opposite to FromJSONToValue
10296	 *
10297	 * @param \ormTagSet $value
10298	 *
10299	 * @return array
10300	 */
10301	public function GetForJSON($value)
10302	{
10303		$aRet = array();
10304		if (is_object($value) && ($value instanceof ormTagSet))
10305		{
10306			$aRet = $value->GetValues();
10307		}
10308
10309		return $aRet;
10310	}
10311
10312	/**
10313	 * Helper to form a value, given JSON decoded data
10314	 * The operation is the opposite to GetForJSON
10315	 *
10316	 * @param $json
10317	 *
10318	 * @return \ormTagSet
10319	 * @throws \CoreException
10320	 * @throws \CoreUnexpectedValue
10321	 * @throws \Exception
10322	 */
10323	public function FromJSONToValue($json)
10324	{
10325		$oSet = new ormTagSet($this->GetHostClass(), $this->GetCode(), $this->GetMaxItems());
10326		$oSet->SetValues($json);
10327
10328		return $oSet;
10329	}
10330
10331	/**
10332	 * The part of the current attribute in the object's signature, for the supplied value
10333	 *
10334	 * @param mixed $value The value of this attribute for the object
10335	 *
10336	 * @return string The "signature" for this field/attribute
10337	 */
10338	public function Fingerprint($value)
10339	{
10340		if ($value instanceof ormTagSet)
10341		{
10342			$aValues = $value->GetValues();
10343
10344			return implode(' ', $aValues);
10345		}
10346
10347		return parent::Fingerprint($value);
10348	}
10349
10350	static public function GetFormFieldClass()
10351	{
10352		return '\\Combodo\\iTop\\Form\\Field\\TagSetField';
10353	}
10354}
10355
10356/**
10357 * The attribute dedicated to the friendly name automatic attribute (not written)
10358 *
10359 * @package     iTopORM
10360 */
10361class AttributeFriendlyName extends AttributeDefinition
10362{
10363	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
10364	public $m_sValue;
10365
10366	public function __construct($sCode)
10367	{
10368		$this->m_sCode = $sCode;
10369		$aParams = array();
10370		$aParams["default_value"] = '';
10371		parent::__construct($sCode, $aParams);
10372
10373		$this->m_sValue = $this->Get("default_value");
10374	}
10375
10376
10377	public function GetEditClass()
10378	{
10379		return "";
10380	}
10381
10382	public function GetValuesDef()
10383	{
10384		return null;
10385	}
10386
10387	public function GetPrerequisiteAttributes($sClass = null)
10388	{
10389		return $this->GetOptional("depends_on", array());
10390	}
10391
10392	static public function IsScalar()
10393	{
10394		return true;
10395	}
10396
10397	public function IsNullAllowed()
10398	{
10399		return false;
10400	}
10401
10402	public function GetSQLExpressions($sPrefix = '')
10403	{
10404		if ($sPrefix == '')
10405		{
10406			$sPrefix = $this->GetCode(); // Warning AttributeComputedFieldVoid does not have any sql property
10407		}
10408
10409		return array('' => $sPrefix);
10410	}
10411
10412	static public function IsBasedOnOQLExpression()
10413	{
10414		return true;
10415	}
10416
10417	public function GetOQLExpression()
10418	{
10419		return MetaModel::GetNameExpression($this->GetHostClass());
10420	}
10421
10422	public function GetLabel($sDefault = null)
10423	{
10424		$sLabel = parent::GetLabel('');
10425		if (strlen($sLabel) == 0)
10426		{
10427			$sLabel = Dict::S('Core:FriendlyName-Label');
10428		}
10429
10430		return $sLabel;
10431	}
10432
10433	public function GetDescription($sDefault = null)
10434	{
10435		$sLabel = parent::GetDescription('');
10436		if (strlen($sLabel) == 0)
10437		{
10438			$sLabel = Dict::S('Core:FriendlyName-Description');
10439		}
10440
10441		return $sLabel;
10442	}
10443
10444	public function FromSQLToValue($aCols, $sPrefix = '')
10445	{
10446		$sValue = $aCols[$sPrefix];
10447
10448		return $sValue;
10449	}
10450
10451	public function IsWritable()
10452	{
10453		return false;
10454	}
10455
10456	public function IsMagic()
10457	{
10458		return true;
10459	}
10460
10461	static public function IsBasedOnDBColumns()
10462	{
10463		return false;
10464	}
10465
10466	public function SetFixedValue($sValue)
10467	{
10468		$this->m_sValue = $sValue;
10469	}
10470
10471	public function GetDefaultValue(DBObject $oHostObject = null)
10472	{
10473		return $this->m_sValue;
10474	}
10475
10476	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
10477	{
10478		return Str::pure2html((string)$sValue);
10479	}
10480
10481	public function GetAsCSV(
10482		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
10483		$bConvertToPlainText = false
10484	) {
10485		$sFrom = array("\r\n", $sTextQualifier);
10486		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
10487		$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
10488
10489		return $sTextQualifier.$sEscaped.$sTextQualifier;
10490	}
10491
10492	static function GetFormFieldClass()
10493	{
10494		return '\\Combodo\\iTop\\Form\\Field\\StringField';
10495	}
10496
10497	public function MakeFormField(DBObject $oObject, $oFormField = null)
10498	{
10499		if ($oFormField === null)
10500		{
10501			$sFormFieldClass = static::GetFormFieldClass();
10502			$oFormField = new $sFormFieldClass($this->GetCode());
10503		}
10504		$oFormField->SetReadOnly(true);
10505		parent::MakeFormField($oObject, $oFormField);
10506
10507		return $oFormField;
10508	}
10509
10510	// Do not display friendly names in the history of change
10511	public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null)
10512	{
10513		return '';
10514	}
10515
10516	public function GetFilterDefinitions()
10517	{
10518		return array($this->GetCode() => new FilterFromAttribute($this));
10519	}
10520
10521	public function GetBasicFilterOperators()
10522	{
10523		return array("=" => "equals", "!=" => "differs from");
10524	}
10525
10526	public function GetBasicFilterLooseOperator()
10527	{
10528		return "Contains";
10529	}
10530
10531	public function GetBasicFilterSQLExpr($sOpCode, $value)
10532	{
10533		$sQValue = CMDBSource::Quote($value);
10534		switch ($sOpCode)
10535		{
10536			case '=':
10537			case '!=':
10538				return $this->GetSQLExpr()." $sOpCode $sQValue";
10539			case 'Contains':
10540				return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%");
10541			case 'NotLike':
10542				return $this->GetSQLExpr()." NOT LIKE $sQValue";
10543			case 'Like':
10544			default:
10545				return $this->GetSQLExpr()." LIKE $sQValue";
10546		}
10547	}
10548
10549	public function IsPartOfFingerprint()
10550	{
10551		return false;
10552	}
10553}
10554
10555/**
10556 * Holds the setting for the redundancy on a specific relation
10557 * Its value is a string, containing either:
10558 * - 'disabled'
10559 * - 'n', where n is a positive integer value giving the minimum count of items upstream
10560 * - 'n%', where n is a positive integer value, giving the minimum as a percentage of the total count of items upstream
10561 *
10562 * @package     iTopORM
10563 */
10564class AttributeRedundancySettings extends AttributeDBField
10565{
10566	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
10567
10568	static public function ListExpectedParams()
10569	{
10570		return array(
10571			'sql',
10572			'relation_code',
10573			'from_class',
10574			'neighbour_id',
10575			'enabled',
10576			'enabled_mode',
10577			'min_up',
10578			'min_up_type',
10579			'min_up_mode'
10580		);
10581	}
10582
10583	public function GetValuesDef()
10584	{
10585		return null;
10586	}
10587
10588	public function GetPrerequisiteAttributes($sClass = null)
10589	{
10590		return array();
10591	}
10592
10593	public function GetEditClass()
10594	{
10595		return "RedundancySetting";
10596	}
10597
10598	protected function GetSQLCol($bFullSpec = false)
10599	{
10600		return "VARCHAR(20)"
10601			.CMDBSource::GetSqlStringColumnDefinition()
10602			.($bFullSpec ? $this->GetSQLColSpec() : '');
10603	}
10604
10605
10606	public function GetValidationPattern()
10607	{
10608		return "^[0-9]{1,3}|[0-9]{1,2}%|disabled$";
10609	}
10610
10611	public function GetMaxSize()
10612	{
10613		return 20;
10614	}
10615
10616	public function GetDefaultValue(DBObject $oHostObject = null)
10617	{
10618		$sRet = 'disabled';
10619		if ($this->Get('enabled'))
10620		{
10621			if ($this->Get('min_up_type') == 'count')
10622			{
10623				$sRet = (string)$this->Get('min_up');
10624			}
10625			else // percent
10626			{
10627				$sRet = $this->Get('min_up').'%';
10628			}
10629		}
10630
10631		return $sRet;
10632	}
10633
10634	public function IsNullAllowed()
10635	{
10636		return false;
10637	}
10638
10639	public function GetNullValue()
10640	{
10641		return '';
10642	}
10643
10644	public function IsNull($proposedValue)
10645	{
10646		return ($proposedValue == '');
10647	}
10648
10649	public function MakeRealValue($proposedValue, $oHostObj)
10650	{
10651		if (is_null($proposedValue))
10652		{
10653			return '';
10654		}
10655
10656		return (string)$proposedValue;
10657	}
10658
10659	public function ScalarToSQL($value)
10660	{
10661		if (!is_string($value))
10662		{
10663			throw new CoreException('Expected the attribute value to be a string', array(
10664				'found_type' => gettype($value),
10665				'value' => $value,
10666				'class' => $this->GetHostClass(),
10667				'attribute' => $this->GetCode()
10668			));
10669		}
10670
10671		return $value;
10672	}
10673
10674	public function GetRelationQueryData()
10675	{
10676		foreach(MetaModel::EnumRelationQueries($this->GetHostClass(), $this->Get('relation_code'),
10677			false) as $sDummy => $aQueryInfo)
10678		{
10679			if ($aQueryInfo['sFromClass'] == $this->Get('from_class'))
10680			{
10681				if ($aQueryInfo['sNeighbour'] == $this->Get('neighbour_id'))
10682				{
10683					return $aQueryInfo;
10684				}
10685			}
10686		}
10687
10688		return array();
10689	}
10690
10691	/**
10692	 * Find the user option label
10693	 *
10694	 * @param string $sUserOption possible values : disabled|cout|percent
10695	 * @param string $sDefault
10696	 *
10697	 * @return string
10698	 * @throws \Exception
10699	 */
10700	public function GetUserOptionFormat($sUserOption, $sDefault = null)
10701	{
10702		$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, null, true /*user lang*/);
10703		if (is_null($sLabel))
10704		{
10705			// If no default value is specified, let's define the most relevant one for developping purposes
10706			if (is_null($sDefault))
10707			{
10708				$sDefault = str_replace('_', ' ', $this->m_sCode.':'.$sUserOption.'(%1$s)');
10709			}
10710			// Browse the hierarchy again, accepting default (english) translations
10711			$sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, $sDefault, false);
10712		}
10713
10714		return $sLabel;
10715	}
10716
10717	/**
10718	 * Override to display the value in the GUI
10719	 *
10720	 * @param string $sValue
10721	 * @param \DBObject $oHostObject
10722	 * @param bool $bLocalize
10723	 *
10724	 * @return string
10725	 * @throws \CoreException
10726	 * @throws \DictExceptionMissingString
10727	 */
10728	public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
10729	{
10730		$sCurrentOption = $this->GetCurrentOption($sValue);
10731		$sClass = $oHostObject ? get_class($oHostObject) : $this->m_sHostClass;
10732
10733		return sprintf($this->GetUserOptionFormat($sCurrentOption), $this->GetMinUpValue($sValue),
10734			MetaModel::GetName($sClass));
10735	}
10736
10737	public function GetAsCSV(
10738		$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
10739		$bConvertToPlainText = false
10740	) {
10741		$sFrom = array("\r\n", $sTextQualifier);
10742		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
10743		$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
10744
10745		return $sTextQualifier.$sEscaped.$sTextQualifier;
10746	}
10747
10748	/**
10749	 * Helper to interpret the value, given the current settings and string representation of the attribute
10750	 */
10751	public function IsEnabled($sValue)
10752	{
10753		if ($this->get('enabled_mode') == 'fixed')
10754		{
10755			$bRet = $this->get('enabled');
10756		}
10757		else
10758		{
10759			$bRet = ($sValue != 'disabled');
10760		}
10761
10762		return $bRet;
10763	}
10764
10765	/**
10766	 * Helper to interpret the value, given the current settings and string representation of the attribute
10767	 */
10768	public function GetMinUpType($sValue)
10769	{
10770		if ($this->get('min_up_mode') == 'fixed')
10771		{
10772			$sRet = $this->get('min_up_type');
10773		}
10774		else
10775		{
10776			$sRet = 'count';
10777			if (substr(trim($sValue), -1, 1) == '%')
10778			{
10779				$sRet = 'percent';
10780			}
10781		}
10782
10783		return $sRet;
10784	}
10785
10786	/**
10787	 * Helper to interpret the value, given the current settings and string representation of the attribute
10788	 */
10789	public function GetMinUpValue($sValue)
10790	{
10791		if ($this->get('min_up_mode') == 'fixed')
10792		{
10793			$iRet = (int)$this->Get('min_up');
10794		}
10795		else
10796		{
10797			$sRefValue = $sValue;
10798			if (substr(trim($sValue), -1, 1) == '%')
10799			{
10800				$sRefValue = substr(trim($sValue), 0, -1);
10801			}
10802			$iRet = (int)trim($sRefValue);
10803		}
10804
10805		return $iRet;
10806	}
10807
10808	/**
10809	 * Helper to determine if the redundancy can be viewed/edited by the end-user
10810	 */
10811	public function IsVisible()
10812	{
10813		$bRet = false;
10814		if ($this->Get('enabled_mode') == 'fixed')
10815		{
10816			$bRet = $this->Get('enabled');
10817		}
10818		elseif ($this->Get('enabled_mode') == 'user')
10819		{
10820			$bRet = true;
10821		}
10822
10823		return $bRet;
10824	}
10825
10826	public function IsWritable()
10827	{
10828		if (($this->Get('enabled_mode') == 'fixed') && ($this->Get('min_up_mode') == 'fixed'))
10829		{
10830			return false;
10831		}
10832
10833		return true;
10834	}
10835
10836	/**
10837	 * Returns an HTML form that can be read by ReadValueFromPostedForm
10838	 */
10839	public function GetDisplayForm($sCurrentValue, $oPage, $bEditMode = false, $sFormPrefix = '')
10840	{
10841		$sRet = '';
10842		$aUserOptions = $this->GetUserOptions($sCurrentValue);
10843		if (count($aUserOptions) < 2)
10844		{
10845			$bEditOption = false;
10846		}
10847		else
10848		{
10849			$bEditOption = $bEditMode;
10850		}
10851		$sCurrentOption = $this->GetCurrentOption($sCurrentValue);
10852		foreach($aUserOptions as $sUserOption)
10853		{
10854			$bSelected = ($sUserOption == $sCurrentOption);
10855			$sRet .= '<div>';
10856			$sRet .= $this->GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditOption, $sUserOption,
10857				$bSelected);
10858			$sRet .= '</div>';
10859		}
10860
10861		return $sRet;
10862	}
10863
10864	const USER_OPTION_DISABLED = 'disabled';
10865	const USER_OPTION_ENABLED_COUNT = 'count';
10866	const USER_OPTION_ENABLED_PERCENT = 'percent';
10867
10868	/**
10869	 * Depending on the xxx_mode parameters, build the list of options that are allowed to the end-user
10870	 */
10871	protected function GetUserOptions($sValue)
10872	{
10873		$aRet = array();
10874		if ($this->Get('enabled_mode') == 'user')
10875		{
10876			$aRet[] = self::USER_OPTION_DISABLED;
10877		}
10878
10879		if ($this->Get('min_up_mode') == 'user')
10880		{
10881			$aRet[] = self::USER_OPTION_ENABLED_COUNT;
10882			$aRet[] = self::USER_OPTION_ENABLED_PERCENT;
10883		}
10884		else
10885		{
10886			if ($this->GetMinUpType($sValue) == 'count')
10887			{
10888				$aRet[] = self::USER_OPTION_ENABLED_COUNT;
10889			}
10890			else
10891			{
10892				$aRet[] = self::USER_OPTION_ENABLED_PERCENT;
10893			}
10894		}
10895
10896		return $aRet;
10897	}
10898
10899	/**
10900	 * Convert the string representation into one of the existing options
10901	 */
10902	protected function GetCurrentOption($sValue)
10903	{
10904		$sRet = self::USER_OPTION_DISABLED;
10905		if ($this->IsEnabled($sValue))
10906		{
10907			if ($this->GetMinUpType($sValue) == 'count')
10908			{
10909				$sRet = self::USER_OPTION_ENABLED_COUNT;
10910			}
10911			else
10912			{
10913				$sRet = self::USER_OPTION_ENABLED_PERCENT;
10914			}
10915		}
10916
10917		return $sRet;
10918	}
10919
10920	/**
10921	 * Display an option (form, or current value)
10922	 *
10923	 * @param string $sCurrentValue
10924	 * @param \WebPage $oPage
10925	 * @param string $sFormPrefix
10926	 * @param bool $bEditMode
10927	 * @param string $sUserOption
10928	 * @param bool $bSelected
10929	 *
10930	 * @return string
10931	 * @throws \CoreException
10932	 * @throws \DictExceptionMissingString
10933	 * @throws \Exception
10934	 */
10935	protected function GetDisplayOption(
10936		$sCurrentValue, $oPage, $sFormPrefix, $bEditMode, $sUserOption, $bSelected = true
10937	) {
10938		$sRet = '';
10939
10940		$iCurrentValue = $this->GetMinUpValue($sCurrentValue);
10941		if ($bEditMode)
10942		{
10943			$sValue = null;
10944			$sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id');
10945			switch ($sUserOption)
10946			{
10947				case self::USER_OPTION_DISABLED:
10948					$sValue = ''; // Empty placeholder
10949					break;
10950
10951				case self::USER_OPTION_ENABLED_COUNT:
10952					if ($bEditMode)
10953					{
10954						$sName = $sHtmlNamesPrefix.'_min_up_count';
10955						$sEditValue = $bSelected ? $iCurrentValue : '';
10956						$sValue = '<input class="redundancy-min-up-count" type="string" size="3" name="'.$sName.'" value="'.$sEditValue.'">';
10957						// To fix an issue on Firefox: focus set to the option (because the input is within the label for the option)
10958						$oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});");
10959					}
10960					else
10961					{
10962						$sValue = $iCurrentValue;
10963					}
10964					break;
10965
10966				case self::USER_OPTION_ENABLED_PERCENT:
10967					if ($bEditMode)
10968					{
10969						$sName = $sHtmlNamesPrefix.'_min_up_percent';
10970						$sEditValue = $bSelected ? $iCurrentValue : '';
10971						$sValue = '<input class="redundancy-min-up-percent" type="string" size="3" name="'.$sName.'" value="'.$sEditValue.'">';
10972						// To fix an issue on Firefox: focus set to the option (because the input is within the label for the option)
10973						$oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});");
10974					}
10975					else
10976					{
10977						$sValue = $iCurrentValue;
10978					}
10979					break;
10980			}
10981			$sLabel = sprintf($this->GetUserOptionFormat($sUserOption), $sValue,
10982				MetaModel::GetName($this->GetHostClass()));
10983
10984			$sOptionName = $sHtmlNamesPrefix.'_user_option';
10985			$sOptionId = $sOptionName.'_'.$sUserOption;
10986			$sChecked = $bSelected ? 'checked' : '';
10987			$sRet = '<input type="radio" name="'.$sOptionName.'" id="'.$sOptionId.'" value="'.$sUserOption.'" '.$sChecked.'> <label for="'.$sOptionId.'">'.$sLabel.'</label>';
10988		}
10989		else
10990		{
10991			// Read-only: display only the currently selected option
10992			if ($bSelected)
10993			{
10994				$sRet = sprintf($this->GetUserOptionFormat($sUserOption), $iCurrentValue,
10995					MetaModel::GetName($this->GetHostClass()));
10996			}
10997		}
10998
10999		return $sRet;
11000	}
11001
11002	/**
11003	 * Makes the string representation out of the values given by the form defined in GetDisplayForm
11004	 */
11005	public function ReadValueFromPostedForm($sFormPrefix)
11006	{
11007		$sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id');
11008
11009		$iMinUpCount = (int)utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_count', null, 'raw_data');
11010		$iMinUpPercent = (int)utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_percent', null, 'raw_data');
11011		$sSelectedOption = utils::ReadPostedParam($sHtmlNamesPrefix.'_user_option', null, 'raw_data');
11012		switch ($sSelectedOption)
11013		{
11014			case self::USER_OPTION_ENABLED_COUNT:
11015				$sRet = $iMinUpCount;
11016				break;
11017
11018			case self::USER_OPTION_ENABLED_PERCENT:
11019				$sRet = $iMinUpPercent.'%';
11020				break;
11021
11022			case self::USER_OPTION_DISABLED:
11023			default:
11024				$sRet = 'disabled';
11025				break;
11026		}
11027
11028		return $sRet;
11029	}
11030}
11031
11032/**
11033 * Custom fields managed by an external implementation
11034 *
11035 * @package     iTopORM
11036 */
11037class AttributeCustomFields extends AttributeDefinition
11038{
11039	const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
11040
11041	static public function ListExpectedParams()
11042	{
11043		return array_merge(parent::ListExpectedParams(), array("handler_class"));
11044	}
11045
11046	public function GetEditClass()
11047	{
11048		return "CustomFields";
11049	}
11050
11051	public function IsWritable()
11052	{
11053		return true;
11054	}
11055
11056	static public function LoadFromDB()
11057	{
11058		return false;
11059	} // See ReadValue...
11060
11061	public function GetDefaultValue(DBObject $oHostObject = null)
11062	{
11063		return new ormCustomFieldsValue($oHostObject, $this->GetCode());
11064	}
11065
11066	public function GetBasicFilterOperators()
11067	{
11068		return array();
11069	}
11070
11071	public function GetBasicFilterLooseOperator()
11072	{
11073		return '';
11074	}
11075
11076	public function GetBasicFilterSQLExpr($sOpCode, $value)
11077	{
11078		return '';
11079	}
11080
11081	/**
11082	 * @param DBObject $oHostObject
11083	 * @param array|null $aValues
11084	 *
11085	 * @return CustomFieldsHandler
11086	 */
11087	public function GetHandler($aValues = null)
11088	{
11089		$sHandlerClass = $this->Get('handler_class');
11090		$oHandler = new $sHandlerClass($this->GetCode());
11091		if (!is_null($aValues))
11092		{
11093			$oHandler->SetCurrentValues($aValues);
11094		}
11095
11096		return $oHandler;
11097	}
11098
11099	public function GetPrerequisiteAttributes($sClass = null)
11100	{
11101		$sHandlerClass = $this->Get('handler_class');
11102
11103		return $sHandlerClass::GetPrerequisiteAttributes($sClass);
11104	}
11105
11106	public function GetEditValue($sValue, $oHostObj = null)
11107	{
11108		return $this->GetForTemplate($sValue, '', $oHostObj, true);
11109	}
11110
11111	/**
11112	 * Makes the string representation out of the values given by the form defined in GetDisplayForm
11113	 */
11114	public function ReadValueFromPostedForm($oHostObject, $sFormPrefix)
11115	{
11116		$aRawData = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$this->GetCode()}", '{}', 'raw_data'),
11117			true);
11118
11119		return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aRawData);
11120	}
11121
11122	public function MakeRealValue($proposedValue, $oHostObject)
11123	{
11124		if (is_object($proposedValue) && ($proposedValue instanceof ormCustomFieldsValue))
11125		{
11126			return $proposedValue;
11127		}
11128		elseif (is_string($proposedValue))
11129		{
11130			$aValues = json_decode($proposedValue, true);
11131
11132			return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues);
11133		}
11134		elseif (is_array($proposedValue))
11135		{
11136			return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $proposedValue);
11137		}
11138		elseif (is_null($proposedValue))
11139		{
11140			return new ormCustomFieldsValue($oHostObject, $this->GetCode());
11141		}
11142		throw new Exception('Unexpected type for the value of a custom fields attribute: '.gettype($proposedValue));
11143	}
11144
11145	static public function GetFormFieldClass()
11146	{
11147		return '\\Combodo\\iTop\\Form\\Field\\SubFormField';
11148	}
11149
11150	/**
11151	 * Override to build the relevant form field
11152	 *
11153	 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the
11154	 * $oFormField is passed, MakeFormField behaves more like a Prepare.
11155	 */
11156	public function MakeFormField(DBObject $oObject, $oFormField = null)
11157	{
11158		if ($oFormField === null)
11159		{
11160			$sFormFieldClass = static::GetFormFieldClass();
11161			$oFormField = new $sFormFieldClass($this->GetCode());
11162			$oFormField->SetForm($this->GetForm($oObject));
11163		}
11164		parent::MakeFormField($oObject, $oFormField);
11165
11166		return $oFormField;
11167	}
11168
11169	/**
11170	 * @param DBObject $oHostObject
11171	 * @param null $sFormPrefix
11172	 *
11173	 * @return Combodo\iTop\Form\Form
11174	 * @throws \Exception
11175	 */
11176	public function GetForm(DBObject $oHostObject, $sFormPrefix = null)
11177	{
11178		try
11179		{
11180			$oValue = $oHostObject->Get($this->GetCode());
11181			$oHandler = $this->GetHandler($oValue->GetValues());
11182			$sFormId = is_null($sFormPrefix) ? 'cf_'.$this->GetCode() : $sFormPrefix.'_cf_'.$this->GetCode();
11183			$oHandler->BuildForm($oHostObject, $sFormId);
11184			$oForm = $oHandler->GetForm();
11185		} catch (Exception $e)
11186		{
11187			$oForm = new \Combodo\iTop\Form\Form('');
11188			$oField = new \Combodo\iTop\Form\Field\LabelField('');
11189			$oField->SetLabel('Custom field error: '.$e->getMessage());
11190			$oForm->AddField($oField);
11191			$oForm->Finalize();
11192		}
11193
11194		return $oForm;
11195	}
11196
11197	/**
11198	 * Read the data from where it has been stored. This verb must be implemented as soon as LoadFromDB returns false
11199	 * and LoadInObject returns true
11200	 *
11201	 * @param $oHostObject
11202	 *
11203	 * @return ormCustomFieldsValue
11204	 */
11205	public function ReadValue($oHostObject)
11206	{
11207		try
11208		{
11209			$oHandler = $this->GetHandler();
11210			$aValues = $oHandler->ReadValues($oHostObject);
11211			$oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues);
11212		} catch (Exception $e)
11213		{
11214			$oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode());
11215		}
11216
11217		return $oRet;
11218	}
11219
11220	/**
11221	 * Record the data (currently in the processing of recording the host object)
11222	 * It is assumed that the data has been checked prior to calling Write()
11223	 *
11224	 * @param DBObject $oHostObject
11225	 * @param ormCustomFieldsValue|null $oValue (null is the default value)
11226	 */
11227	public function WriteValue(DBObject $oHostObject, ormCustomFieldsValue $oValue = null)
11228	{
11229		if (is_null($oValue))
11230		{
11231			$oHandler = $this->GetHandler();
11232			$aValues = array();
11233		}
11234		else
11235		{
11236			// Pass the values through the form to make sure that they are correct
11237			$oHandler = $this->GetHandler($oValue->GetValues());
11238			$oHandler->BuildForm($oHostObject, '');
11239			$oForm = $oHandler->GetForm();
11240			$aValues = $oForm->GetCurrentValues();
11241		}
11242
11243		return $oHandler->WriteValues($oHostObject, $aValues);
11244	}
11245
11246	/**
11247	 * The part of the current attribute in the object's signature, for the supplied value
11248	 *
11249	 * @param ormCustomFieldsValue $value The value of this attribute for the object
11250	 *
11251	 * @return string The "signature" for this field/attribute
11252	 */
11253	public function Fingerprint($value)
11254	{
11255		$oHandler = $this->GetHandler($value->GetValues());
11256
11257		return $oHandler->GetValueFingerprint();
11258	}
11259
11260	/**
11261	 * Check the validity of the data
11262	 *
11263	 * @param DBObject $oHostObject
11264	 * @param $value
11265	 *
11266	 * @return bool|string true or error message
11267	 */
11268	public function CheckValue(DBObject $oHostObject, $value)
11269	{
11270		try
11271		{
11272			$oHandler = $this->GetHandler($value->GetValues());
11273			$oHandler->BuildForm($oHostObject, '');
11274			$oForm = $oHandler->GetForm();
11275			$oForm->Validate();
11276			if ($oForm->GetValid())
11277			{
11278				$ret = true;
11279			}
11280			else
11281			{
11282				$aMessages = array();
11283				foreach($oForm->GetErrorMessages() as $sFieldId => $aFieldMessages)
11284				{
11285					$aMessages[] = $sFieldId.': '.implode(', ', $aFieldMessages);
11286				}
11287				$ret = 'Invalid value: '.implode(', ', $aMessages);
11288			}
11289		} catch (Exception $e)
11290		{
11291			$ret = $e->getMessage();
11292		}
11293
11294		return $ret;
11295	}
11296
11297	/**
11298	 * Cleanup data upon object deletion (object id still available here)
11299	 *
11300	 * @param DBObject $oHostObject
11301	 *
11302	 * @return
11303	 * @throws \CoreException
11304	 */
11305	public function DeleteValue(DBObject $oHostObject)
11306	{
11307		$oValue = $oHostObject->Get($this->GetCode());
11308		$oHandler = $this->GetHandler($oValue->GetValues());
11309
11310		return $oHandler->DeleteValues($oHostObject);
11311	}
11312
11313	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
11314	{
11315		try
11316		{
11317			$sRet = $value->GetAsHTML($bLocalize);
11318		} catch (Exception $e)
11319		{
11320			$sRet = 'Custom field error: '.htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8');
11321		}
11322
11323		return $sRet;
11324	}
11325
11326	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
11327	{
11328		try
11329		{
11330			$sRet = $value->GetAsXML($bLocalize);
11331		} catch (Exception $e)
11332		{
11333			$sRet = Str::pure2xml('Custom field error: '.$e->getMessage());
11334		}
11335
11336		return $sRet;
11337	}
11338
11339	public function GetAsCSV(
11340		$value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
11341		$bConvertToPlainText = false
11342	) {
11343		try
11344		{
11345			$sRet = $value->GetAsCSV($sSeparator, $sTextQualifier, $bLocalize, $bConvertToPlainText);
11346		} catch (Exception $e)
11347		{
11348			$sFrom = array("\r\n", $sTextQualifier);
11349			$sTo = array("\n", $sTextQualifier.$sTextQualifier);
11350			$sEscaped = str_replace($sFrom, $sTo, 'Custom field error: '.$e->getMessage());
11351			$sRet = $sTextQualifier.$sEscaped.$sTextQualifier;
11352		}
11353
11354		return $sRet;
11355	}
11356
11357	/**
11358	 * List the available verbs for 'GetForTemplate'
11359	 */
11360	public function EnumTemplateVerbs()
11361	{
11362		$sHandlerClass = $this->Get('handler_class');
11363
11364		return $sHandlerClass::EnumTemplateVerbs();
11365	}
11366
11367	/**
11368	 * Get various representations of the value, for insertion into a template (e.g. in Notifications)
11369	 *
11370	 * @param $value mixed The current value of the field
11371	 * @param $sVerb string The verb specifying the representation of the value
11372	 * @param $oHostObject DBObject The object
11373	 * @param $bLocalize bool Whether or not to localize the value
11374	 *
11375	 * @return string
11376	 */
11377	public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
11378	{
11379		try
11380		{
11381			$sRet = $value->GetForTemplate($sVerb, $bLocalize);
11382		} catch (Exception $e)
11383		{
11384			$sRet = 'Custom field error: '.$e->getMessage();
11385		}
11386
11387		return $sRet;
11388	}
11389
11390	public function MakeValueFromString(
11391		$sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
11392		$sAttributeQualifier = null
11393	) {
11394		return null;
11395	}
11396
11397	/**
11398	 * Helper to get a value that will be JSON encoded
11399	 * The operation is the opposite to FromJSONToValue
11400	 *
11401	 * @param $value
11402	 *
11403	 * @return string
11404	 */
11405	public function GetForJSON($value)
11406	{
11407		return null;
11408	}
11409
11410	/**
11411	 * Helper to form a value, given JSON decoded data
11412	 * The operation is the opposite to GetForJSON
11413	 *
11414	 * @param string $json
11415	 *
11416	 * @return array
11417	 */
11418	public function FromJSONToValue($json)
11419	{
11420		return null;
11421	}
11422
11423	public function Equals($val1, $val2)
11424	{
11425		try
11426		{
11427			$bEquals = $val1->Equals($val2);
11428		} catch (Exception $e)
11429		{
11430			$bEquals = false;
11431		}
11432
11433		return $bEquals;
11434	}
11435}
11436
11437class AttributeArchiveFlag extends AttributeBoolean
11438{
11439	public function __construct($sCode)
11440	{
11441		parent::__construct($sCode, array(
11442			"allowed_values" => null,
11443			"sql" => $sCode,
11444			"default_value" => false,
11445			"is_null_allowed" => false,
11446			"depends_on" => array()
11447		));
11448	}
11449
11450	public function RequiresIndex()
11451	{
11452		return true;
11453	}
11454
11455	public function CopyOnAllTables()
11456	{
11457		return true;
11458	}
11459
11460	public function IsWritable()
11461	{
11462		return false;
11463	}
11464
11465	public function IsMagic()
11466	{
11467		return true;
11468	}
11469
11470	public function GetLabel($sDefault = null)
11471	{
11472		$sDefault = Dict::S('Core:AttributeArchiveFlag/Label', $sDefault);
11473
11474		return parent::GetLabel($sDefault);
11475	}
11476
11477	public function GetDescription($sDefault = null)
11478	{
11479		$sDefault = Dict::S('Core:AttributeArchiveFlag/Label+', $sDefault);
11480
11481		return parent::GetDescription($sDefault);
11482	}
11483}
11484
11485class AttributeArchiveDate extends AttributeDate
11486{
11487	public function GetLabel($sDefault = null)
11488	{
11489		$sDefault = Dict::S('Core:AttributeArchiveDate/Label', $sDefault);
11490
11491		return parent::GetLabel($sDefault);
11492	}
11493
11494	public function GetDescription($sDefault = null)
11495	{
11496		$sDefault = Dict::S('Core:AttributeArchiveDate/Label+', $sDefault);
11497
11498		return parent::GetDescription($sDefault);
11499	}
11500}
11501
11502class AttributeObsolescenceFlag extends AttributeBoolean
11503{
11504	public function __construct($sCode)
11505	{
11506		parent::__construct($sCode, array(
11507			"allowed_values" => null,
11508			"sql" => $sCode,
11509			"default_value" => "",
11510			"is_null_allowed" => false,
11511			"depends_on" => array()
11512		));
11513	}
11514
11515	public function IsWritable()
11516	{
11517		return false;
11518	}
11519
11520	public function IsMagic()
11521	{
11522		return true;
11523	}
11524
11525	static public function IsBasedOnDBColumns()
11526	{
11527		return false;
11528	}
11529
11530	/**
11531	 * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via
11532	 * GetOQLExpression)
11533	 *
11534	 * @return bool
11535	 */
11536	static public function IsBasedOnOQLExpression()
11537	{
11538		return true;
11539	}
11540
11541	public function GetOQLExpression()
11542	{
11543		return MetaModel::GetObsolescenceExpression($this->GetHostClass());
11544	}
11545
11546	public function GetSQLExpressions($sPrefix = '')
11547	{
11548		return array();
11549	}
11550
11551	public function GetSQLColumns($bFullSpec = false)
11552	{
11553		return array();
11554	} // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation)
11555
11556	public function GetSQLValues($value)
11557	{
11558		return array();
11559	} // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update)
11560
11561	public function GetEditClass()
11562	{
11563		return "";
11564	}
11565
11566	public function GetValuesDef()
11567	{
11568		return null;
11569	}
11570
11571	public function GetPrerequisiteAttributes($sClass = null)
11572	{
11573		return $this->GetOptional("depends_on", array());
11574	}
11575
11576	public function IsDirectField()
11577	{
11578		return true;
11579	}
11580
11581	static public function IsScalar()
11582	{
11583		return true;
11584	}
11585
11586	public function GetSQLExpr()
11587	{
11588		return null;
11589	}
11590
11591	public function GetDefaultValue(DBObject $oHostObject = null)
11592	{
11593		return $this->MakeRealValue("", $oHostObject);
11594	}
11595
11596	public function IsNullAllowed()
11597	{
11598		return false;
11599	}
11600
11601	public function GetLabel($sDefault = null)
11602	{
11603		$sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label', $sDefault);
11604
11605		return parent::GetLabel($sDefault);
11606	}
11607
11608	public function GetDescription($sDefault = null)
11609	{
11610		$sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label+', $sDefault);
11611
11612		return parent::GetDescription($sDefault);
11613	}
11614}
11615
11616class AttributeObsolescenceDate extends AttributeDate
11617{
11618	public function GetLabel($sDefault = null)
11619	{
11620		$sDefault = Dict::S('Core:AttributeObsolescenceDate/Label', $sDefault);
11621
11622		return parent::GetLabel($sDefault);
11623	}
11624
11625	public function GetDescription($sDefault = null)
11626	{
11627		$sDefault = Dict::S('Core:AttributeObsolescenceDate/Label+', $sDefault);
11628
11629		return parent::GetDescription($sDefault);
11630	}
11631}
11632