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
20require_once(APPROOT.'core/modulehandler.class.inc.php');
21require_once(APPROOT.'core/querybuildercontext.class.inc.php');
22require_once(APPROOT.'core/querymodifier.class.inc.php');
23require_once(APPROOT.'core/metamodelmodifier.inc.php');
24require_once(APPROOT.'core/computing.inc.php');
25require_once(APPROOT.'core/relationgraph.class.inc.php');
26require_once(APPROOT.'core/apc-compat.php');
27require_once(APPROOT.'core/expressioncache.class.inc.php');
28
29/**
30 * Metamodel
31 *
32 * @copyright   Copyright (C) 2010-2018 Combodo SARL
33 * @license     http://opensource.org/licenses/AGPL-3.0
34 */
35
36/**
37 * @package     iTopORM
38 */
39define('ENUM_PARENT_CLASSES_EXCLUDELEAF', 1);
40/**
41 * @package     iTopORM
42 */
43define('ENUM_PARENT_CLASSES_ALL', 2);
44
45/**
46 * Specifies that this attribute is visible/editable.... normal (default config)
47 *
48 * @package     iTopORM
49 */
50define('OPT_ATT_NORMAL', 0);
51/**
52 * Specifies that this attribute is hidden in that state
53 *
54 * @package     iTopORM
55 */
56define('OPT_ATT_HIDDEN', 1);
57/**
58 * Specifies that this attribute is not editable in that state
59 *
60 * @package     iTopORM
61 */
62define('OPT_ATT_READONLY', 2);
63/**
64 * Specifieds that the attribute must be set (different than default value?) when arriving into that state
65 *
66 * @package     iTopORM
67 */
68define('OPT_ATT_MANDATORY', 4);
69/**
70 * Specifies that the attribute must change when arriving into that state
71 *
72 * @package     iTopORM
73 */
74define('OPT_ATT_MUSTCHANGE', 8);
75/**
76 * Specifies that the attribute must be proposed when arriving into that state
77 *
78 * @package     iTopORM
79 */
80define('OPT_ATT_MUSTPROMPT', 16);
81/**
82 * Specifies that the attribute is in 'slave' mode compared to one data exchange task:
83 * it should not be edited inside iTop anymore
84 *
85 * @package     iTopORM
86 */
87define('OPT_ATT_SLAVE', 32);
88
89/**
90 * DB Engine -should be moved into CMDBSource
91 *
92 * Used to be myisam, the switch was made with r798
93 *
94 * @package     iTopORM
95 */
96define('MYSQL_ENGINE', 'innodb');
97
98
99/**
100 * (API) The objects definitions as well as their mapping to the database
101 *
102 * @package     iTopORM
103 */
104abstract class MetaModel
105{
106	///////////////////////////////////////////////////////////////////////////
107	//
108	// STATIC Members
109	//
110	///////////////////////////////////////////////////////////////////////////
111
112	/** @var bool */
113	private static $m_bTraceSourceFiles = false;
114	/** @var array */
115	private static $m_aClassToFile = array();
116	/** @var string */
117	protected static $m_sEnvironment = 'production';
118
119	/**
120	 * @return array
121	 */
122	public static function GetClassFiles()
123	{
124		return self::$m_aClassToFile;
125	}
126
127	//
128
129	/**
130	 * Purpose: workaround the following limitation = PHP5 does not allow to know the class (derived
131	 * from the current one) from which a static function is called (__CLASS__ and self are
132	 * interpreted during parsing)
133	 *
134	 * @param string $sExpectedFunctionName
135	 * @param bool $bRecordSourceFile
136	 *
137	 * @return string
138	 */
139	private static function GetCallersPHPClass($sExpectedFunctionName = null, $bRecordSourceFile = false)
140	{
141		$aBacktrace = debug_backtrace();
142		// $aBacktrace[0] is where we are
143		// $aBacktrace[1] is the caller of GetCallersPHPClass
144		// $aBacktrace[1] is the info we want
145		if (!empty($sExpectedFunctionName))
146		{
147			assert($aBacktrace[2]['function'] == $sExpectedFunctionName);
148		}
149		if ($bRecordSourceFile)
150		{
151			self::$m_aClassToFile[$aBacktrace[2]["class"]] = $aBacktrace[1]["file"];
152		}
153		return $aBacktrace[2]["class"];
154	}
155
156	// Static init -why and how it works
157	//
158	// We found the following limitations:
159	//- it is not possible to define non scalar constants
160	//- it is not possible to declare a static variable as '= new myclass()'
161	// Then we had do propose this model, in which a derived (non abstract)
162	// class should implement Init(), to call InheritAttributes or AddAttribute.
163
164	/**
165	 * @param string $sClass
166	 *
167	 * @throws \CoreException
168	 */
169	private static function _check_subclass($sClass)
170	{
171		// See also IsValidClass()... ???? #@#
172		// class is mandatory
173		// (it is not possible to guess it when called as myderived::...)
174		if (!array_key_exists($sClass, self::$m_aClassParams))
175		{
176			throw new CoreException("Unknown class '$sClass'");
177		}
178	}
179
180	public static function static_var_dump()
181	{
182		var_dump(get_class_vars(__CLASS__));
183	}
184
185	/** @var Config m_oConfig */
186	private static $m_oConfig = null;
187	/** @var array */
188	protected static $m_aModulesParameters = array();
189
190	/** @var bool */
191	private static $m_bSkipCheckToWrite = false;
192	/** @var bool */
193	private static $m_bSkipCheckExtKeys = false;
194
195	/** @var bool */
196	private static $m_bUseAPCCache = false;
197
198	/** @var bool */
199	private static $m_bLogIssue = false;
200	/** @var bool */
201	private static $m_bLogNotification = false;
202	/** @var bool */
203	private static $m_bLogWebService = false;
204
205	/**
206	 * @return bool the current flag value
207	 */
208	public static function SkipCheckToWrite()
209	{
210		return self::$m_bSkipCheckToWrite;
211	}
212
213	/**
214	 * @return bool the current flag value
215	 */
216	public static function SkipCheckExtKeys()
217	{
218		return self::$m_bSkipCheckExtKeys;
219	}
220
221	/**
222	 * @return bool the current flag value
223	 */
224	public static function IsLogEnabledIssue()
225	{
226		return self::$m_bLogIssue;
227	}
228
229	/**
230	 * @return bool the current flag value
231	 */
232	public static function IsLogEnabledNotification()
233	{
234		return self::$m_bLogNotification;
235	}
236
237	/**
238	 * @return bool the current flag value
239	 */
240	public static function IsLogEnabledWebService()
241	{
242		return self::$m_bLogWebService;
243	}
244
245	/** @var string */
246	private static $m_sDBName = "";
247	/**
248	 * table prefix for the current application instance (allow several applications on the same DB)
249	 *
250	 * @var string
251	 */
252	private static $m_sTablePrefix = "";
253	/** @var array */
254	private static $m_Category2Class = array();
255	/**
256	 * array of "classname" => "rootclass"
257	 *
258	 * @var array
259	 */
260	private static $m_aRootClasses = array();
261	/**
262	 * array of ("classname" => array of "parentclass")
263	 *
264	 * @var array
265	 */
266	private static $m_aParentClasses = array();
267	/**
268	 * array of ("classname" => array of "childclass")
269	 *
270	 * @var array
271	 */
272	private static $m_aChildClasses = array();
273
274	/**
275	 * array of ("classname" => array of class information)
276	 *
277	 * @var array
278	 */
279	private static $m_aClassParams = array();
280	/**
281	 * array of ("classname" => array of highlightscale information)
282	 *
283	 * @var array
284	 */
285	private static $m_aHighlightScales = array();
286
287	/**
288	 * @param string $sRefClass
289	 *
290	 * @return string
291	 */
292	static public function GetParentPersistentClass($sRefClass)
293	{
294		$sClass = get_parent_class($sRefClass);
295		if (!$sClass)
296		{
297			return '';
298		}
299
300		if ($sClass == 'DBObject')
301		{
302			return '';
303		} // Warning: __CLASS__ is lower case in my version of PHP
304
305		// Note: the UI/business model may implement pure PHP classes (intermediate layers)
306		if (array_key_exists($sClass, self::$m_aClassParams))
307		{
308			return $sClass;
309		}
310		return self::GetParentPersistentClass($sClass);
311	}
312
313	/**
314	 * @param string $sClass
315	 *
316	 * @return string
317	 * @throws \CoreException
318	 * @throws \DictExceptionMissingString
319	 */
320	final static public function GetName($sClass)
321	{
322		self::_check_subclass($sClass);
323		return $sClass::GetClassName($sClass);
324	}
325
326	/**
327	 * @param string $sClass
328	 *
329	 * @return string
330	 * @throws \CoreException
331	 * @throws \DictExceptionMissingString
332	 */
333	final static public function GetName_Obsolete($sClass)
334	{
335		// Written for compatibility with a data model written prior to version 0.9.1
336		self::_check_subclass($sClass);
337		if (array_key_exists('name', self::$m_aClassParams[$sClass]))
338		{
339			return self::$m_aClassParams[$sClass]['name'];
340		}
341		else
342		{
343			return self::GetName($sClass);
344		}
345	}
346
347	/**
348	 * @param string $sClassLabel
349	 * @param bool $bCaseSensitive
350	 *
351	 * @return null
352	 * @throws \CoreException
353	 * @throws \DictExceptionMissingString
354	 */
355	final static public function GetClassFromLabel($sClassLabel, $bCaseSensitive = true)
356	{
357		foreach(self::GetClasses() as $sClass)
358		{
359			if ($bCaseSensitive)
360			{
361				if (self::GetName($sClass) == $sClassLabel)
362				{
363					return $sClass;
364				}
365			}
366			else
367			{
368				if (strcasecmp(self::GetName($sClass), $sClassLabel) == 0)
369				{
370					return $sClass;
371				}
372			}
373		}
374
375		return null;
376	}
377
378	/**
379	 * @param string $sClass
380	 *
381	 * @return string
382	 * @throws \CoreException
383	 */
384	final static public function GetCategory($sClass)
385	{
386		self::_check_subclass($sClass);
387		return self::$m_aClassParams[$sClass]["category"];
388	}
389
390	/**
391	 * @param string $sClass
392	 * @param string $sCategory
393	 *
394	 * @return bool
395	 * @throws \CoreException
396	 */
397	final static public function HasCategory($sClass, $sCategory)
398	{
399		self::_check_subclass($sClass);
400		return (strpos(self::$m_aClassParams[$sClass]["category"], $sCategory) !== false);
401	}
402
403	/**
404	 * @param string $sClass
405	 *
406	 * @return string
407	 * @throws \CoreException
408	 * @throws \DictExceptionMissingString
409	 */
410	final static public function GetClassDescription($sClass)
411	{
412		self::_check_subclass($sClass);
413		return $sClass::GetClassDescription($sClass);
414	}
415
416	/**
417	 * @param string $sClass
418	 *
419	 * @return string
420	 * @throws \CoreException
421	 * @throws \DictExceptionMissingString
422	 */
423	final static public function GetClassDescription_Obsolete($sClass)
424	{
425		// Written for compatibility with a data model written prior to version 0.9.1
426		self::_check_subclass($sClass);
427		if (array_key_exists('description', self::$m_aClassParams[$sClass]))
428		{
429			return self::$m_aClassParams[$sClass]['description'];
430		}
431		else
432		{
433			return self::GetClassDescription($sClass);
434		}
435	}
436
437	/**
438	 * @param string $sClass
439	 * @param bool $bImgTag
440	 * @param string $sMoreStyles
441	 *
442	 * @return string
443	 * @throws \CoreException
444	 */
445	final static public function GetClassIcon($sClass, $bImgTag = true, $sMoreStyles = '')
446	{
447		self::_check_subclass($sClass);
448
449		$sIcon = '';
450		if (array_key_exists('icon', self::$m_aClassParams[$sClass]))
451		{
452			$sIcon = self::$m_aClassParams[$sClass]['icon'];
453		}
454		if (strlen($sIcon) == 0)
455		{
456			$sParentClass = self::GetParentPersistentClass($sClass);
457			if (strlen($sParentClass) > 0)
458			{
459				return self::GetClassIcon($sParentClass, $bImgTag, $sMoreStyles);
460			}
461		}
462		$sIcon = str_replace('/modules/', '/env-'.self::$m_sEnvironment.'/', $sIcon); // Support of pre-2.0 modules
463		if ($bImgTag && ($sIcon != ''))
464		{
465			$sIcon = "<img src=\"$sIcon\" style=\"vertical-align:middle;$sMoreStyles\"/>";
466		}
467
468		return $sIcon;
469	}
470
471	/**
472	 * @param string $sClass
473	 *
474	 * @return bool
475	 * @throws \CoreException
476	 */
477	final static public function IsAutoIncrementKey($sClass)
478	{
479		self::_check_subclass($sClass);
480		return (self::$m_aClassParams[$sClass]["key_type"] == "autoincrement");
481	}
482
483	/**
484	 * @param string $sClass
485	 *
486	 * @return bool
487	 * @throws \CoreException
488	 */
489	final static public function IsArchivable($sClass)
490	{
491		self::_check_subclass($sClass);
492		return self::$m_aClassParams[$sClass]["archive"];
493	}
494
495	/**
496	 * @param string $sClass
497	 *
498	 * @return bool
499	 * @throws \CoreException
500	 */
501	final static public function IsObsoletable($sClass)
502	{
503		self::_check_subclass($sClass);
504		return (!is_null(self::$m_aClassParams[$sClass]['obsolescence_expression']));
505	}
506
507	/**
508	 * @param string $sClass
509	 *
510	 * @return \Expression
511	 * @throws \CoreException
512	 */
513	final static public function GetObsolescenceExpression($sClass)
514	{
515		if (self::IsObsoletable($sClass))
516		{
517			self::_check_subclass($sClass);
518			$sOql = self::$m_aClassParams[$sClass]['obsolescence_expression'];
519			$oRet = Expression::FromOQL("COALESCE($sOql, 0)");
520		}
521		else
522		{
523			$oRet = Expression::FromOQL("0");
524		}
525
526		return $oRet;
527	}
528
529	/**
530	 * @param string $sClass
531	 * @param bool $bClassDefinitionOnly if true then will only return properties defined in the specified class on not the properties
532	 *                      from its parent classes
533	 *
534	 * @return array rule id as key, rule properties as value
535	 * @throws \CoreException
536	 * @since 2.6 N°659 uniqueness constraint
537	 * @see #SetUniquenessRuleRootClass that fixes a specific 'root_class' property to know which class is root per rule
538	 */
539	final public static function GetUniquenessRules($sClass, $bClassDefinitionOnly = false)
540	{
541		if (!isset(self::$m_aClassParams[$sClass]))
542		{
543			return array();
544		}
545
546		$aCurrentUniquenessRules = array();
547
548		if (array_key_exists('uniqueness_rules', self::$m_aClassParams[$sClass]))
549		{
550			$aCurrentUniquenessRules = self::$m_aClassParams[$sClass]['uniqueness_rules'];
551		}
552
553		if ($bClassDefinitionOnly)
554		{
555			return $aCurrentUniquenessRules;
556		}
557
558		$sParentClass = self::GetParentClass($sClass);
559		if ($sParentClass)
560		{
561			$aParentUniquenessRules = self::GetUniquenessRules($sParentClass);
562			foreach ($aParentUniquenessRules as $sUniquenessRuleId => $aParentUniquenessRuleProperties)
563			{
564				$bCopyDisabledKey = true;
565				$bCurrentDisabledValue = null;
566
567				if (array_key_exists($sUniquenessRuleId, $aCurrentUniquenessRules))
568				{
569					if (self::IsUniquenessRuleContainingOnlyDisabledKey($aCurrentUniquenessRules[$sUniquenessRuleId]))
570					{
571						$bCopyDisabledKey = false;
572					}
573					else
574					{
575						continue;
576					}
577				}
578
579				$aMergedUniquenessProperties = $aParentUniquenessRuleProperties;
580				if (!$bCopyDisabledKey)
581				{
582					$aMergedUniquenessProperties['disabled'] = $aCurrentUniquenessRules[$sUniquenessRuleId]['disabled'];
583				}
584				$aCurrentUniquenessRules[$sUniquenessRuleId] = $aMergedUniquenessProperties;
585			}
586		}
587
588		return $aCurrentUniquenessRules;
589	}
590
591	/**
592	 * @param string $sRootClass
593	 * @param string $sRuleId
594	 *
595	 * @throws \CoreException
596	 * @since 2.6.1 N°1918 (sous les pavés, la plage) initialize in 'root_class' property the class that has the first
597	 *         definition of the rule in the hierarchy
598	 */
599	final private static function SetUniquenessRuleRootClass($sRootClass, $sRuleId)
600	{
601		foreach (self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL) as $sClass)
602		{
603			self::$m_aClassParams[$sClass]['uniqueness_rules'][$sRuleId]['root_class'] = $sClass;
604		}
605	}
606
607	/**
608	 * @param string $sRuleId
609	 * @param string $sLeafClassName
610	 *
611	 * @return string name of the class, null if not present
612	 */
613	final public static function GetRootClassForUniquenessRule($sRuleId, $sLeafClassName)
614	{
615		$sFirstClassWithRuleId = null;
616		if (isset(self::$m_aClassParams[$sLeafClassName]['uniqueness_rules'][$sRuleId]))
617		{
618			$sFirstClassWithRuleId = $sLeafClassName;
619		}
620
621		$sParentClass = self::GetParentClass($sLeafClassName);
622		if ($sParentClass)
623		{
624			$sParentClassWithRuleId = self::GetRootClassForUniquenessRule($sRuleId, $sParentClass);
625			if (!is_null($sParentClassWithRuleId))
626			{
627				$sFirstClassWithRuleId = $sParentClassWithRuleId;
628			}
629		}
630
631		return $sFirstClassWithRuleId;
632	}
633
634	/**
635	 * @param string $sRootClass
636	 * @param string $sRuleId
637	 *
638	 * @return string[] child classes with the rule disabled, and that are concrete classes
639	 *
640	 * @throws \CoreException
641	 * @since 2.6.1 N°1968 (soyez réalistes, demandez l'impossible)
642	 */
643	final public static function GetChildClassesWithDisabledUniquenessRule($sRootClass, $sRuleId)
644	{
645		$aClassesWithDisabledRule = array();
646		foreach (self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_EXCLUDETOP) as $sChildClass)
647		{
648			if (array_key_exists($sChildClass, $aClassesWithDisabledRule))
649			{
650				continue;
651			}
652			if (!array_key_exists('uniqueness_rules', self::$m_aClassParams[$sChildClass]))
653			{
654				continue;
655			}
656			if (!array_key_exists($sRuleId, self::$m_aClassParams[$sChildClass]['uniqueness_rules']))
657			{
658				continue;
659			}
660
661			if (self::$m_aClassParams[$sChildClass]['uniqueness_rules'][$sRuleId]['disabled'] === true)
662			{
663				$aDisabledClassChildren = self::EnumChildClasses($sChildClass, ENUM_CHILD_CLASSES_ALL);
664				foreach ($aDisabledClassChildren as $sDisabledClassChild)
665				{
666					if (!self::IsAbstract($sDisabledClassChild))
667					{
668						$aClassesWithDisabledRule[] = $sDisabledClassChild;
669					}
670				}
671			}
672		}
673
674		return $aClassesWithDisabledRule;
675	}
676
677	/**
678	 * @param array $aRuleProperties
679	 *
680	 * @return bool
681	 * @since 2.6 N°659 uniqueness constraint
682	 */
683	private static function IsUniquenessRuleContainingOnlyDisabledKey($aRuleProperties)
684	{
685		$aNonNullRuleProperties = array_filter($aRuleProperties, function ($v) {
686			return (!is_null($v));
687		});
688
689		return ((count($aNonNullRuleProperties) == 1) && (array_key_exists('disabled', $aNonNullRuleProperties)));
690	}
691
692
693	/**
694	 * @param string $sClass
695	 *
696	 * @return array
697	 * @throws \CoreException
698	 * @throws \DictExceptionMissingString
699	 */
700	final static public function GetNameSpec($sClass)
701	{
702		self::_check_subclass($sClass);
703		$nameRawSpec = self::$m_aClassParams[$sClass]["name_attcode"];
704		if (is_array($nameRawSpec))
705		{
706			$sFormat = Dict::S("Class:$sClass/Name", '');
707			if (strlen($sFormat) == 0)
708			{
709				// Default to "%1$s %2$s..."
710				for($i = 1; $i <= count($nameRawSpec); $i++)
711				{
712					if (empty($sFormat))
713					{
714						$sFormat .= '%'.$i.'$s';
715					}
716					else
717					{
718						$sFormat .= ' %'.$i.'$s';
719					}
720				}
721			}
722			return array($sFormat, $nameRawSpec);
723		}
724		elseif (empty($nameRawSpec))
725		{
726			return array($sClass, array());
727		}
728		else
729		{
730			// string -> attcode
731			return array('%1$s', array($nameRawSpec));
732		}
733	}
734
735	/**
736	 * Get the friendly name expression for a given class
737	 *
738	 * @param string $sClass
739	 *
740	 * @return Expression
741	 * @throws \CoreException
742	 * @throws \DictExceptionMissingString
743	 */
744	final static public function GetNameExpression($sClass)
745	{
746		$aNameSpec = self::GetNameSpec($sClass);
747		$sFormat = $aNameSpec[0];
748		$aAttributes = $aNameSpec[1];
749
750		$aPieces = preg_split('/%([0-9])\\$s/', $sFormat, -1, PREG_SPLIT_DELIM_CAPTURE);
751		$aExpressions = array();
752		foreach($aPieces as $i => $sPiece)
753		{
754			if ($i & 1)
755			{
756				// $i is ODD - sPiece is a delimiter
757				//
758				$iReplacement = (int)$sPiece - 1;
759
760				if (isset($aAttributes[$iReplacement]))
761				{
762					$sAttCode = $aAttributes[$iReplacement];
763					$aExpressions[] = new FieldExpression($sAttCode);
764				}
765			}
766			else
767			{
768				// $i is EVEN - sPiece is a literal
769				//
770				if (strlen($sPiece) > 0)
771				{
772					$aExpressions[] = new ScalarExpression($sPiece);
773				}
774			}
775		}
776
777		return new CharConcatExpression($aExpressions);
778	}
779
780	/**
781	 * @param string $sClass
782	 *
783	 * @return string The friendly name IIF it is equivalent to a single attribute
784	 * @throws \CoreException
785	 * @throws \DictExceptionMissingString
786	 */
787	final static public function GetFriendlyNameAttributeCode($sClass)
788	{
789		$aNameSpec = self::GetNameSpec($sClass);
790		$sFormat = trim($aNameSpec[0]);
791		$aAttributes = $aNameSpec[1];
792		if (($sFormat != '') && ($sFormat != '%1$s'))
793		{
794			return null;
795		}
796		if (count($aAttributes) > 1)
797		{
798			return null;
799		}
800		return reset($aAttributes);
801	}
802
803	/**
804	 * Returns the list of attributes composing the friendlyname
805	 *
806	 * @param $sClass
807	 *
808	 * @return array
809	 */
810	final static public function GetFriendlyNameAttributeCodeList($sClass)
811	{
812		$aNameSpec = self::GetNameSpec($sClass);
813		$aAttributes = $aNameSpec[1];
814		return $aAttributes;
815	}
816
817	/**
818	 * @param string $sClass
819	 *
820	 * @return string
821	 * @throws \CoreException
822	 */
823	final static public function GetStateAttributeCode($sClass)
824	{
825		self::_check_subclass($sClass);
826		return self::$m_aClassParams[$sClass]["state_attcode"];
827	}
828
829	/**
830	 * @param string $sClass
831	 *
832	 * @return string
833	 * @throws \CoreException
834	 * @throws \Exception
835	 */
836	final static public function GetDefaultState($sClass)
837	{
838		$sDefaultState = '';
839		$sStateAttrCode = self::GetStateAttributeCode($sClass);
840		if (!empty($sStateAttrCode))
841		{
842			$oStateAttrDef = self::GetAttributeDef($sClass, $sStateAttrCode);
843			$sDefaultState = $oStateAttrDef->GetDefaultValue();
844		}
845		return $sDefaultState;
846	}
847
848	/**
849	 * @param string $sClass
850	 *
851	 * @return array
852	 * @throws \CoreException
853	 */
854	final static public function GetReconcKeys($sClass)
855	{
856		self::_check_subclass($sClass);
857		return self::$m_aClassParams[$sClass]["reconc_keys"];
858	}
859
860	/**
861	 * @param string $sClass
862	 *
863	 * @return string
864	 * @throws \CoreException
865	 */
866	final static public function GetDisplayTemplate($sClass)
867	{
868		self::_check_subclass($sClass);
869		return array_key_exists("display_template", self::$m_aClassParams[$sClass]) ? self::$m_aClassParams[$sClass]["display_template"] : '';
870	}
871
872	/**
873	 * @param string $sClass
874	 * @param bool $bOnlyDeclared
875	 *
876	 * @return array
877	 * @throws \CoreException
878	 */
879	final static public function GetOrderByDefault($sClass, $bOnlyDeclared = false)
880	{
881		self::_check_subclass($sClass);
882		$aOrderBy = array_key_exists("order_by_default", self::$m_aClassParams[$sClass]) ? self::$m_aClassParams[$sClass]["order_by_default"] : array();
883		if ($bOnlyDeclared)
884		{
885			// Used to reverse engineer the declaration of the data model
886			return $aOrderBy;
887		}
888		else
889		{
890			if (count($aOrderBy) == 0)
891			{
892				$aOrderBy['friendlyname'] = true;
893			}
894			return $aOrderBy;
895		}
896	}
897
898	/**
899	 * @param string $sClass
900	 * @param string $sAttCode
901	 *
902	 * @return mixed
903	 * @throws \CoreException
904	 */
905	final static public function GetAttributeOrigin($sClass, $sAttCode)
906	{
907		self::_check_subclass($sClass);
908		return self::$m_aAttribOrigins[$sClass][$sAttCode];
909	}
910
911	/**
912	 * @param string $sClass
913	 * @param string $sAttCode
914	 *
915	 * @return array
916	 * @throws \CoreException
917	 * @throws \Exception
918	 */
919	final static public function GetPrerequisiteAttributes($sClass, $sAttCode)
920	{
921		self::_check_subclass($sClass);
922		$oAtt = self::GetAttributeDef($sClass, $sAttCode);
923		// Temporary implementation: later, we might be able to compute
924		// the dependencies, based on the attributes definition
925		// (allowed values and default values)
926
927		// Even non-writable attributes (like ExternalFields) can now have Prerequisites
928		return $oAtt->GetPrerequisiteAttributes();
929	}
930
931	/**
932	 * Find all attributes that depend on the specified one (reverse of GetPrerequisiteAttributes)
933	 *
934	 * @param string $sClass Name of the class
935	 * @param string $sAttCode Code of the attributes
936	 *
937	 * @return Array List of attribute codes that depend on the given attribute, empty array if none.
938	 * @throws \CoreException
939	 * @throws \Exception
940	 */
941	final static public function GetDependentAttributes($sClass, $sAttCode)
942	{
943		$aResults = array();
944		self::_check_subclass($sClass);
945		foreach(self::ListAttributeDefs($sClass) as $sDependentAttCode => $void)
946		{
947			$aPrerequisites = self::GetPrerequisiteAttributes($sClass, $sDependentAttCode);
948			if (in_array($sAttCode, $aPrerequisites))
949			{
950				$aResults[] = $sDependentAttCode;
951			}
952		}
953		return $aResults;
954	}
955
956	/**
957	 * @param string $sClass
958	 * @param string $sAttCode
959	 *
960	 * @return string
961	 * @throws \CoreException
962	 */
963	final static public function DBGetTable($sClass, $sAttCode = null)
964	{
965		self::_check_subclass($sClass);
966		if (empty($sAttCode) || ($sAttCode == "id"))
967		{
968			$sTableRaw = self::$m_aClassParams[$sClass]["db_table"];
969			if (empty($sTableRaw))
970			{
971				// return an empty string whenever the table is undefined, meaning that there is no table associated to this 'abstract' class
972				return '';
973			}
974			else
975			{
976				// If the format changes here, do not forget to update the setup index page (detection of installed modules)
977				return self::$m_sTablePrefix.$sTableRaw;
978			}
979		}
980		// This attribute has been inherited (compound objects)
981		return self::DBGetTable(self::$m_aAttribOrigins[$sClass][$sAttCode]);
982	}
983
984	/**
985	 * @param string $sClass
986	 *
987	 * @return string
988	 */
989	final static public function DBGetView($sClass)
990	{
991		return self::$m_sTablePrefix."view_".$sClass;
992	}
993
994	/**
995	 * @return array
996	 * @throws \CoreException
997	 */
998	final static public function DBEnumTables()
999	{
1000		// This API does not rely on our capability to query the DB and retrieve
1001		// the list of existing tables
1002		// Rather, it uses the list of expected tables, corresponding to the data model
1003		$aTables = array();
1004		foreach(self::GetClasses() as $sClass)
1005		{
1006			if (!self::HasTable($sClass))
1007			{
1008				continue;
1009			}
1010			$sTable = self::DBGetTable($sClass);
1011
1012			// Could be completed later with all the classes that are using a given table
1013			if (!array_key_exists($sTable, $aTables))
1014			{
1015				$aTables[$sTable] = array();
1016			}
1017			$aTables[$sTable][] = $sClass;
1018		}
1019
1020		return $aTables;
1021	}
1022
1023	/**
1024	 * @param string $sClass
1025	 *
1026	 * @return array
1027	 * @throws \CoreException
1028	 */
1029	final static public function DBGetIndexes($sClass)
1030	{
1031		self::_check_subclass($sClass);
1032		if (isset(self::$m_aClassParams[$sClass]['indexes']))
1033		{
1034			$aRet = self::$m_aClassParams[$sClass]['indexes'];
1035		}
1036		else
1037		{
1038			$aRet = array();
1039		}
1040
1041		return $aRet;
1042	}
1043
1044
1045	/**
1046	 * @param $sClass
1047	 * @param $aColumns
1048	 * @param $aTableInfo
1049	 *
1050	 * @return array
1051	 * @throws \CoreException
1052	 */
1053	private static function DBGetIndexesLength($sClass, $aColumns, $aTableInfo)
1054	{
1055		$aLength = array();
1056		$aAttDefs = self::ListAttributeDefs($sClass);
1057		foreach($aColumns as $sAttSqlCode)
1058		{
1059			$iLength = null;
1060			foreach($aAttDefs as $sAttCode => $oAttDef)
1061			{
1062				if (($sAttCode == $sAttSqlCode) || ($oAttDef->IsParam('sql') && ($oAttDef->Get('sql') == $sAttSqlCode)))
1063				{
1064					$iLength = $oAttDef->GetIndexLength();
1065					break;
1066				}
1067			}
1068			$aLength[] = $iLength;
1069		}
1070		return $aLength;
1071	}
1072
1073	/**
1074	 * @param string $sClass
1075	 *
1076	 * @return string
1077	 * @throws \CoreException
1078	 */
1079	final static public function DBGetKey($sClass)
1080	{
1081		self::_check_subclass($sClass);
1082		return self::$m_aClassParams[$sClass]["db_key_field"];
1083	}
1084
1085	/**
1086	 * @param string $sClass
1087	 *
1088	 * @return mixed
1089	 * @throws \CoreException
1090	 */
1091	final static public function DBGetClassField($sClass)
1092	{
1093		self::_check_subclass($sClass);
1094		return self::$m_aClassParams[$sClass]["db_finalclass_field"];
1095	}
1096
1097	/**
1098	 * @param string $sClass
1099	 *
1100	 * @return boolean true if the class has no parent and no children
1101	 * @throws \CoreException
1102	 */
1103	final static public function IsStandaloneClass($sClass)
1104	{
1105		self::_check_subclass($sClass);
1106
1107		if (count(self::$m_aChildClasses[$sClass]) == 0)
1108		{
1109			if (count(self::$m_aParentClasses[$sClass]) == 0)
1110			{
1111				return true;
1112			}
1113		}
1114
1115		return false;
1116	}
1117
1118	/**
1119	 * @param string $sParentClass
1120	 * @param string $sChildClass
1121	 *
1122	 * @return bool
1123	 * @throws \CoreException
1124	 */
1125	final static public function IsParentClass($sParentClass, $sChildClass)
1126	{
1127		self::_check_subclass($sChildClass);
1128		self::_check_subclass($sParentClass);
1129		if (in_array($sParentClass, self::$m_aParentClasses[$sChildClass]))
1130		{
1131			return true;
1132		}
1133		if ($sChildClass == $sParentClass)
1134		{
1135			return true;
1136		}
1137
1138		return false;
1139	}
1140
1141	/**
1142	 * @param string $sClassA
1143	 * @param string $sClassB
1144	 *
1145	 * @return bool
1146	 * @throws \CoreException
1147	 */
1148	final static public function IsSameFamilyBranch($sClassA, $sClassB)
1149	{
1150		self::_check_subclass($sClassA);
1151		self::_check_subclass($sClassB);
1152		if (in_array($sClassA, self::$m_aParentClasses[$sClassB]))
1153		{
1154			return true;
1155		}
1156		if (in_array($sClassB, self::$m_aParentClasses[$sClassA]))
1157		{
1158			return true;
1159		}
1160		if ($sClassA == $sClassB)
1161		{
1162			return true;
1163		}
1164
1165		return false;
1166	}
1167
1168	/**
1169	 * @param string $sClassA
1170	 * @param string $sClassB
1171	 *
1172	 * @return bool
1173	 * @throws \CoreException
1174	 */
1175	final static public function IsSameFamily($sClassA, $sClassB)
1176	{
1177		self::_check_subclass($sClassA);
1178		self::_check_subclass($sClassB);
1179		return (self::GetRootClass($sClassA) == self::GetRootClass($sClassB));
1180	}
1181
1182	// Attributes of a given class may contain attributes defined in a parent class
1183	// - Some attributes are a copy of the definition
1184	// - Some attributes correspond to the upper class table definition (compound objects)
1185	// (see also filters definition)
1186	/**
1187	 * array of ("classname" => array of attributes)
1188	 *
1189	 * @var \AttributeDefinition[]
1190	 */
1191	private static $m_aAttribDefs = array();
1192	/**
1193	 * array of ("classname" => array of ("attcode"=>"sourceclass"))
1194	 *
1195	 * @var array
1196	 */
1197	private static $m_aAttribOrigins = array();
1198	/**
1199	 * array of ("classname" => array of ("attcode"))
1200	 *
1201	 * @var array
1202	 */
1203	private static $m_aIgnoredAttributes = array();
1204	/**
1205	 * array of  ("classname" => array of ("attcode" => array of ("metaattcode" => oMetaAttDef))
1206	 *
1207	 * @var array
1208	 */
1209	private static $m_aEnumToMeta = array();
1210
1211	/**
1212	 * @param string $sClass
1213	 *
1214	 * @return AttributeDefinition[]
1215	 * @throws \CoreException
1216	 */
1217	final static public function ListAttributeDefs($sClass)
1218	{
1219		self::_check_subclass($sClass);
1220		return self::$m_aAttribDefs[$sClass];
1221	}
1222
1223	/**
1224	 * @param string $sClass
1225	 *
1226	 * @return array
1227	 * @throws \CoreException
1228	 */
1229	final public static function GetAttributesList($sClass)
1230	{
1231		self::_check_subclass($sClass);
1232		return array_keys(self::$m_aAttribDefs[$sClass]);
1233	}
1234
1235	/**
1236	 * @param string $sClass
1237	 *
1238	 * @return array
1239	 * @throws \CoreException
1240	 */
1241	final public static function GetFiltersList($sClass)
1242	{
1243		self::_check_subclass($sClass);
1244		return array_keys(self::$m_aFilterDefs[$sClass]);
1245	}
1246
1247	/**
1248	 * @param string $sClass
1249	 *
1250	 * @return array
1251	 * @throws \CoreException
1252	 */
1253	final public static function GetKeysList($sClass)
1254	{
1255		self::_check_subclass($sClass);
1256		$aExtKeys = array();
1257		foreach(self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef)
1258		{
1259			if ($oAttDef->IsExternalKey())
1260			{
1261				$aExtKeys[] = $sAttCode;
1262			}
1263		}
1264
1265		return $aExtKeys;
1266	}
1267
1268	/**
1269	 * @param string $sClass
1270	 * @param string $sAttCode
1271	 *
1272	 * @return bool
1273	 */
1274	final static public function IsValidKeyAttCode($sClass, $sAttCode)
1275	{
1276		if (!array_key_exists($sClass, self::$m_aAttribDefs))
1277		{
1278			return false;
1279		}
1280		if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]))
1281		{
1282			return false;
1283		}
1284		return (self::$m_aAttribDefs[$sClass][$sAttCode]->IsExternalKey());
1285	}
1286
1287	/**
1288	 * @param string $sClass
1289	 * @param string $sAttCode
1290	 * @param bool $bExtended
1291	 *
1292	 * @return bool
1293	 * @throws \Exception
1294	 */
1295	final static public function IsValidAttCode($sClass, $sAttCode, $bExtended = false)
1296	{
1297		if (!array_key_exists($sClass, self::$m_aAttribDefs))
1298		{
1299			return false;
1300		}
1301
1302		if ($bExtended)
1303		{
1304			if (($iPos = strpos($sAttCode, '->')) === false)
1305			{
1306				$bRes = array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]);
1307			}
1308			else
1309			{
1310				$sExtKeyAttCode = substr($sAttCode, 0, $iPos);
1311				$sRemoteAttCode = substr($sAttCode, $iPos + 2);
1312				if (MetaModel::IsValidAttCode($sClass, $sExtKeyAttCode))
1313				{
1314					$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode);
1315					$sRemoteClass = $oKeyAttDef->GetTargetClass();
1316					$bRes = MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode, true);
1317				}
1318				else
1319				{
1320					$bRes = false;
1321				}
1322			}
1323		}
1324		else
1325		{
1326			$bRes = array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]);
1327		}
1328
1329		return $bRes;
1330	}
1331
1332	/**
1333	 * @param string $sClass
1334	 * @param string $sAttCode
1335	 *
1336	 * @return bool
1337	 */
1338	final static public function IsAttributeOrigin($sClass, $sAttCode)
1339	{
1340		return (self::$m_aAttribOrigins[$sClass][$sAttCode] == $sClass);
1341	}
1342
1343	/**
1344	 * @param string $sClass
1345	 * @param string $sFilterCode
1346	 *
1347	 * @return bool
1348	 */
1349	final static public function IsValidFilterCode($sClass, $sFilterCode)
1350	{
1351		if (!array_key_exists($sClass, self::$m_aFilterDefs))
1352		{
1353			return false;
1354		}
1355		return (array_key_exists($sFilterCode, self::$m_aFilterDefs[$sClass]));
1356	}
1357
1358	/**
1359	 * @param string $sClass
1360	 *
1361	 * @return bool
1362	 */
1363	public static function IsValidClass($sClass)
1364	{
1365		return (array_key_exists($sClass, self::$m_aAttribDefs));
1366	}
1367
1368	/**
1369	 * @param $oObject
1370	 *
1371	 * @return bool
1372	 */
1373	public static function IsValidObject($oObject)
1374	{
1375		if (!is_object($oObject))
1376		{
1377			return false;
1378		}
1379		return (self::IsValidClass(get_class($oObject)));
1380	}
1381
1382	/**
1383	 * @param string $sClass
1384	 * @param string $sAttCode
1385	 *
1386	 * @return bool
1387	 * @throws \CoreException
1388	 */
1389	public static function IsReconcKey($sClass, $sAttCode)
1390	{
1391		return (in_array($sAttCode, self::GetReconcKeys($sClass)));
1392	}
1393
1394	/**
1395	 * @param string $sClass Class name
1396	 * @param string $sAttCode Attribute code
1397	 *
1398	 * @return AttributeDefinition the AttributeDefinition of the $sAttCode attribute of the $sClass class
1399	 * @throws Exception
1400	 */
1401	final static public function GetAttributeDef($sClass, $sAttCode)
1402	{
1403		self::_check_subclass($sClass);
1404		if (isset(self::$m_aAttribDefs[$sClass][$sAttCode]))
1405		{
1406			return self::$m_aAttribDefs[$sClass][$sAttCode];
1407		}
1408		elseif (($iPos = strpos($sAttCode, '->')) !== false)
1409		{
1410			$sExtKeyAttCode = substr($sAttCode, 0, $iPos);
1411			$sRemoteAttCode = substr($sAttCode, $iPos + 2);
1412			$oKeyAttDef = self::GetAttributeDef($sClass, $sExtKeyAttCode);
1413			$sRemoteClass = $oKeyAttDef->GetTargetClass();
1414			return self::GetAttributeDef($sRemoteClass, $sRemoteAttCode);
1415		}
1416		else
1417		{
1418			throw new Exception("Unknown attribute $sAttCode from class $sClass");
1419		}
1420	}
1421
1422	/**
1423	 * @param string $sClass
1424	 *
1425	 * @return array
1426	 * @throws \CoreException
1427	 */
1428	final static public function GetExternalKeys($sClass)
1429	{
1430		$aExtKeys = array();
1431		foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAtt)
1432		{
1433			if ($oAtt->IsExternalKey())
1434			{
1435				$aExtKeys[$sAttCode] = $oAtt;
1436			}
1437		}
1438
1439		return $aExtKeys;
1440	}
1441
1442	/**
1443	 * @param string $sClass
1444	 *
1445	 * @return array
1446	 * @throws \CoreException
1447	 */
1448	final static public function GetLinkedSets($sClass)
1449	{
1450		$aLinkedSets = array();
1451		foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAtt)
1452		{
1453			if (is_subclass_of($oAtt, 'AttributeLinkedSet'))
1454			{
1455				$aLinkedSets[$sAttCode] = $oAtt;
1456			}
1457		}
1458
1459		return $aLinkedSets;
1460	}
1461
1462	/**
1463	 * @param string $sClass
1464	 * @param string $sKeyAttCode
1465	 *
1466	 * @return mixed
1467	 * @throws \CoreException
1468	 */
1469	final static public function GetExternalFields($sClass, $sKeyAttCode)
1470	{
1471		static $aExtFields = array();
1472		if (!isset($aExtFields[$sClass][$sKeyAttCode]))
1473		{
1474			$aExtFields[$sClass][$sKeyAttCode] = array();
1475			foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAtt)
1476			{
1477				if ($oAtt->IsExternalField() && ($oAtt->GetKeyAttCode() == $sKeyAttCode))
1478				{
1479					$aExtFields[$sClass][$sKeyAttCode][$oAtt->GetExtAttCode()] = $oAtt;
1480				}
1481			}
1482		}
1483		return $aExtFields[$sClass][$sKeyAttCode];
1484	}
1485
1486	/**
1487	 * @param string $sClass
1488	 * @param string $sKeyAttCode
1489	 * @param string $sRemoteAttCode
1490	 *
1491	 * @return null|string
1492	 * @throws \CoreException
1493	 */
1494	final static public function FindExternalField($sClass, $sKeyAttCode, $sRemoteAttCode)
1495	{
1496		$aExtFields = self::GetExternalFields($sClass, $sKeyAttCode);
1497		if (isset($aExtFields[$sRemoteAttCode]))
1498		{
1499			return $aExtFields[$sRemoteAttCode];
1500		}
1501		else
1502		{
1503			return null;
1504		}
1505	}
1506
1507	/** @var array */
1508	protected static $m_aTrackForwardCache = array();
1509
1510	/**
1511	 * List external keys for which there is a LinkSet (direct or indirect) on the other end
1512	 *
1513	 * For those external keys, a change will have a special meaning on the other end
1514	 * in term of change tracking
1515	 *
1516	 * @param string $sClass
1517	 *
1518	 * @return mixed
1519	 * @throws \CoreException
1520	 */
1521	final static public function GetTrackForwardExternalKeys($sClass)
1522	{
1523		if (!isset(self::$m_aTrackForwardCache[$sClass]))
1524		{
1525			$aRes = array();
1526			foreach(MetaModel::GetExternalKeys($sClass) as $sAttCode => $oAttDef)
1527			{
1528				$sRemoteClass = $oAttDef->GetTargetClass();
1529				foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef)
1530				{
1531					if (!$oRemoteAttDef->IsLinkSet())
1532					{
1533						continue;
1534					}
1535					if (!is_subclass_of($sClass, $oRemoteAttDef->GetLinkedClass()) && $oRemoteAttDef->GetLinkedClass() != $sClass)
1536					{
1537						continue;
1538					}
1539					if ($oRemoteAttDef->GetExtKeyToMe() != $sAttCode)
1540					{
1541						continue;
1542					}
1543					$aRes[$sAttCode] = $oRemoteAttDef;
1544				}
1545			}
1546			self::$m_aTrackForwardCache[$sClass] = $aRes;
1547		}
1548		return self::$m_aTrackForwardCache[$sClass];
1549	}
1550
1551	/**
1552	 * @param string $sClass
1553	 * @param string $sAttCode
1554	 *
1555	 * @return array
1556	 */
1557	final static public function ListMetaAttributes($sClass, $sAttCode)
1558	{
1559		if (isset(self::$m_aEnumToMeta[$sClass][$sAttCode]))
1560		{
1561			$aRet = self::$m_aEnumToMeta[$sClass][$sAttCode];
1562		}
1563		else
1564		{
1565			$aRet = array();
1566		}
1567		return $aRet;
1568	}
1569
1570	/**
1571	 * Get the attribute label
1572	 *
1573	 * @param string sClass Persistent class
1574	 * @param string sAttCodeEx Extended attribute code: attcode[->attcode]
1575	 * @param bool $bShowMandatory If true, add a star character (at the end or before the ->) to show that the field
1576	 *     is mandatory
1577	 *
1578	 * @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName
1579	 * @throws \Exception
1580	 */
1581	public static function GetLabel($sClass, $sAttCodeEx, $bShowMandatory = false)
1582	{
1583		if (($iPos = strpos($sAttCodeEx, '->')) === false)
1584		{
1585			if ($sAttCodeEx == 'id')
1586			{
1587				$sLabel = Dict::S('UI:CSVImport:idField');
1588			}
1589			else
1590			{
1591				$oAttDef = self::GetAttributeDef($sClass, $sAttCodeEx);
1592				$sMandatory = ($bShowMandatory && !$oAttDef->IsNullAllowed()) ? '*' : '';
1593				$sLabel = $oAttDef->GetLabel().$sMandatory;
1594			}
1595		}
1596		else
1597		{
1598			$sExtKeyAttCode = substr($sAttCodeEx, 0, $iPos);
1599			$sRemoteAttCode = substr($sAttCodeEx, $iPos + 2);
1600			$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode);
1601			$sRemoteClass = $oKeyAttDef->GetTargetClass();
1602			// Recurse
1603			$sLabel = self::GetLabel($sClass, $sExtKeyAttCode).'->'.self::GetLabel($sRemoteClass, $sRemoteAttCode);
1604		}
1605
1606		return $sLabel;
1607	}
1608
1609	/**
1610	 * @param string $sClass
1611	 * @param string $sAttCode
1612	 *
1613	 * @return string
1614	 * @throws \Exception
1615	 */
1616	public static function GetDescription($sClass, $sAttCode)
1617	{
1618		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
1619		if ($oAttDef)
1620		{
1621			return $oAttDef->GetDescription();
1622		}
1623		return "";
1624	}
1625
1626	// Filters of a given class may contain filters defined in a parent class
1627	// - Some filters are a copy of the definition
1628	// - Some filters correspond to the upper class table definition (compound objects)
1629	// (see also attributes definition)
1630	/**
1631	 * array of ("classname" => array filterdef)
1632	 *
1633	 * @var array
1634	 */
1635	private static $m_aFilterDefs = array();
1636	/**
1637	 * array of ("classname" => array of ("attcode"=>"sourceclass"))
1638	 *
1639	 * @var array
1640	 */
1641	private static $m_aFilterOrigins = array();
1642
1643	/**
1644	 * @param string $sClass
1645	 *
1646	 * @return mixed
1647	 * @throws \CoreException
1648	 */
1649	public static function GetClassFilterDefs($sClass)
1650	{
1651		self::_check_subclass($sClass);
1652		return self::$m_aFilterDefs[$sClass];
1653	}
1654
1655	/**
1656	 * @param string $sClass
1657	 * @param string $sFilterCode
1658	 *
1659	 * @return mixed
1660	 * @throws \CoreException
1661	 */
1662	final static public function GetClassFilterDef($sClass, $sFilterCode)
1663	{
1664		self::_check_subclass($sClass);
1665		if (!array_key_exists($sFilterCode, self::$m_aFilterDefs[$sClass]))
1666		{
1667			throw new CoreException("Unknown filter code '$sFilterCode' for class '$sClass'");
1668		}
1669		return self::$m_aFilterDefs[$sClass][$sFilterCode];
1670	}
1671
1672	/**
1673	 * @param string $sClass
1674	 * @param string $sFilterCode
1675	 *
1676	 * @return string
1677	 * @throws \CoreException
1678	 */
1679	public static function GetFilterLabel($sClass, $sFilterCode)
1680	{
1681		$oFilter = self::GetClassFilterDef($sClass, $sFilterCode);
1682		if ($oFilter)
1683		{
1684			return $oFilter->GetLabel();
1685		}
1686
1687		return "";
1688	}
1689
1690	/**
1691	 * @param string $sClass
1692	 * @param string $sFilterCode
1693	 *
1694	 * @return string
1695	 * @throws \CoreException
1696	 */
1697	public static function GetFilterDescription($sClass, $sFilterCode)
1698	{
1699		$oFilter = self::GetClassFilterDef($sClass, $sFilterCode);
1700		if ($oFilter)
1701		{
1702			return $oFilter->GetDescription();
1703		}
1704		return "";
1705	}
1706
1707	/**
1708	 * @param string $sClass
1709	 * @param string $sFilterCode
1710	 *
1711	 * @return array
1712	 * @throws \CoreException
1713	 */
1714	public static function GetFilterOperators($sClass, $sFilterCode)
1715	{
1716		$oFilter = self::GetClassFilterDef($sClass, $sFilterCode);
1717		if ($oFilter)
1718		{
1719			return $oFilter->GetOperators();
1720		}
1721		return array();
1722	}
1723
1724	/**
1725	 * @param string $sClass
1726	 * @param string $sFilterCode
1727	 *
1728	 * @return array
1729	 * @throws \CoreException
1730	 */
1731	public static function GetFilterLooseOperator($sClass, $sFilterCode)
1732	{
1733		$oFilter = self::GetClassFilterDef($sClass, $sFilterCode);
1734		if ($oFilter)
1735		{
1736			return $oFilter->GetLooseOperator();
1737		}
1738
1739		return array();
1740	}
1741
1742	/**
1743	 * @param string $sClass
1744	 * @param string $sFilterCode
1745	 * @param string $sOpCode
1746	 *
1747	 * @return string
1748	 * @throws \CoreException
1749	 */
1750	public static function GetFilterOpDescription($sClass, $sFilterCode, $sOpCode)
1751	{
1752		$oFilter = self::GetClassFilterDef($sClass, $sFilterCode);
1753		if ($oFilter)
1754		{
1755			return $oFilter->GetOpDescription($sOpCode);
1756		}
1757
1758		return "";
1759	}
1760
1761	/**
1762	 * @param string $sFilterCode
1763	 *
1764	 * @return string
1765	 */
1766	public static function GetFilterHTMLInput($sFilterCode)
1767	{
1768		return "<INPUT name=\"$sFilterCode\">";
1769	}
1770
1771	// Lists of attributes/search filters
1772	//
1773	/**
1774	 * array of ("listcode" => various info on the list, common to every classes)
1775	 *
1776	 * @var array
1777	 */
1778	private static $m_aListInfos = array();
1779	/**
1780	 * array of ("classname" => array of "listcode" => list)
1781	 * list may be an array of attcode / fltcode
1782	 * list may be an array of "groupname" => (array of attcode / fltcode)
1783	 *
1784	 * @var array
1785	 */
1786	private static $m_aListData = array();
1787
1788	/**
1789	 * @return array
1790	 */
1791	public static function EnumZLists()
1792	{
1793		return array_keys(self::$m_aListInfos);
1794	}
1795
1796	/**
1797	 * @param string $sListCode
1798	 *
1799	 * @return mixed
1800	 */
1801	final static public function GetZListInfo($sListCode)
1802	{
1803		return self::$m_aListInfos[$sListCode];
1804	}
1805
1806	/**
1807	 * @param string $sClass
1808	 * @param string $sListCode
1809	 *
1810	 * @return array
1811	 */
1812	public static function GetZListItems($sClass, $sListCode)
1813	{
1814		if (array_key_exists($sClass, self::$m_aListData))
1815		{
1816			if (array_key_exists($sListCode, self::$m_aListData[$sClass]))
1817			{
1818				return self::$m_aListData[$sClass][$sListCode];
1819			}
1820		}
1821		$sParentClass = self::GetParentPersistentClass($sClass);
1822		if (empty($sParentClass))
1823		{
1824			return array();
1825		} // nothing for the mother of all classes
1826		// Dig recursively
1827		return self::GetZListItems($sParentClass, $sListCode);
1828	}
1829
1830	/**
1831	 * @param string $sClass
1832	 * @param string $sListCode
1833	 * @param string $sAttCodeOrFltCode
1834	 * @param string $sGroup
1835	 *
1836	 * @return bool
1837	 */
1838	public static function IsAttributeInZList($sClass, $sListCode, $sAttCodeOrFltCode, $sGroup = null)
1839	{
1840		$aZList = self::FlattenZlist(self::GetZListItems($sClass, $sListCode));
1841		if (!$sGroup)
1842		{
1843			return (in_array($sAttCodeOrFltCode, $aZList));
1844		}
1845		return (in_array($sAttCodeOrFltCode, $aZList[$sGroup]));
1846	}
1847
1848	//
1849	// Relations
1850	//
1851	/**
1852	 * array of ("relcode" => various info on the list, common to every classes)
1853	 *
1854	 * @var array
1855	 */
1856	private static $m_aRelationInfos = array();
1857
1858	/**
1859	 * TO BE DEPRECATED: use EnumRelationsEx instead
1860	 *
1861	 * @param string $sClass
1862	 *
1863	 * @return array multitype:string unknown |Ambigous <string, multitype:>
1864	 * @throws \CoreException
1865	 * @throws \Exception
1866	 * @throws \OQLException
1867	 */
1868	public static function EnumRelations($sClass = '')
1869	{
1870		$aResult = array_keys(self::$m_aRelationInfos);
1871		if (!empty($sClass))
1872		{
1873			// Return only the relations that have a meaning (i.e. for which at least one query is defined)
1874			// for the specified class
1875			$aClassRelations = array();
1876			foreach($aResult as $sRelCode)
1877			{
1878				$aQueriesDown = self::EnumRelationQueries($sClass, $sRelCode);
1879				if (count($aQueriesDown) > 0)
1880				{
1881					$aClassRelations[] = $sRelCode;
1882				}
1883				// Temporary patch: until the impact analysis GUI gets rewritten,
1884				// let's consider that "depends on" is equivalent to "impacts/up"
1885				// The current patch has been implemented in DBObject and MetaModel
1886				if ($sRelCode == 'impacts')
1887				{
1888					$aQueriesUp = self::EnumRelationQueries($sClass, 'impacts', false);
1889					if (count($aQueriesUp) > 0)
1890					{
1891						$aClassRelations[] = 'depends on';
1892					}
1893				}
1894			}
1895
1896			return $aClassRelations;
1897		}
1898
1899		// Temporary patch: until the impact analysis GUI gets rewritten,
1900		// let's consider that "depends on" is equivalent to "impacts/up"
1901		// The current patch has been implemented in DBObject and MetaModel
1902		if (in_array('impacts', $aResult))
1903		{
1904			$aResult[] = 'depends on';
1905		}
1906
1907		return $aResult;
1908	}
1909
1910	/**
1911	 * @param string $sClass
1912	 *
1913	 * @return array
1914	 * @throws \CoreException
1915	 * @throws \Exception
1916	 * @throws \OQLException
1917	 */
1918	public static function EnumRelationsEx($sClass)
1919	{
1920		$aRelationInfo = array_keys(self::$m_aRelationInfos);
1921		// Return only the relations that have a meaning (i.e. for which at least one query is defined)
1922		// for the specified class
1923		$aClassRelations = array();
1924		foreach($aRelationInfo as $sRelCode)
1925		{
1926			$aQueriesDown = self::EnumRelationQueries($sClass, $sRelCode, true /* Down */);
1927			if (count($aQueriesDown) > 0)
1928			{
1929				$aClassRelations[$sRelCode]['down'] = self::GetRelationLabel($sRelCode, true);
1930			}
1931
1932			$aQueriesUp = self::EnumRelationQueries($sClass, $sRelCode, false /* Up */);
1933			if (count($aQueriesUp) > 0)
1934			{
1935				$aClassRelations[$sRelCode]['up'] = self::GetRelationLabel($sRelCode, false);
1936			}
1937		}
1938
1939		return $aClassRelations;
1940	}
1941
1942	/**
1943	 * @param string $sRelCode Relation code
1944	 * @param bool $bDown Relation direction, is it downstream (true) or upstream (false). Default is true.
1945	 *
1946	 * @return string
1947	 * @throws \DictExceptionMissingString
1948	 */
1949	final static public function GetRelationDescription($sRelCode, $bDown = true)
1950	{
1951		// Legacy convention had only one description describing the relation.
1952		// Now, as the relation is bidirectional, we have a description for each directions.
1953		$sLegacy = Dict::S("Relation:$sRelCode/Description");
1954
1955		if($bDown)
1956		{
1957			$sKey = "Relation:$sRelCode/DownStream+";
1958		}
1959		else
1960		{
1961			$sKey = "Relation:$sRelCode/UpStream+";
1962		}
1963		$sRet = Dict::S($sKey, $sLegacy);
1964
1965		return $sRet;
1966	}
1967
1968	/**
1969	 * @param string $sRelCode Relation code
1970	 * @param bool $bDown Relation direction, is it downstream (true) or upstream (false). Default is true.
1971	 *
1972	 * @return string
1973	 * @throws \DictExceptionMissingString
1974	 */
1975	final static public function GetRelationLabel($sRelCode, $bDown = true)
1976	{
1977		if ($bDown)
1978		{
1979			// The legacy convention is confusing with regard to the way we have conceptualized the relations:
1980			// In the former representation, the main stream was named after "up"
1981			// Now, the relation from A to B says that something is transmitted from A to B, thus going DOWNstream as described in a petri net.
1982			$sKey = "Relation:$sRelCode/DownStream";
1983			$sLegacy = Dict::S("Relation:$sRelCode/VerbUp", $sKey);
1984		}
1985		else
1986		{
1987			$sKey = "Relation:$sRelCode/UpStream";
1988			$sLegacy = Dict::S("Relation:$sRelCode/VerbDown", $sKey);
1989		}
1990
1991		return Dict::S($sKey, $sLegacy);
1992	}
1993
1994	/**
1995	 * @param string $sRelCode
1996	 *
1997	 * @return array
1998	 * @throws \CoreException
1999	 * @throws \Exception
2000	 * @throws \OQLException
2001	 */
2002	protected static function ComputeRelationQueries($sRelCode)
2003	{
2004		$bHasLegacy = false;
2005		$aQueries = array();
2006		foreach(self::GetClasses() as $sClass)
2007		{
2008			$aQueries[$sClass]['down'] = array();
2009			if (!array_key_exists('up', $aQueries[$sClass]))
2010			{
2011				$aQueries[$sClass]['up'] = array();
2012			}
2013
2014			$aNeighboursDown = call_user_func_array(array($sClass, 'GetRelationQueriesEx'), array($sRelCode));
2015
2016			// Translate attributes into queries (new style of spec only)
2017			foreach($aNeighboursDown as $sNeighbourId => $aNeighbourData)
2018			{
2019				$aNeighbourData['sFromClass'] = $aNeighbourData['sDefinedInClass'];
2020				try
2021				{
2022					if (strlen($aNeighbourData['sQueryDown']) == 0)
2023					{
2024						$oAttDef = self::GetAttributeDef($sClass, $aNeighbourData['sAttribute']);
2025						if ($oAttDef instanceof AttributeExternalKey)
2026						{
2027							$sTargetClass = $oAttDef->GetTargetClass();
2028							$aNeighbourData['sToClass'] = $sTargetClass;
2029							$aNeighbourData['sQueryDown'] = 'SELECT '.$sTargetClass.' AS o WHERE o.id = :this->'.$aNeighbourData['sAttribute'];
2030							$aNeighbourData['sQueryUp'] = 'SELECT '.$aNeighbourData['sFromClass'].' AS o WHERE o.'.$aNeighbourData['sAttribute'].' = :this->id';
2031						}
2032						elseif ($oAttDef instanceof AttributeLinkedSet)
2033						{
2034							$sLinkedClass = $oAttDef->GetLinkedClass();
2035							$sExtKeyToMe = $oAttDef->GetExtKeyToMe();
2036							if ($oAttDef->IsIndirect())
2037							{
2038								$sExtKeyToRemote = $oAttDef->GetExtKeyToRemote();
2039								$oRemoteAttDef = self::GetAttributeDef($sLinkedClass, $sExtKeyToRemote);
2040								$sRemoteClass = $oRemoteAttDef->GetTargetClass();
2041
2042								$aNeighbourData['sToClass'] = $sRemoteClass;
2043								$aNeighbourData['sQueryDown'] = "SELECT $sRemoteClass AS o JOIN $sLinkedClass AS lnk ON lnk.$sExtKeyToRemote = o.id WHERE lnk.$sExtKeyToMe = :this->id";
2044								$aNeighbourData['sQueryUp'] = "SELECT ".$aNeighbourData['sFromClass']." AS o JOIN $sLinkedClass AS lnk ON lnk.$sExtKeyToMe = o.id WHERE lnk.$sExtKeyToRemote = :this->id";
2045							}
2046							else
2047							{
2048								$aNeighbourData['sToClass'] = $sLinkedClass;
2049								$aNeighbourData['sQueryDown'] = "SELECT $sLinkedClass AS o WHERE o.$sExtKeyToMe = :this->id";
2050								$aNeighbourData['sQueryUp'] = "SELECT ".$aNeighbourData['sFromClass']." AS o WHERE o.id = :this->$sExtKeyToMe";
2051							}
2052						}
2053						else
2054						{
2055							throw new Exception("Unexpected attribute type for '{$aNeighbourData['sAttribute']}'. Expecting a link set or external key.");
2056						}
2057					}
2058					else
2059					{
2060						$oSearch = DBObjectSearch::FromOQL($aNeighbourData['sQueryDown']);
2061						$aNeighbourData['sToClass'] = $oSearch->GetClass();
2062					}
2063				}
2064				catch (Exception $e)
2065				{
2066					throw new Exception("Wrong definition for the relation $sRelCode/{$aNeighbourData['sDefinedInClass']}/{$aNeighbourData['sNeighbour']}: ".$e->getMessage());
2067				}
2068
2069				if ($aNeighbourData['sDirection'] == 'down')
2070				{
2071					$aNeighbourData['sQueryUp'] = null;
2072				}
2073
2074				$sArrowId = $aNeighbourData['sDefinedInClass'].'_'.$sNeighbourId;
2075				$aQueries[$sClass]['down'][$sArrowId] = $aNeighbourData;
2076
2077				// Compute the reverse index
2078				if ($aNeighbourData['sDefinedInClass'] == $sClass)
2079				{
2080					if ($aNeighbourData['sDirection'] == 'both')
2081					{
2082						$sToClass = $aNeighbourData['sToClass'];
2083						foreach(self::EnumChildClasses($sToClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass)
2084						{
2085							$aQueries[$sSubClass]['up'][$sArrowId] = $aNeighbourData;
2086						}
2087					}
2088				}
2089			}
2090
2091			// Read legacy definitions
2092			// The up/down queries have to be reconcilied, which can only be done later when all the classes have been browsed
2093			//
2094			// The keys used to store a query (up or down) into the array are built differently between the modern and legacy made data:
2095			// Modern way: aQueries[sClass]['up'|'down'][sArrowId], where sArrowId is made of the source class + neighbour id (XML def)
2096			// Legacy way: aQueries[sClass]['up'|'down'][sRemoteClass]
2097			// The modern way does allow for several arrows between two classes
2098			// The legacy way aims at simplifying the transformation (reconciliation between up and down)
2099			if ($sRelCode == 'impacts')
2100			{
2101				$sRevertCode = 'depends on';
2102
2103				$aLegacy = call_user_func_array(array($sClass, 'GetRelationQueries'), array($sRelCode));
2104				foreach($aLegacy as $sId => $aLegacyEntry)
2105				{
2106					$bHasLegacy = true;
2107
2108					$oFilter = DBObjectSearch::FromOQL($aLegacyEntry['sQuery']);
2109					$sRemoteClass = $oFilter->GetClass();
2110
2111					// Determine wether the query is inherited from a parent or not
2112					$bInherited = false;
2113					foreach(self::EnumParentClasses($sClass) as $sParent)
2114					{
2115						if (!isset($aQueries[$sParent]['down'][$sRemoteClass]))
2116						{
2117							continue;
2118						}
2119						if ($aLegacyEntry['sQuery'] == $aQueries[$sParent]['down'][$sRemoteClass]['sQueryDown'])
2120						{
2121							$bInherited = true;
2122							$aQueries[$sClass]['down'][$sRemoteClass] = $aQueries[$sParent]['down'][$sRemoteClass];
2123							break;
2124						}
2125					}
2126
2127					if (!$bInherited)
2128					{
2129						$aQueries[$sClass]['down'][$sRemoteClass] = array(
2130							'_legacy_' => true,
2131							'sDefinedInClass' => $sClass,
2132							'sFromClass' => $sClass,
2133							'sToClass' => $sRemoteClass,
2134							'sDirection' => 'down',
2135							'sQueryDown' => $aLegacyEntry['sQuery'],
2136							'sQueryUp' => null,
2137							'sNeighbour' => $sRemoteClass // Normalize the neighbour id
2138						);
2139					}
2140				}
2141
2142				$aLegacy = call_user_func_array(array($sClass, 'GetRelationQueries'), array($sRevertCode));
2143				foreach($aLegacy as $sId => $aLegacyEntry)
2144				{
2145					$bHasLegacy = true;
2146
2147					$oFilter = DBObjectSearch::FromOQL($aLegacyEntry['sQuery']);
2148					$sRemoteClass = $oFilter->GetClass();
2149
2150					// Determine wether the query is inherited from a parent or not
2151					$bInherited = false;
2152					foreach(self::EnumParentClasses($sClass) as $sParent)
2153					{
2154						if (!isset($aQueries[$sParent]['up'][$sRemoteClass]))
2155						{
2156							continue;
2157						}
2158						if ($aLegacyEntry['sQuery'] == $aQueries[$sParent]['up'][$sRemoteClass]['sQueryUp'])
2159						{
2160							$bInherited = true;
2161							$aQueries[$sClass]['up'][$sRemoteClass] = $aQueries[$sParent]['up'][$sRemoteClass];
2162							break;
2163						}
2164					}
2165
2166					if (!$bInherited)
2167					{
2168						$aQueries[$sClass]['up'][$sRemoteClass] = array(
2169							'_legacy_' => true,
2170							'sDefinedInClass' => $sRemoteClass,
2171							'sFromClass' => $sRemoteClass,
2172							'sToClass' => $sClass,
2173							'sDirection' => 'both',
2174							'sQueryDown' => null,
2175							'sQueryUp' => $aLegacyEntry['sQuery'],
2176							'sNeighbour' => $sClass// Normalize the neighbour id
2177						);
2178					}
2179				}
2180			}
2181			else
2182			{
2183				// Cannot take the legacy system into account... simply ignore it
2184			}
2185		} // foreach class
2186
2187		// Perform the up/down reconciliation for the legacy definitions
2188		if ($bHasLegacy)
2189		{
2190			foreach(self::GetClasses() as $sClass)
2191			{
2192				// Foreach "up" legacy query, update its "down" counterpart
2193				if (isset($aQueries[$sClass]['up']))
2194				{
2195					foreach($aQueries[$sClass]['up'] as $sNeighbourId => $aNeighbourData)
2196					{
2197						if (!array_key_exists('_legacy_', $aNeighbourData))
2198						{
2199							continue;
2200						}
2201						if (!$aNeighbourData['_legacy_'])
2202						{
2203							continue;
2204						} // Skip modern definitions
2205
2206						$sLocalClass = $aNeighbourData['sToClass'];
2207						foreach(self::EnumChildClasses($aNeighbourData['sFromClass'], ENUM_CHILD_CLASSES_ALL) as $sRemoteClass)
2208						{
2209							if (isset($aQueries[$sRemoteClass]['down'][$sLocalClass]))
2210							{
2211								$aQueries[$sRemoteClass]['down'][$sLocalClass]['sQueryUp'] = $aNeighbourData['sQueryUp'];
2212								$aQueries[$sRemoteClass]['down'][$sLocalClass]['sDirection'] = 'both';
2213							}
2214							// Be silent in order to transparently support legacy data models where the counterpart query does not always exist
2215							//else
2216							//{
2217							//	throw new Exception("Legacy definition of the relation '$sRelCode/$sRevertCode', defined on $sLocalClass (relation: $sRevertCode, inherited to $sClass), missing the counterpart query on class $sRemoteClass ($sRelCode)");
2218							//}
2219						}
2220					}
2221				}
2222				// Foreach "down" legacy query, update its "up" counterpart (if any)
2223				foreach($aQueries[$sClass]['down'] as $sNeighbourId => $aNeighbourData)
2224				{
2225					if (!$aNeighbourData['_legacy_'])
2226					{
2227						continue;
2228					} // Skip modern definitions
2229
2230					$sLocalClass = $aNeighbourData['sFromClass'];
2231					foreach(self::EnumChildClasses($aNeighbourData['sToClass'], ENUM_CHILD_CLASSES_ALL) as $sRemoteClass)
2232					{
2233						if (isset($aQueries[$sRemoteClass]['up'][$sLocalClass]))
2234						{
2235							$aQueries[$sRemoteClass]['up'][$sLocalClass]['sQueryDown'] = $aNeighbourData['sQueryDown'];
2236						}
2237					}
2238				}
2239			}
2240		}
2241
2242		return $aQueries;
2243	}
2244
2245	/**
2246	 * @param string $sClass
2247	 * @param string $sRelCode
2248	 * @param bool $bDown
2249	 *
2250	 * @return array
2251	 * @throws \CoreException
2252	 * @throws \Exception
2253	 * @throws \OQLException
2254	 */
2255	public static function EnumRelationQueries($sClass, $sRelCode, $bDown = true)
2256	{
2257		static $aQueries = array();
2258		if (!isset($aQueries[$sRelCode]))
2259		{
2260			$aQueries[$sRelCode] = self::ComputeRelationQueries($sRelCode);
2261		}
2262		$sDirection = $bDown ? 'down' : 'up';
2263		if (isset($aQueries[$sRelCode][$sClass][$sDirection]))
2264		{
2265			return $aQueries[$sRelCode][$sClass][$sDirection];
2266		}
2267		else
2268		{
2269			return array();
2270		}
2271	}
2272
2273	/**
2274	 * Compute the "RelatedObjects" for a whole set of DBObjects
2275	 *
2276	 * @param string $sRelCode The code of the relation to use for the computation
2277	 * @param array $aSourceObjects The objects to start with
2278	 * @param int $iMaxDepth
2279	 * @param boolean $bEnableRedundancy
2280	 * @param array $aUnreachable Array of objects to be considered as 'unreachable'
2281	 * @param array $aContexts
2282	 *
2283	 * @return RelationGraph The graph of all the related objects
2284	 * @throws \Exception
2285	 */
2286	static public function GetRelatedObjectsDown($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true, $aUnreachable = array(), $aContexts = array())
2287	{
2288		$oGraph = new RelationGraph();
2289		foreach($aSourceObjects as $oObject)
2290		{
2291			$oGraph->AddSourceObject($oObject);
2292		}
2293		foreach($aContexts as $key => $sOQL)
2294		{
2295			$oGraph->AddContextQuery($key, $sOQL);
2296		}
2297		$oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy, $aUnreachable);
2298		return $oGraph;
2299	}
2300
2301	/**
2302	 * Compute the "RelatedObjects" in the reverse way
2303	 *
2304	 * @param string $sRelCode The code of the relation to use for the computation
2305	 * @param array $aSourceObjects The objects to start with
2306	 * @param int $iMaxDepth
2307	 * @param boolean $bEnableRedundancy
2308	 * @param array $aContexts
2309	 *
2310	 * @return RelationGraph The graph of all the related objects
2311	 * @throws \Exception
2312	 */
2313	static public function GetRelatedObjectsUp($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true, $aContexts = array())
2314	{
2315		$oGraph = new RelationGraph();
2316		foreach($aSourceObjects as $oObject)
2317		{
2318			$oGraph->AddSinkObject($oObject);
2319		}
2320		foreach($aContexts as $key => $sOQL)
2321		{
2322			$oGraph->AddContextQuery($key, $sOQL);
2323		}
2324		$oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy);
2325		return $oGraph;
2326	}
2327
2328	//
2329	// Object lifecycle model
2330	//
2331	/**
2332	 * array of ("classname" => array of "statecode"=>array('label'=>..., attribute_inherit=> attribute_list=>...))
2333	 *
2334	 * @var array
2335	 */
2336	private static $m_aStates = array();
2337	/**
2338	 * array of ("classname" => array of ("stimuluscode"=>array('label'=>...)))
2339	 *
2340	 * @var array
2341	 */
2342	private static $m_aStimuli = array();
2343	/**
2344	 * array of ("classname" => array of ("statcode_from"=>array of ("stimuluscode" => array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD)))
2345	 *
2346	 * @var array
2347	 */
2348	private static $m_aTransitions = array();
2349
2350	/**
2351	 * @param string $sClass
2352	 *
2353	 * @return array
2354	 */
2355	public static function EnumStates($sClass)
2356	{
2357		if (array_key_exists($sClass, self::$m_aStates))
2358		{
2359			return self::$m_aStates[$sClass];
2360		}
2361		else
2362		{
2363			return array();
2364		}
2365	}
2366
2367	/**
2368	 * @param string $sClass
2369	 *
2370	 * @return array All possible initial states, including the default one
2371	 * @throws \CoreException
2372	 * @throws \Exception
2373	 */
2374	public static function EnumInitialStates($sClass)
2375	{
2376		if (array_key_exists($sClass, self::$m_aStates))
2377		{
2378			$aRet = array();
2379			// Add the states for which the flag 'is_initial_state' is set to <true>
2380			foreach(self::$m_aStates[$sClass] as $aStateCode => $aProps)
2381			{
2382				if (isset($aProps['initial_state_path']))
2383				{
2384					$aRet[$aStateCode] = $aProps['initial_state_path'];
2385				}
2386			}
2387			// Add the default initial state
2388			$sMainInitialState = self::GetDefaultState($sClass);
2389			if (!isset($aRet[$sMainInitialState]))
2390			{
2391				$aRet[$sMainInitialState] = array();
2392			}
2393			return $aRet;
2394		}
2395		else
2396		{
2397			return array();
2398		}
2399	}
2400
2401	/**
2402	 * @param string $sClass
2403	 *
2404	 * @return array
2405	 */
2406	public static function EnumStimuli($sClass)
2407	{
2408		if (array_key_exists($sClass, self::$m_aStimuli))
2409		{
2410			return self::$m_aStimuli[$sClass];
2411		}
2412		else
2413		{
2414			return array();
2415		}
2416	}
2417
2418	/**
2419	 * @param string $sClass
2420	 * @param string $sStateValue
2421	 *
2422	 * @return mixed
2423	 * @throws \CoreException
2424	 * @throws \Exception
2425	 */
2426	public static function GetStateLabel($sClass, $sStateValue)
2427	{
2428		$sStateAttrCode = self::GetStateAttributeCode($sClass);
2429		$oAttDef = self::GetAttributeDef($sClass, $sStateAttrCode);
2430		return $oAttDef->GetValueLabel($sStateValue);
2431	}
2432
2433	/**
2434	 * @param string $sClass
2435	 * @param string $sStateValue
2436	 *
2437	 * @return mixed
2438	 * @throws \CoreException
2439	 * @throws \Exception
2440	 */
2441	public static function GetStateDescription($sClass, $sStateValue)
2442	{
2443		$sStateAttrCode = self::GetStateAttributeCode($sClass);
2444		$oAttDef = self::GetAttributeDef($sClass, $sStateAttrCode);
2445		return $oAttDef->GetValueDescription($sStateValue);
2446	}
2447
2448	/**
2449	 * @param string $sClass
2450	 * @param string $sStateCode
2451	 *
2452	 * @return array
2453	 */
2454	public static function EnumTransitions($sClass, $sStateCode)
2455	{
2456		if (array_key_exists($sClass, self::$m_aTransitions))
2457		{
2458			if (array_key_exists($sStateCode, self::$m_aTransitions[$sClass]))
2459			{
2460				return self::$m_aTransitions[$sClass][$sStateCode];
2461			}
2462		}
2463		return array();
2464	}
2465
2466	/**
2467	 * @param string $sClass
2468	 * @param string $sState
2469	 * @param string $sAttCode
2470	 *
2471	 * @return int the binary combination of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) for the
2472	 *     given attribute in the given state of the object
2473	 * @throws \CoreException
2474	 *
2475	 * @see \DBObject::GetAttributeFlags()
2476	 */
2477	public static function GetAttributeFlags($sClass, $sState, $sAttCode)
2478	{
2479		$iFlags = 0; // By default (if no life cycle) no flag at all
2480		$sStateAttCode = self::GetStateAttributeCode($sClass);
2481		if (!empty($sStateAttCode))
2482		{
2483			$aStates = MetaModel::EnumStates($sClass);
2484			if (!array_key_exists($sState, $aStates))
2485			{
2486				throw new CoreException("Invalid state '$sState' for class '$sClass', expecting a value in {".implode(', ', array_keys($aStates))."}");
2487			}
2488			$aCurrentState = $aStates[$sState];
2489			if ((array_key_exists('attribute_list', $aCurrentState)) && (array_key_exists($sAttCode, $aCurrentState['attribute_list'])))
2490			{
2491				$iFlags = $aCurrentState['attribute_list'][$sAttCode];
2492			}
2493		}
2494		return $iFlags;
2495	}
2496
2497	/**
2498	 * @param string $sClass string
2499	 * @param string $sState string
2500	 * @param string $sStimulus string
2501	 * @param string $sAttCode string
2502	 *
2503	 * @return int The $sAttCode flags when $sStimulus is applied on an object of $sClass in the $sState state.
2504	 * <strong>Note: This does NOT combine flags from the target state</strong>
2505	 * @throws CoreException
2506	 */
2507	public static function GetTransitionFlags($sClass, $sState, $sStimulus, $sAttCode)
2508	{
2509		$iFlags = 0; // By default (if no lifecycle) no flag at all
2510		$sStateAttCode = self::GetStateAttributeCode($sClass);
2511		if (!empty($sStateAttCode))
2512		{
2513			$aTransitions = MetaModel::EnumTransitions($sClass, $sState);
2514			if (!array_key_exists($sStimulus, $aTransitions))
2515			{
2516				throw new CoreException("Invalid transition '$sStimulus' for class '$sClass', expecting a value in {".implode(', ', array_keys($aTransitions))."}");
2517			}
2518
2519			$aCurrentTransition = $aTransitions[$sStimulus];
2520			if ((array_key_exists('attribute_list', $aCurrentTransition)) && (array_key_exists($sAttCode, $aCurrentTransition['attribute_list'])))
2521			{
2522				$iFlags = $aCurrentTransition['attribute_list'][$sAttCode];
2523			}
2524		}
2525
2526		return $iFlags;
2527	}
2528
2529	/**
2530	 * @param string $sClass string Object class
2531	 * @param string $sStimulus string Stimulus code applied
2532	 * @param string $sOriginState string State the stimulus comes from
2533	 *
2534	 * @return array Attribute codes (with their flags) when $sStimulus is applied on an object of $sClass in the $sOriginState state.
2535	 * <strong>Note: Attributes (and flags) from the target state and the transition are combined</strong>
2536	 */
2537	public static function GetTransitionAttributes($sClass, $sStimulus, $sOriginState)
2538	{
2539		$aAttributes = array();
2540
2541		// Retrieving target state
2542		$aTransitions = MetaModel::EnumTransitions($sClass, $sOriginState);
2543		$aTransition = $aTransitions[$sStimulus];
2544		$sTargetState = $aTransition['target_state'];
2545
2546		// Retrieving attributes from state
2547		$aStates = MetaModel::EnumStates($sClass);
2548		$aTargetState = $aStates[$sTargetState];
2549		$aTargetStateAttributes = $aTargetState['attribute_list'];
2550		// - Merging with results (only MUST_XXX and MANDATORY)
2551		foreach($aTargetStateAttributes as $sTargetStateAttCode => $iTargetStateAttFlags)
2552		{
2553			$iTmpAttFlags = OPT_ATT_NORMAL;
2554			if ($iTargetStateAttFlags & OPT_ATT_MUSTPROMPT)
2555			{
2556				$iTmpAttFlags = $iTmpAttFlags | OPT_ATT_MUSTPROMPT;
2557			}
2558			if ($iTargetStateAttFlags & OPT_ATT_MUSTCHANGE)
2559			{
2560				$iTmpAttFlags = $iTmpAttFlags | OPT_ATT_MUSTCHANGE;
2561			}
2562			if ($iTargetStateAttFlags & OPT_ATT_MANDATORY)
2563			{
2564				$iTmpAttFlags = $iTmpAttFlags | OPT_ATT_MANDATORY;
2565			}
2566
2567			$aAttributes[$sTargetStateAttCode] = $iTmpAttFlags;
2568		}
2569
2570		// Retrieving attributes from transition
2571		$aTransitionAttributes = $aTransition['attribute_list'];
2572		// - Merging with results
2573		foreach($aTransitionAttributes as $sAttCode => $iAttributeFlags)
2574		{
2575			if (array_key_exists($sAttCode, $aAttributes))
2576			{
2577				$aAttributes[$sAttCode] = $aAttributes[$sAttCode] | $iAttributeFlags;
2578			}
2579			else
2580			{
2581				$aAttributes[$sAttCode] = $iAttributeFlags;
2582			}
2583		}
2584
2585		return $aAttributes;
2586	}
2587
2588	/**
2589	 * @param string $sClass
2590	 * @param string $sState
2591	 * @param string $sAttCode
2592	 *
2593	 * @return int Combines the flags from the all states that compose the initial_state_path
2594	 * @throws \CoreException
2595	 * @throws \Exception
2596	 */
2597	public static function GetInitialStateAttributeFlags($sClass, $sState, $sAttCode)
2598	{
2599		$iFlags = self::GetAttributeFlags($sClass, $sState, $sAttCode); // Be default set the same flags as the 'target' state
2600		$sStateAttCode = self::GetStateAttributeCode($sClass);
2601		if (!empty($sStateAttCode))
2602		{
2603			$aStates = MetaModel::EnumInitialStates($sClass);
2604			if (array_key_exists($sState, $aStates))
2605			{
2606				$bReadOnly = (($iFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY);
2607				$bHidden = (($iFlags & OPT_ATT_HIDDEN) == OPT_ATT_HIDDEN);
2608				foreach($aStates[$sState] as $sPrevState)
2609				{
2610					$iPrevFlags = self::GetAttributeFlags($sClass, $sPrevState, $sAttCode);
2611					if (($iPrevFlags & OPT_ATT_HIDDEN) != OPT_ATT_HIDDEN)
2612					{
2613						$bReadOnly = $bReadOnly && (($iPrevFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY); // if it is/was not readonly => then it's not
2614					}
2615					$bHidden = $bHidden && (($iPrevFlags & OPT_ATT_HIDDEN) == OPT_ATT_HIDDEN); // if it is/was not hidden => then it's not
2616				}
2617				if ($bReadOnly)
2618				{
2619					$iFlags = $iFlags | OPT_ATT_READONLY;
2620				}
2621				else
2622				{
2623					$iFlags = $iFlags & ~OPT_ATT_READONLY;
2624				}
2625				if ($bHidden)
2626				{
2627					$iFlags = $iFlags | OPT_ATT_HIDDEN;
2628				}
2629				else
2630				{
2631					$iFlags = $iFlags & ~OPT_ATT_HIDDEN;
2632				}
2633			}
2634		}
2635		return $iFlags;
2636	}
2637
2638	/**
2639	 * @param string $sClass
2640	 * @param string $sAttCode
2641	 * @param array $aArgs
2642	 * @param string $sContains
2643	 *
2644	 * @return mixed
2645	 * @throws \Exception
2646	 */
2647	public static function GetAllowedValues_att($sClass, $sAttCode, $aArgs = array(), $sContains = '')
2648	{
2649		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
2650		return $oAttDef->GetAllowedValues($aArgs, $sContains);
2651	}
2652
2653	/**
2654	 * @param string $sClass
2655	 * @param string $sFltCode
2656	 * @param array $aArgs
2657	 * @param string $sContains
2658	 *
2659	 * @return mixed
2660	 * @throws \CoreException
2661	 */
2662	public static function GetAllowedValues_flt($sClass, $sFltCode, $aArgs = array(), $sContains = '')
2663	{
2664		$oFltDef = self::GetClassFilterDef($sClass, $sFltCode);
2665		return $oFltDef->GetAllowedValues($aArgs, $sContains);
2666	}
2667
2668	/**
2669	 * @param string $sClass
2670	 * @param string $sAttCode
2671	 * @param array $aArgs
2672	 * @param string $sContains
2673	 * @param int $iAdditionalValue
2674	 *
2675	 * @return mixed
2676	 * @throws \Exception
2677	 */
2678	public static function GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs = array(), $sContains = '', $iAdditionalValue = null)
2679	{
2680		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
2681		return $oAttDef->GetAllowedValuesAsObjectSet($aArgs, $sContains, $iAdditionalValue);
2682	}
2683
2684
2685
2686	//
2687	// Businezz model declaration verbs (should be static)
2688	//
2689	/**
2690	 * @param string $sListCode
2691	 * @param array $aListInfo
2692	 *
2693	 * @throws \CoreException
2694	 */
2695	public static function RegisterZList($sListCode, $aListInfo)
2696	{
2697		// Check mandatory params
2698		$aMandatParams = array(
2699			"description" => "detailed (though one line) description of the list",
2700			"type" => "attributes | filters",
2701		);
2702		foreach($aMandatParams as $sParamName => $sParamDesc)
2703		{
2704			if (!array_key_exists($sParamName, $aListInfo))
2705			{
2706				throw new CoreException("Declaration of list $sListCode - missing parameter $sParamName");
2707			}
2708		}
2709
2710		self::$m_aListInfos[$sListCode] = $aListInfo;
2711	}
2712
2713	/**
2714	 * @param string $sRelCode
2715	 */
2716	public static function RegisterRelation($sRelCode)
2717	{
2718		// Each item used to be an array of properties...
2719		self::$m_aRelationInfos[$sRelCode] = $sRelCode;
2720	}
2721
2722	/**
2723	 * Helper to correctly add a magic attribute (called from InitClasses)
2724	 *
2725	 * @param \AttributeDefinition $oAttribute
2726	 * @param string $sTargetClass
2727	 * @param string $sOriginClass
2728	 */
2729	private static function AddMagicAttribute(AttributeDefinition $oAttribute, $sTargetClass, $sOriginClass = null)
2730	{
2731		$sCode = $oAttribute->GetCode();
2732		if (is_null($sOriginClass))
2733		{
2734			$sOriginClass = $sTargetClass;
2735		}
2736		$oAttribute->SetHostClass($sTargetClass);
2737		self::$m_aAttribDefs[$sTargetClass][$sCode] = $oAttribute;
2738		self::$m_aAttribOrigins[$sTargetClass][$sCode] = $sOriginClass;
2739
2740		$oFlt = new FilterFromAttribute($oAttribute);
2741		self::$m_aFilterDefs[$sTargetClass][$sCode] = $oFlt;
2742		self::$m_aFilterOrigins[$sTargetClass][$sCode] = $sOriginClass;
2743	}
2744
2745	/**
2746	 * Must be called once and only once...
2747	 *
2748	 * @param string $sTablePrefix
2749	 *
2750	 * @throws \CoreException
2751	 * @throws \Exception
2752	 */
2753	public static function InitClasses($sTablePrefix)
2754	{
2755		if (count(self::GetClasses()) > 0)
2756		{
2757			throw new CoreException("InitClasses should not be called more than once -skipped");
2758		}
2759
2760		self::$m_sTablePrefix = $sTablePrefix;
2761
2762		// Build the list of available extensions
2763		//
2764		$aInterfaces = array('iApplicationUIExtension', 'iApplicationObjectExtension', 'iQueryModifier', 'iOnClassInitialization', 'iPopupMenuExtension', 'iPageUIExtension', 'iPortalUIExtension', 'ModuleHandlerApiInterface', 'iNewsroomProvider');
2765		foreach($aInterfaces as $sInterface)
2766		{
2767			self::$m_aExtensionClasses[$sInterface] = array();
2768		}
2769
2770		foreach(get_declared_classes() as $sPHPClass)
2771		{
2772			$oRefClass = new ReflectionClass($sPHPClass);
2773			$oExtensionInstance = null;
2774			foreach($aInterfaces as $sInterface)
2775			{
2776				if ($oRefClass->implementsInterface($sInterface) && $oRefClass->isInstantiable())
2777				{
2778					if (is_null($oExtensionInstance))
2779					{
2780						$oExtensionInstance = new $sPHPClass;
2781					}
2782					self::$m_aExtensionClasses[$sInterface][$sPHPClass] = $oExtensionInstance;
2783				}
2784			}
2785		}
2786
2787		// Initialize the classes (declared attributes, etc.)
2788		//
2789		$aObsoletableRootClasses = array();
2790		foreach(get_declared_classes() as $sPHPClass)
2791		{
2792			if (is_subclass_of($sPHPClass, 'DBObject'))
2793			{
2794				$sParent = self::GetParentPersistentClass($sPHPClass);
2795				if (array_key_exists($sParent, self::$m_aIgnoredAttributes))
2796				{
2797					// Inherit info about attributes to ignore
2798					self::$m_aIgnoredAttributes[$sPHPClass] = self::$m_aIgnoredAttributes[$sParent];
2799				}
2800				try
2801				{
2802					$oMethod = new ReflectionMethod($sPHPClass, 'Init');
2803					if ($oMethod->getDeclaringClass()->name == $sPHPClass)
2804					{
2805						call_user_func(array($sPHPClass, 'Init'));
2806
2807						// Inherit archive flag
2808						$bParentArchivable = isset(self::$m_aClassParams[$sParent]['archive']) ? self::$m_aClassParams[$sParent]['archive'] : false;
2809						$bArchivable = isset(self::$m_aClassParams[$sPHPClass]['archive']) ? self::$m_aClassParams[$sPHPClass]['archive'] : null;
2810						if (!$bParentArchivable && $bArchivable && !self::IsRootClass($sPHPClass))
2811						{
2812							throw new Exception("Archivability must be declared on top of the class hierarchy above $sPHPClass (consistency throughout the whole class tree is a must)");
2813						}
2814						if ($bParentArchivable && ($bArchivable === false))
2815						{
2816							throw new Exception("$sPHPClass must be archivable (consistency throughout the whole class tree is a must)");
2817						}
2818						$bReallyArchivable = $bParentArchivable || $bArchivable;
2819						self::$m_aClassParams[$sPHPClass]['archive'] = $bReallyArchivable;
2820						$bArchiveRoot = $bReallyArchivable && !$bParentArchivable;
2821						self::$m_aClassParams[$sPHPClass]['archive_root'] = $bArchiveRoot;
2822						if ($bReallyArchivable)
2823						{
2824							self::$m_aClassParams[$sPHPClass]['archive_root_class'] = $bArchiveRoot ? $sPHPClass : self::$m_aClassParams[$sParent]['archive_root_class'];
2825						}
2826
2827						// Inherit obsolescence expression
2828						$sObsolescence = null;
2829						if (isset(self::$m_aClassParams[$sPHPClass]['obsolescence_expression']))
2830						{
2831							// Defined or overloaded
2832							$sObsolescence = self::$m_aClassParams[$sPHPClass]['obsolescence_expression'];
2833							$aObsoletableRootClasses[self::$m_aRootClasses[$sPHPClass]] = true;
2834						}
2835						elseif (isset(self::$m_aClassParams[$sParent]['obsolescence_expression']))
2836						{
2837							// Inherited
2838							$sObsolescence = self::$m_aClassParams[$sParent]['obsolescence_expression'];
2839						}
2840						self::$m_aClassParams[$sPHPClass]['obsolescence_expression'] = $sObsolescence;
2841
2842						foreach(MetaModel::EnumPlugins('iOnClassInitialization') as $sPluginClass => $oClassInit)
2843						{
2844							$oClassInit->OnAfterClassInitialization($sPHPClass);
2845						}
2846					}
2847
2848					$aCurrentClassUniquenessRules = MetaModel::GetUniquenessRules($sPHPClass, true);
2849					if (!empty($aCurrentClassUniquenessRules))
2850					{
2851						$aClassFields = self::GetAttributesList($sPHPClass);
2852						foreach ($aCurrentClassUniquenessRules as $sUniquenessRuleId => $aUniquenessRuleProperties)
2853						{
2854							$bIsRuleOverride = self::HasSameUniquenessRuleInParent($sPHPClass, $sUniquenessRuleId);
2855							try
2856							{
2857								self::CheckUniquenessRuleValidity($aUniquenessRuleProperties, $bIsRuleOverride, $aClassFields);
2858							}
2859							catch (CoreUnexpectedValue $e)
2860							{
2861								throw new Exception("Invalid uniqueness rule declaration : class={$sPHPClass}, rule=$sUniquenessRuleId, reason={$e->getMessage()}");
2862							}
2863
2864							if (!$bIsRuleOverride)
2865							{
2866								self::SetUniquenessRuleRootClass($sPHPClass, $sUniquenessRuleId);
2867							}
2868						}
2869					}
2870
2871				}
2872				catch (ReflectionException $e)
2873				{
2874					// This class is only implementing methods, ignore it from the MetaModel perspective
2875				}
2876			}
2877		}
2878
2879		// Add a 'class' attribute/filter to the root classes and their children
2880		//
2881		foreach(self::EnumRootClasses() as $sRootClass)
2882		{
2883			if (self::IsStandaloneClass($sRootClass))
2884			{
2885				continue;
2886			}
2887
2888			$sDbFinalClassField = self::DBGetClassField($sRootClass);
2889			if (strlen($sDbFinalClassField) == 0)
2890			{
2891				$sDbFinalClassField = 'finalclass';
2892				self::$m_aClassParams[$sRootClass]["db_finalclass_field"] = 'finalclass';
2893			}
2894			$oClassAtt = new AttributeFinalClass('finalclass', array(
2895				"sql" => $sDbFinalClassField,
2896				"default_value" => $sRootClass,
2897				"is_null_allowed" => false,
2898				"depends_on" => array()
2899			));
2900			self::AddMagicAttribute($oClassAtt, $sRootClass);
2901
2902			$bObsoletable = array_key_exists($sRootClass, $aObsoletableRootClasses);
2903			if ($bObsoletable && is_null(self::$m_aClassParams[$sRootClass]['obsolescence_expression']))
2904			{
2905				self::$m_aClassParams[$sRootClass]['obsolescence_expression'] = '0';
2906			}
2907
2908
2909			foreach(self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_EXCLUDETOP) as $sChildClass)
2910			{
2911				if (array_key_exists('finalclass', self::$m_aAttribDefs[$sChildClass]))
2912				{
2913					throw new CoreException("Class $sChildClass, 'finalclass' is a reserved keyword, it cannot be used as an attribute code");
2914				}
2915				if (array_key_exists('finalclass', self::$m_aFilterDefs[$sChildClass]))
2916				{
2917					throw new CoreException("Class $sChildClass, 'finalclass' is a reserved keyword, it cannot be used as a filter code");
2918				}
2919				$oCloned = clone $oClassAtt;
2920				$oCloned->SetFixedValue($sChildClass);
2921				self::AddMagicAttribute($oCloned, $sChildClass, $sRootClass);
2922
2923				if ($bObsoletable && is_null(self::$m_aClassParams[$sChildClass]['obsolescence_expression']))
2924				{
2925					self::$m_aClassParams[$sChildClass]['obsolescence_expression'] = '0';
2926				}
2927			}
2928		}
2929
2930		// Add magic attributes to the classes
2931		foreach(self::GetClasses() as $sClass)
2932		{
2933			$sRootClass = self::$m_aRootClasses[$sClass];
2934
2935			// Create the friendly name attribute
2936			$sFriendlyNameAttCode = 'friendlyname';
2937			$oFriendlyName = new AttributeFriendlyName($sFriendlyNameAttCode);
2938			self::AddMagicAttribute($oFriendlyName, $sClass);
2939
2940			if (self::$m_aClassParams[$sClass]["archive_root"])
2941			{
2942				// Create archive attributes on top the archivable hierarchy
2943				$oArchiveFlag = new AttributeArchiveFlag('archive_flag');
2944				self::AddMagicAttribute($oArchiveFlag, $sClass);
2945
2946				$oArchiveDate = new AttributeArchiveDate('archive_date', array('magic' => true, "allowed_values" => null, "sql" => 'archive_date', "default_value" => '', "is_null_allowed" => true, "depends_on" => array()));
2947				self::AddMagicAttribute($oArchiveDate, $sClass);
2948			}
2949			elseif (self::$m_aClassParams[$sClass]["archive"])
2950			{
2951				$sArchiveRoot = self::$m_aClassParams[$sClass]['archive_root_class'];
2952				// Inherit archive attributes
2953				$oArchiveFlag = clone self::$m_aAttribDefs[$sArchiveRoot]['archive_flag'];
2954				$oArchiveFlag->SetHostClass($sClass);
2955				self::$m_aAttribDefs[$sClass]['archive_flag'] = $oArchiveFlag;
2956				self::$m_aAttribOrigins[$sClass]['archive_flag'] = $sArchiveRoot;
2957				$oArchiveDate = clone self::$m_aAttribDefs[$sArchiveRoot]['archive_date'];
2958				$oArchiveDate->SetHostClass($sClass);
2959				self::$m_aAttribDefs[$sClass]['archive_date'] = $oArchiveDate;
2960				self::$m_aAttribOrigins[$sClass]['archive_date'] = $sArchiveRoot;
2961			}
2962			if (!is_null(self::$m_aClassParams[$sClass]['obsolescence_expression']))
2963			{
2964				$oObsolescenceFlag = new AttributeObsolescenceFlag('obsolescence_flag');
2965				self::AddMagicAttribute($oObsolescenceFlag, $sClass);
2966
2967				if (self::$m_aRootClasses[$sClass] == $sClass)
2968				{
2969					$oObsolescenceDate = new AttributeObsolescenceDate('obsolescence_date', array('magic' => true, "allowed_values" => null, "sql" => 'obsolescence_date', "default_value" => '', "is_null_allowed" => true, "depends_on" => array()));
2970					self::AddMagicAttribute($oObsolescenceDate, $sClass);
2971				}
2972				else
2973				{
2974					$oObsolescenceDate = clone self::$m_aAttribDefs[$sRootClass]['obsolescence_date'];
2975					$oObsolescenceDate->SetHostClass($sClass);
2976					self::$m_aAttribDefs[$sClass]['obsolescence_date'] = $oObsolescenceDate;
2977					self::$m_aAttribOrigins[$sClass]['obsolescence_date'] = $sRootClass;
2978				}
2979			}
2980		}
2981
2982		// Prepare external fields and filters
2983		// Add final class to external keys
2984		// Add magic attributes to external keys (finalclass, friendlyname, archive_flag, obsolescence_flag)
2985		foreach(self::GetClasses() as $sClass)
2986		{
2987			foreach(self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef)
2988			{
2989				// Compute the filter codes
2990				//
2991				foreach($oAttDef->GetFilterDefinitions() as $sFilterCode => $oFilterDef)
2992				{
2993					self::$m_aFilterDefs[$sClass][$sFilterCode] = $oFilterDef;
2994
2995					if ($oAttDef->IsExternalField())
2996					{
2997						$sKeyAttCode = $oAttDef->GetKeyAttCode();
2998						$oKeyDef = self::GetAttributeDef($sClass, $sKeyAttCode);
2999						self::$m_aFilterOrigins[$sClass][$sFilterCode] = $oKeyDef->GetTargetClass();
3000					}
3001					else
3002					{
3003						self::$m_aFilterOrigins[$sClass][$sFilterCode] = self::$m_aAttribOrigins[$sClass][$sAttCode];
3004					}
3005				}
3006
3007				// Compute the fields that will be used to display a pointer to another object
3008				//
3009				if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE))
3010				{
3011					// oAttDef is either
3012					// - an external KEY / FIELD (direct),
3013					// - an external field pointing to an external KEY / FIELD
3014					// - an external field pointing to an external field pointing to....
3015					$sRemoteClass = $oAttDef->GetTargetClass(EXTKEY_ABSOLUTE);
3016
3017					if ($oAttDef->IsExternalField())
3018					{
3019						// This is a key, but the value comes from elsewhere
3020						// Create an external field pointing to the remote friendly name attribute
3021						$sKeyAttCode = $oAttDef->GetKeyAttCode();
3022						$sRemoteAttCode = $oAttDef->GetExtAttCode()."_friendlyname";
3023						$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
3024						$oFriendlyName = new AttributeExternalField($sFriendlyNameAttCode, array("allowed_values" => null, "extkey_attcode" => $sKeyAttCode, "target_attcode" => $sRemoteAttCode, "depends_on" => array()));
3025						self::AddMagicAttribute($oFriendlyName, $sClass, self::$m_aAttribOrigins[$sClass][$sKeyAttCode]);
3026					}
3027					else
3028					{
3029						// Create the friendly name attribute
3030						$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
3031						$oFriendlyName = new AttributeExternalField($sFriendlyNameAttCode, array('allowed_values' => null, 'extkey_attcode' => $sAttCode, "target_attcode" => 'friendlyname', 'depends_on' => array()));
3032						self::AddMagicAttribute($oFriendlyName, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]);
3033
3034						if (self::HasChildrenClasses($sRemoteClass))
3035						{
3036							// First, create an external field attribute, that gets the final class
3037							$sClassRecallAttCode = $sAttCode.'_finalclass_recall';
3038							$oClassRecall = new AttributeExternalField($sClassRecallAttCode, array(
3039								"allowed_values" => null,
3040								"extkey_attcode" => $sAttCode,
3041								"target_attcode" => "finalclass",
3042								"is_null_allowed" => true,
3043								"depends_on" => array()
3044							));
3045							self::AddMagicAttribute($oClassRecall, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]);
3046
3047							// Add it to the ZLists where the external key is present
3048							//foreach(self::$m_aListData[$sClass] as $sListCode => $aAttributes)
3049							$sListCode = 'list';
3050							if (isset(self::$m_aListData[$sClass][$sListCode]))
3051							{
3052								$aAttributes = self::$m_aListData[$sClass][$sListCode];
3053								// temporary.... no loop
3054								{
3055									if (in_array($sAttCode, $aAttributes))
3056									{
3057										$aNewList = array();
3058										foreach($aAttributes as $iPos => $sAttToDisplay)
3059										{
3060											if (is_string($sAttToDisplay) && ($sAttToDisplay == $sAttCode))
3061											{
3062												// Insert the final class right before
3063												$aNewList[] = $sClassRecallAttCode;
3064											}
3065											$aNewList[] = $sAttToDisplay;
3066										}
3067										self::$m_aListData[$sClass][$sListCode] = $aNewList;
3068									}
3069								}
3070							}
3071						}
3072					}
3073
3074					if (self::IsArchivable($sRemoteClass))
3075					{
3076						$sCode = $sAttCode.'_archive_flag';
3077						if ($oAttDef->IsExternalField())
3078						{
3079							// This is a key, but the value comes from elsewhere
3080							// Create an external field pointing to the remote attribute
3081							$sKeyAttCode = $oAttDef->GetKeyAttCode();
3082							$sRemoteAttCode = $oAttDef->GetExtAttCode().'_archive_flag';
3083						}
3084						else
3085						{
3086							$sKeyAttCode = $sAttCode;
3087							$sRemoteAttCode = 'archive_flag';
3088						}
3089						$oMagic = new AttributeExternalField($sCode, array("allowed_values" => null, "extkey_attcode" => $sKeyAttCode, "target_attcode" => $sRemoteAttCode, "depends_on" => array()));
3090						self::AddMagicAttribute($oMagic, $sClass, self::$m_aAttribOrigins[$sClass][$sKeyAttCode]);
3091
3092					}
3093					if (self::IsObsoletable($sRemoteClass))
3094					{
3095						$sCode = $sAttCode.'_obsolescence_flag';
3096						if ($oAttDef->IsExternalField())
3097						{
3098							// This is a key, but the value comes from elsewhere
3099							// Create an external field pointing to the remote attribute
3100							$sKeyAttCode = $oAttDef->GetKeyAttCode();
3101							$sRemoteAttCode = $oAttDef->GetExtAttCode().'_obsolescence_flag';
3102						}
3103						else
3104						{
3105							$sKeyAttCode = $sAttCode;
3106							$sRemoteAttCode = 'obsolescence_flag';
3107						}
3108						$oMagic = new AttributeExternalField($sCode, array("allowed_values" => null, "extkey_attcode" => $sKeyAttCode, "target_attcode" => $sRemoteAttCode, "depends_on" => array()));
3109						self::AddMagicAttribute($oMagic, $sClass, self::$m_aAttribOrigins[$sClass][$sKeyAttCode]);
3110					}
3111				}
3112				if ($oAttDef instanceof AttributeMetaEnum)
3113				{
3114					$aMappingData = $oAttDef->GetMapRule($sClass);
3115					if ($aMappingData != null)
3116					{
3117						$sEnumAttCode = $aMappingData['attcode'];
3118						self::$m_aEnumToMeta[$sClass][$sEnumAttCode][$sAttCode] = $oAttDef;
3119					}
3120				}
3121			}
3122
3123			// Add a 'id' filter
3124			//
3125			if (array_key_exists('id', self::$m_aAttribDefs[$sClass]))
3126			{
3127				throw new CoreException("Class $sClass, 'id' is a reserved keyword, it cannot be used as an attribute code");
3128			}
3129			if (array_key_exists('id', self::$m_aFilterDefs[$sClass]))
3130			{
3131				throw new CoreException("Class $sClass, 'id' is a reserved keyword, it cannot be used as a filter code");
3132			}
3133			$oFilter = new FilterPrivateKey('id', array('id_field' => self::DBGetKey($sClass)));
3134			self::$m_aFilterDefs[$sClass]['id'] = $oFilter;
3135			self::$m_aFilterOrigins[$sClass]['id'] = $sClass;
3136		}
3137	}
3138
3139	/**
3140	 * @param string $sClassName
3141	 * @param string $sUniquenessRuleId
3142	 *
3143	 * @return bool true if one of the parent class (recursive) has the same rule defined
3144	 * @throws \CoreException
3145	 */
3146	private static function HasSameUniquenessRuleInParent($sClassName, $sUniquenessRuleId)
3147	{
3148		$sParentClass = self::GetParentClass($sClassName);
3149		if (empty($sParentClass))
3150		{
3151			return false;
3152		}
3153
3154		$aParentClassUniquenessRules = self::GetUniquenessRules($sParentClass);
3155		if (array_key_exists($sUniquenessRuleId, $aParentClassUniquenessRules))
3156		{
3157			return true;
3158		}
3159
3160		return self::HasSameUniquenessRuleInParent($sParentClass, $sUniquenessRuleId);
3161	}
3162
3163	/**
3164	 * @param array $aUniquenessRuleProperties
3165	 * @param bool $bRuleOverride if false then control an original declaration validity,
3166	 *                   otherwise an override validity (can only have the 'disabled' key)
3167	 * @param string[] $aExistingClassFields if non empty, will check that all fields declared in the rules exists in the class
3168	 *
3169	 * @throws \CoreUnexpectedValue if the rule is invalid
3170	 *
3171	 * @since 2.6 N°659 uniqueness constraint
3172	 * @since 2.6.1 N°1968 (joli mois de mai...) disallow overrides of 'attributes' properties
3173	 */
3174	public static function CheckUniquenessRuleValidity($aUniquenessRuleProperties, $bRuleOverride = true, $aExistingClassFields = array())
3175	{
3176		$MANDATORY_ATTRIBUTES = array('attributes');
3177		$UNIQUENESS_MANDATORY_KEYS_NB = count($MANDATORY_ATTRIBUTES);
3178
3179		$bHasMissingMandatoryKey = true;
3180		$iMissingMandatoryKeysNb = $UNIQUENESS_MANDATORY_KEYS_NB;
3181		/** @var boolean $bHasNonDisabledKeys true if rule contains at least one key that is not 'disabled' */
3182		$bHasNonDisabledKeys = false;
3183		$bDisabledKeyValue = null;
3184
3185		foreach ($aUniquenessRuleProperties as $sUniquenessRuleKey => $aUniquenessRuleProperty)
3186		{
3187			if ($sUniquenessRuleKey === 'disabled')
3188			{
3189				$bDisabledKeyValue = $aUniquenessRuleProperty;
3190				if (!is_null($aUniquenessRuleProperty))
3191				{
3192					continue;
3193				}
3194			}
3195			if (is_null($aUniquenessRuleProperty))
3196			{
3197				continue;
3198			}
3199
3200			$bHasNonDisabledKeys = true;
3201
3202			if (in_array($sUniquenessRuleKey, $MANDATORY_ATTRIBUTES, true)) {
3203				$iMissingMandatoryKeysNb--;
3204			}
3205
3206			if ($sUniquenessRuleKey === 'attributes')
3207			{
3208				if (!empty($aExistingClassFields))
3209				{
3210					foreach ($aUniquenessRuleProperties[$sUniquenessRuleKey] as $sRuleAttribute)
3211					{
3212						if (!in_array($sRuleAttribute, $aExistingClassFields, true))
3213						{
3214							throw new CoreUnexpectedValue("Uniqueness rule : non existing field '$sRuleAttribute'");
3215						}
3216					}
3217				}
3218			}
3219		}
3220
3221		if ($iMissingMandatoryKeysNb === 0)
3222		{
3223			$bHasMissingMandatoryKey = false;
3224		}
3225
3226		if ($bRuleOverride && $bHasNonDisabledKeys)
3227		{
3228			throw new CoreUnexpectedValue('Uniqueness rule : only the \'disabled\' key can be overridden');
3229		}
3230		if ($bRuleOverride && is_null($bDisabledKeyValue))
3231		{
3232			throw new CoreUnexpectedValue('Uniqueness rule : when overriding a rule, value must be set for the \'disabled\' key');
3233		}
3234		if (!$bRuleOverride && $bHasMissingMandatoryKey)
3235		{
3236			throw new CoreUnexpectedValue('Uniqueness rule : missing mandatory property');
3237		}
3238	}
3239
3240	/**
3241	 * To be overriden, must be called for any object class (optimization)
3242	 */
3243	public static function Init()
3244	{
3245		// In fact it is an ABSTRACT function, but this is not compatible with the fact that it is STATIC (error in E_STRICT interpretation)
3246	}
3247
3248	/**
3249	 * To be overloaded by biz model declarations
3250	 *
3251	 * @param string $sRelCode
3252	 *
3253	 * @return array
3254	 */
3255	public static function GetRelationQueries($sRelCode)
3256	{
3257		// In fact it is an ABSTRACT function, but this is not compatible with the fact that it is STATIC (error in E_STRICT interpretation)
3258		return array();
3259	}
3260
3261	/**
3262	 * @param array $aParams
3263	 *
3264	 * @throws \CoreException
3265	 */
3266	public static function Init_Params($aParams)
3267	{
3268		// Check mandatory params
3269		$aMandatParams = array(
3270			"category" => "group classes by modules defining their visibility in the UI",
3271			"key_type" => "autoincrement | string",
3272			"name_attcode" => "define wich attribute is the class name, may be an array of attributes (format specified in the dictionary as 'Class:myclass/Name' => '%1\$s %2\$s...'",
3273			"state_attcode" => "define wich attribute is representing the state (object lifecycle)",
3274			"reconc_keys" => "define the attributes that will 'almost uniquely' identify an object in batch processes",
3275			"db_table" => "database table",
3276			"db_key_field" => "database field which is the key",
3277			"db_finalclass_field" => "database field wich is the reference to the actual class of the object, considering that this will be a compound class",
3278		);
3279
3280		$sClass = self::GetCallersPHPClass("Init", self::$m_bTraceSourceFiles);
3281
3282		foreach($aMandatParams as $sParamName => $sParamDesc)
3283		{
3284			if (!array_key_exists($sParamName, $aParams))
3285			{
3286				throw new CoreException("Declaration of class $sClass - missing parameter $sParamName");
3287			}
3288		}
3289
3290		$aCategories = explode(',', $aParams['category']);
3291		foreach($aCategories as $sCategory)
3292		{
3293			self::$m_Category2Class[$sCategory][] = $sClass;
3294		}
3295		self::$m_Category2Class[''][] = $sClass; // all categories, include this one
3296
3297
3298		self::$m_aRootClasses[$sClass] = $sClass; // first, let consider that I am the root... updated on inheritance
3299		self::$m_aParentClasses[$sClass] = array();
3300		self::$m_aChildClasses[$sClass] = array();
3301
3302		self::$m_aClassParams[$sClass] = $aParams;
3303
3304		self::$m_aAttribDefs[$sClass] = array();
3305		self::$m_aAttribOrigins[$sClass] = array();
3306		self::$m_aFilterDefs[$sClass] = array();
3307		self::$m_aFilterOrigins[$sClass] = array();
3308	}
3309
3310	/**
3311	 * @param array $aSource1
3312	 * @param array $aSource2
3313	 *
3314	 * @return array
3315	 */
3316	protected static function object_array_mergeclone($aSource1, $aSource2)
3317	{
3318		$aRes = array();
3319		foreach($aSource1 as $key => $object)
3320		{
3321			$aRes[$key] = clone $object;
3322		}
3323		foreach($aSource2 as $key => $object)
3324		{
3325			$aRes[$key] = clone $object;
3326		}
3327
3328		return $aRes;
3329	}
3330
3331	/**
3332	 * @param string $sSourceClass
3333	 */
3334	public static function Init_InheritAttributes($sSourceClass = null)
3335	{
3336		$sTargetClass = self::GetCallersPHPClass("Init");
3337		if (empty($sSourceClass))
3338		{
3339			// Default: inherit from parent class
3340			$sSourceClass = self::GetParentPersistentClass($sTargetClass);
3341			if (empty($sSourceClass))
3342			{
3343				return;
3344			} // no attributes for the mother of all classes
3345		}
3346		if (isset(self::$m_aAttribDefs[$sSourceClass]))
3347		{
3348			if (!isset(self::$m_aAttribDefs[$sTargetClass]))
3349			{
3350				self::$m_aAttribDefs[$sTargetClass] = array();
3351				self::$m_aAttribOrigins[$sTargetClass] = array();
3352			}
3353			self::$m_aAttribDefs[$sTargetClass] = self::object_array_mergeclone(self::$m_aAttribDefs[$sTargetClass], self::$m_aAttribDefs[$sSourceClass]);
3354			foreach(self::$m_aAttribDefs[$sTargetClass] as $sAttCode => $oAttDef)
3355			{
3356				$oAttDef->SetHostClass($sTargetClass);
3357			}
3358			self::$m_aAttribOrigins[$sTargetClass] = array_merge(self::$m_aAttribOrigins[$sTargetClass], self::$m_aAttribOrigins[$sSourceClass]);
3359		}
3360		// Build root class information
3361		if (array_key_exists($sSourceClass, self::$m_aRootClasses))
3362		{
3363			// Inherit...
3364			self::$m_aRootClasses[$sTargetClass] = self::$m_aRootClasses[$sSourceClass];
3365		}
3366		else
3367		{
3368			// This class will be the root class
3369			self::$m_aRootClasses[$sSourceClass] = $sSourceClass;
3370			self::$m_aRootClasses[$sTargetClass] = $sSourceClass;
3371		}
3372		self::$m_aParentClasses[$sTargetClass] += self::$m_aParentClasses[$sSourceClass];
3373		self::$m_aParentClasses[$sTargetClass][] = $sSourceClass;
3374		// I am the child of each and every parent...
3375		foreach(self::$m_aParentClasses[$sTargetClass] as $sAncestorClass)
3376		{
3377			self::$m_aChildClasses[$sAncestorClass][] = $sTargetClass;
3378		}
3379	}
3380
3381	/**
3382	 * @param string $sClass
3383	 *
3384	 * @return bool
3385	 */
3386	protected static function Init_IsKnownClass($sClass)
3387	{
3388		// Differs from self::IsValidClass()
3389		// because it is being called before all the classes have been initialized
3390		if (!class_exists($sClass))
3391		{
3392			return false;
3393		}
3394		if (!is_subclass_of($sClass, 'DBObject'))
3395		{
3396			return false;
3397		}
3398
3399		return true;
3400	}
3401
3402	/**
3403	 * @param \AttributeDefinition $oAtt
3404	 * @param string $sTargetClass
3405	 *
3406	 * @throws \Exception
3407	 */
3408	public static function Init_AddAttribute(AttributeDefinition $oAtt, $sTargetClass = null)
3409	{
3410		if (!$sTargetClass)
3411		{
3412			$sTargetClass = self::GetCallersPHPClass("Init");
3413		}
3414
3415		$sAttCode = $oAtt->GetCode();
3416		if ($sAttCode == 'finalclass')
3417		{
3418			throw new Exception("Declaration of $sTargetClass: using the reserved keyword '$sAttCode' in attribute declaration");
3419		}
3420		if ($sAttCode == 'friendlyname')
3421		{
3422			throw new Exception("Declaration of $sTargetClass: using the reserved keyword '$sAttCode' in attribute declaration");
3423		}
3424		if (array_key_exists($sAttCode, self::$m_aAttribDefs[$sTargetClass]))
3425		{
3426			throw new Exception("Declaration of $sTargetClass: attempting to redeclare the inherited attribute '$sAttCode', originaly declared in ".self::$m_aAttribOrigins[$sTargetClass][$sAttCode]);
3427		}
3428
3429		// Set the "host class" as soon as possible, since HierarchicalKeys use it for their 'target class' as well
3430		// and this needs to be know early (for Init_IsKnowClass 19 lines below)
3431		$oAtt->SetHostClass($sTargetClass);
3432
3433		// Some attributes could refer to a class
3434		// declared in a module which is currently not installed/active
3435		// We simply discard those attributes
3436		//
3437		if ($oAtt->IsLinkSet())
3438		{
3439			$sRemoteClass = $oAtt->GetLinkedClass();
3440			if (!self::Init_IsKnownClass($sRemoteClass))
3441			{
3442				self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = $sRemoteClass;
3443				return;
3444			}
3445		}
3446		elseif ($oAtt->IsExternalKey())
3447		{
3448			$sRemoteClass = $oAtt->GetTargetClass();
3449			if (!self::Init_IsKnownClass($sRemoteClass))
3450			{
3451				self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = $sRemoteClass;
3452				return;
3453			}
3454		}
3455		elseif ($oAtt->IsExternalField())
3456		{
3457			$sExtKeyAttCode = $oAtt->GetKeyAttCode();
3458			if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$sExtKeyAttCode]))
3459			{
3460				// The corresponding external key has already been ignored
3461				self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = self::$m_aIgnoredAttributes[$sTargetClass][$sExtKeyAttCode];
3462				return;
3463			}
3464			//TODO Check if the target attribute is still there
3465			// this is not simple to implement because is involves
3466			// several passes (the load order has a significant influence on that)
3467		}
3468
3469		self::$m_aAttribDefs[$sTargetClass][$oAtt->GetCode()] = $oAtt;
3470		self::$m_aAttribOrigins[$sTargetClass][$oAtt->GetCode()] = $sTargetClass;
3471		// Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used
3472	}
3473
3474	/**
3475	 * @param string $sListCode
3476	 * @param array $aItems
3477	 * @param string $sTargetClass
3478	 */
3479	public static function Init_SetZListItems($sListCode, $aItems, $sTargetClass = null)
3480	{
3481		MyHelpers::CheckKeyInArray('list code', $sListCode, self::$m_aListInfos);
3482
3483		if (!$sTargetClass)
3484		{
3485			$sTargetClass = self::GetCallersPHPClass("Init");
3486		}
3487
3488		// Discard attributes that do not make sense
3489		// (missing classes in the current module combination, resulting in irrelevant ext key or link set)
3490		//
3491		self::Init_CheckZListItems($aItems, $sTargetClass);
3492		self::$m_aListData[$sTargetClass][$sListCode] = $aItems;
3493	}
3494
3495	/**
3496	 * @param array $aItems
3497	 * @param string $sTargetClass
3498	 */
3499	protected static function Init_CheckZListItems(&$aItems, $sTargetClass)
3500	{
3501		foreach($aItems as $iFoo => $attCode)
3502		{
3503			if (is_array($attCode))
3504			{
3505				// Note: to make sure that the values will be updated recursively,
3506				//  do not pass $attCode, but $aItems[$iFoo] instead
3507				self::Init_CheckZListItems($aItems[$iFoo], $sTargetClass);
3508				if (count($aItems[$iFoo]) == 0)
3509				{
3510					unset($aItems[$iFoo]);
3511				}
3512			}
3513			else
3514			{
3515				if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$attCode]))
3516				{
3517					unset($aItems[$iFoo]);
3518				}
3519			}
3520		}
3521	}
3522
3523	/**
3524	 * @param array $aList
3525	 *
3526	 * @return array
3527	 */
3528	public static function FlattenZList($aList)
3529	{
3530		$aResult = array();
3531		foreach($aList as $value)
3532		{
3533			if (!is_array($value))
3534			{
3535				$aResult[] = $value;
3536			}
3537			else
3538			{
3539				$aResult = array_merge($aResult, self::FlattenZList($value));
3540			}
3541		}
3542
3543		return $aResult;
3544	}
3545
3546	/**
3547	 * @param string $sStateCode
3548	 * @param array $aStateDef
3549	 */
3550	public static function Init_DefineState($sStateCode, $aStateDef)
3551	{
3552		$sTargetClass = self::GetCallersPHPClass("Init");
3553		if (is_null($aStateDef['attribute_list']))
3554		{
3555			$aStateDef['attribute_list'] = array();
3556		}
3557
3558		$sParentState = $aStateDef['attribute_inherit'];
3559		if (!empty($sParentState))
3560		{
3561			// Inherit from the given state (must be defined !)
3562			//
3563			$aToInherit = self::$m_aStates[$sTargetClass][$sParentState];
3564
3565			// Reset the constraint when it was mandatory to set the value at the previous state
3566			//
3567			foreach($aToInherit['attribute_list'] as $sState => $iFlags)
3568			{
3569				$iFlags = $iFlags & ~OPT_ATT_MUSTPROMPT;
3570				$iFlags = $iFlags & ~OPT_ATT_MUSTCHANGE;
3571				$aToInherit['attribute_list'][$sState] = $iFlags;
3572			}
3573
3574			// The inherited configuration could be overriden
3575			$aStateDef['attribute_list'] = array_merge($aToInherit['attribute_list'], $aStateDef['attribute_list']);
3576		}
3577
3578		foreach($aStateDef['attribute_list'] as $sAttCode => $iFlags)
3579		{
3580			if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$sAttCode]))
3581			{
3582				unset($aStateDef['attribute_list'][$sAttCode]);
3583			}
3584		}
3585
3586		self::$m_aStates[$sTargetClass][$sStateCode] = $aStateDef;
3587
3588		// by default, create an empty set of transitions associated to that state
3589		self::$m_aTransitions[$sTargetClass][$sStateCode] = array();
3590	}
3591
3592	/**
3593	 * @param array $aHighlightScale
3594	 */
3595	public static function Init_DefineHighlightScale($aHighlightScale)
3596	{
3597		$sTargetClass = self::GetCallersPHPClass("Init");
3598		self::$m_aHighlightScales[$sTargetClass] = $aHighlightScale;
3599	}
3600
3601	/**
3602	 * @param string $sTargetClass
3603	 *
3604	 * @return array
3605	 */
3606	public static function GetHighlightScale($sTargetClass)
3607	{
3608		$aScale = array();
3609		$aParentScale = array();
3610		$sParentClass = self::GetParentPersistentClass($sTargetClass);
3611		if (!empty($sParentClass))
3612		{
3613			// inherit the scale from the parent class
3614			$aParentScale = self::GetHighlightScale($sParentClass);
3615		}
3616		if (array_key_exists($sTargetClass, self::$m_aHighlightScales))
3617		{
3618			$aScale = self::$m_aHighlightScales[$sTargetClass];
3619		}
3620		return array_merge($aParentScale, $aScale); // Merge both arrays, the values from the last one have precedence
3621	}
3622
3623	/**
3624	 * @param string $sTargetClass
3625	 * @param string $sStateCode
3626	 *
3627	 * @return string
3628	 */
3629	public static function GetHighlightCode($sTargetClass, $sStateCode)
3630	{
3631		$sCode = '';
3632		if (array_key_exists($sTargetClass, self::$m_aStates)
3633			&& array_key_exists($sStateCode, self::$m_aStates[$sTargetClass])
3634			&& array_key_exists('highlight', self::$m_aStates[$sTargetClass][$sStateCode]))
3635		{
3636			$sCode = self::$m_aStates[$sTargetClass][$sStateCode]['highlight']['code'];
3637		}
3638		else
3639		{
3640			// Check the parent's definition
3641			$sParentClass = self::GetParentPersistentClass($sTargetClass);
3642			if (!empty($sParentClass))
3643			{
3644				$sCode = self::GetHighlightCode($sParentClass, $sStateCode);
3645			}
3646		}
3647
3648		return $sCode;
3649	}
3650
3651	/**
3652	 * @param string $sStateCode
3653	 * @param string $sAttCode
3654	 * @param int $iFlags
3655	 */
3656	public static function Init_OverloadStateAttribute($sStateCode, $sAttCode, $iFlags)
3657	{
3658		// Warning: this is not sufficient: the flags have to be copied to the states that are inheriting from this state
3659		$sTargetClass = self::GetCallersPHPClass("Init");
3660		self::$m_aStates[$sTargetClass][$sStateCode]['attribute_list'][$sAttCode] = $iFlags;
3661	}
3662
3663	/**
3664	 * @param ObjectStimulus $oStimulus
3665	 */
3666	public static function Init_DefineStimulus($oStimulus)
3667	{
3668		$sTargetClass = self::GetCallersPHPClass("Init");
3669		self::$m_aStimuli[$sTargetClass][$oStimulus->GetCode()] = $oStimulus;
3670
3671		// I wanted to simplify the syntax of the declaration of objects in the biz model
3672		// Therefore, the reference to the host class is set there
3673		$oStimulus->SetHostClass($sTargetClass);
3674	}
3675
3676	/**
3677	 * @param string $sStateCode
3678	 * @param string $sStimulusCode
3679	 * @param array $aTransitionDef
3680	 */
3681	public static function Init_DefineTransition($sStateCode, $sStimulusCode, $aTransitionDef)
3682	{
3683		$sTargetClass = self::GetCallersPHPClass("Init");
3684		if (is_null($aTransitionDef['actions']))
3685		{
3686			$aTransitionDef['actions'] = array();
3687		}
3688		self::$m_aTransitions[$sTargetClass][$sStateCode][$sStimulusCode] = $aTransitionDef;
3689	}
3690
3691	/**
3692	 * @param string $sSourceClass
3693	 */
3694	public static function Init_InheritLifecycle($sSourceClass = '')
3695	{
3696		$sTargetClass = self::GetCallersPHPClass("Init");
3697		if (empty($sSourceClass))
3698		{
3699			// Default: inherit from parent class
3700			$sSourceClass = self::GetParentPersistentClass($sTargetClass);
3701			if (empty($sSourceClass))
3702			{
3703				return;
3704			} // no attributes for the mother of all classes
3705		}
3706
3707		self::$m_aClassParams[$sTargetClass]["state_attcode"] = self::$m_aClassParams[$sSourceClass]["state_attcode"];
3708		self::$m_aStates[$sTargetClass] = self::$m_aStates[$sSourceClass];
3709		// #@# Note: the aim is to clone the data, could be an issue if the simuli objects are changed
3710		self::$m_aStimuli[$sTargetClass] = self::$m_aStimuli[$sSourceClass];
3711		self::$m_aTransitions[$sTargetClass] = self::$m_aTransitions[$sSourceClass];
3712	}
3713
3714	//
3715	// Static API
3716	//
3717
3718	/**
3719	 * @param string $sClass
3720	 *
3721	 * @return string
3722	 * @throws \CoreException
3723	 */
3724	public static function GetRootClass($sClass = null)
3725	{
3726		self::_check_subclass($sClass);
3727		return self::$m_aRootClasses[$sClass];
3728	}
3729
3730	/**
3731	 * @param string $sClass
3732	 *
3733	 * @return bool
3734	 * @throws \CoreException
3735	 */
3736	public static function IsRootClass($sClass)
3737	{
3738		self::_check_subclass($sClass);
3739		return (self::GetRootClass($sClass) == $sClass);
3740	}
3741
3742	/**
3743	 * @param string $sClass
3744	 *
3745	 * @return string
3746	 */
3747	public static function GetParentClass($sClass)
3748	{
3749		if (count(self::$m_aParentClasses[$sClass]) == 0)
3750		{
3751			return null;
3752		}
3753		else
3754		{
3755			return end(self::$m_aParentClasses[$sClass]);
3756		}
3757	}
3758
3759	/**
3760	 * @param string[] $aClasses
3761	 *
3762	 * @return string
3763	 * @throws \CoreException
3764	 */
3765	public static function GetLowestCommonAncestor($aClasses)
3766	{
3767		$sAncestor = null;
3768		foreach($aClasses as $sClass)
3769		{
3770			if (is_null($sAncestor))
3771			{
3772				// first loop
3773				$sAncestor = $sClass;
3774			}
3775			elseif ($sClass == $sAncestor)
3776			{
3777				// remains the same
3778			}
3779			elseif (self::GetRootClass($sClass) != self::GetRootClass($sAncestor))
3780			{
3781				$sAncestor = null;
3782				break;
3783			}
3784			else
3785			{
3786				$sAncestor = self::LowestCommonAncestor($sAncestor, $sClass);
3787			}
3788		}
3789		return $sAncestor;
3790	}
3791
3792	/**
3793	 * Note: assumes that class A and B have a common ancestor
3794	 *
3795	 * @param string $sClassA
3796	 * @param string $sClassB
3797	 *
3798	 * @return string
3799	 */
3800	protected static function LowestCommonAncestor($sClassA, $sClassB)
3801	{
3802		if ($sClassA == $sClassB)
3803		{
3804			$sRet = $sClassA;
3805		}
3806		elseif (is_subclass_of($sClassA, $sClassB))
3807		{
3808			$sRet = $sClassB;
3809		}
3810		elseif (is_subclass_of($sClassB, $sClassA))
3811		{
3812			$sRet = $sClassA;
3813		}
3814		else
3815		{
3816			// Recurse
3817			$sRet = self::LowestCommonAncestor($sClassA, self::GetParentClass($sClassB));
3818		}
3819		return $sRet;
3820	}
3821
3822	/**
3823	 * Tells if a class contains a hierarchical key, and if so what is its AttCode
3824	 *
3825	 * @param string $sClass
3826	 *
3827	 * @return mixed String = sAttCode or false if the class is not part of a hierarchy
3828	 * @throws \CoreException
3829	 */
3830	public static function IsHierarchicalClass($sClass)
3831	{
3832		$sHierarchicalKeyCode = false;
3833		foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAtt)
3834		{
3835			if ($oAtt->IsHierarchicalKey())
3836			{
3837				$sHierarchicalKeyCode = $sAttCode; // Found the hierarchical key, no need to continue
3838				break;
3839			}
3840		}
3841		return $sHierarchicalKeyCode;
3842	}
3843
3844	/**
3845	 * @return array
3846	 */
3847	public static function EnumRootClasses()
3848	{
3849		return array_unique(self::$m_aRootClasses);
3850	}
3851
3852	/**
3853	 * @param string $sClass
3854	 * @param int $iOption
3855	 * @param bool $bRootFirst
3856	 *
3857	 * @return array
3858	 * @throws \CoreException
3859	 */
3860	public static function EnumParentClasses($sClass, $iOption = ENUM_PARENT_CLASSES_EXCLUDELEAF, $bRootFirst = true)
3861	{
3862		self::_check_subclass($sClass);
3863		if ($bRootFirst)
3864		{
3865			$aRes = self::$m_aParentClasses[$sClass];
3866		}
3867		else
3868		{
3869			$aRes = array_reverse(self::$m_aParentClasses[$sClass], true);
3870		}
3871		if ($iOption != ENUM_PARENT_CLASSES_EXCLUDELEAF)
3872		{
3873			if ($bRootFirst)
3874			{
3875				// Leaf class at the end
3876				$aRes[] = $sClass;
3877			}
3878			else
3879			{
3880				// Leaf class on top
3881				array_unshift($aRes, $sClass);
3882			}
3883		}
3884
3885		return $aRes;
3886	}
3887
3888	/**
3889	 * @param string $sClass
3890	 * @param int $iOption one of ENUM_CHILD_CLASSES_EXCLUDETOP, ENUM_CHILD_CLASSES_ALL
3891	 *
3892	 * @return array
3893	 * @throws \CoreException
3894	 */
3895	public static function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP)
3896	{
3897		self::_check_subclass($sClass);
3898
3899		$aRes = self::$m_aChildClasses[$sClass];
3900		if ($iOption != ENUM_CHILD_CLASSES_EXCLUDETOP)
3901		{
3902			// Add it to the list
3903			$aRes[] = $sClass;
3904		}
3905
3906		return $aRes;
3907	}
3908
3909	/**
3910	 * @return array
3911	 * @throws \CoreException
3912	 */
3913	public static function EnumArchivableClasses()
3914	{
3915		$aRes = array();
3916		foreach(self::GetClasses() as $sClass)
3917		{
3918			if (self::IsArchivable($sClass))
3919			{
3920				$aRes[] = $sClass;
3921			}
3922		}
3923
3924		return $aRes;
3925	}
3926
3927	/**
3928	 * @param bool $bRootClassesOnly
3929	 *
3930	 * @return array
3931	 * @throws \CoreException
3932	 */
3933	public static function EnumObsoletableClasses($bRootClassesOnly = true)
3934	{
3935		$aRes = array();
3936		foreach(self::GetClasses() as $sClass)
3937		{
3938			if (self::IsObsoletable($sClass))
3939			{
3940				if ($bRootClassesOnly && !static::IsRootClass($sClass))
3941				{
3942					continue;
3943				}
3944				$aRes[] = $sClass;
3945			}
3946		}
3947		return $aRes;
3948	}
3949
3950	/**
3951	 * @param string $sClass
3952	 *
3953	 * @return bool
3954	 */
3955	public static function HasChildrenClasses($sClass)
3956	{
3957		return (count(self::$m_aChildClasses[$sClass]) > 0);
3958	}
3959
3960	/**
3961	 * @return array
3962	 */
3963	public static function EnumCategories()
3964	{
3965		return array_keys(self::$m_Category2Class);
3966	}
3967
3968	// Note: use EnumChildClasses to take the compound objects into account
3969
3970	/**
3971	 * @param string $sClass
3972	 *
3973	 * @return array
3974	 * @throws \CoreException
3975	 */
3976	public static function GetSubclasses($sClass)
3977	{
3978		self::_check_subclass($sClass);
3979		$aSubClasses = array();
3980		foreach(self::$m_aClassParams as $sSubClass => $foo)
3981		{
3982			if (is_subclass_of($sSubClass, $sClass))
3983			{
3984				$aSubClasses[] = $sSubClass;
3985			}
3986		}
3987
3988		return $aSubClasses;
3989	}
3990
3991	/**
3992	 * @param string $sCategories
3993	 * @param bool $bStrict
3994	 *
3995	 * @return array
3996	 * @throws \CoreException
3997	 */
3998	public static function GetClasses($sCategories = '', $bStrict = false)
3999	{
4000		$aCategories = explode(',', $sCategories);
4001		$aClasses = array();
4002		foreach($aCategories as $sCategory)
4003		{
4004			$sCategory = trim($sCategory);
4005			if (strlen($sCategory) == 0)
4006			{
4007				return array_keys(self::$m_aClassParams);
4008			}
4009
4010			if (array_key_exists($sCategory, self::$m_Category2Class))
4011			{
4012				$aClasses = array_merge($aClasses, self::$m_Category2Class[$sCategory]);
4013			}
4014			elseif ($bStrict)
4015			{
4016				throw new CoreException("unkown class category '$sCategory', expecting a value in {".implode(', ', array_keys(self::$m_Category2Class))."}");
4017			}
4018		}
4019
4020		return array_unique($aClasses);
4021	}
4022
4023	/**
4024	 * @param string $sClass
4025	 *
4026	 * @return bool
4027	 * @throws \CoreException
4028	 */
4029	public static function HasTable($sClass)
4030	{
4031		if (strlen(self::DBGetTable($sClass)) == 0)
4032		{
4033			return false;
4034		}
4035		return true;
4036	}
4037
4038	/**
4039	 * @param string $sClass
4040	 *
4041	 * @return bool
4042	 */
4043	public static function IsAbstract($sClass)
4044	{
4045		$oReflection = new ReflectionClass($sClass);
4046		return $oReflection->isAbstract();
4047	}
4048
4049	/**
4050	 * Normalizes query arguments and adds magic parameters:
4051	 * - current_contact_id
4052	 * - current_contact (DBObject)
4053	 * - current_user (DBObject)
4054	 *
4055	 * @param array $aArgs Context arguments (some can be persistent objects)
4056	 * @param array $aMoreArgs Other query parameters
4057	 * @return array
4058	 */
4059	public static function PrepareQueryArguments($aArgs, $aMoreArgs = array())
4060	{
4061		$aScalarArgs = array();
4062		foreach(array_merge($aArgs, $aMoreArgs) as $sArgName => $value)
4063		{
4064			if (self::IsValidObject($value))
4065			{
4066				if (strpos($sArgName, '->object()') === false)
4067				{
4068					// Normalize object arguments
4069					$aScalarArgs[$sArgName.'->object()'] = $value;
4070				}
4071				else
4072				{
4073					// Leave as is
4074					$aScalarArgs[$sArgName] = $value;
4075				}
4076			}
4077			else
4078			{
4079				if (is_scalar($value))
4080				{
4081					$aScalarArgs[$sArgName] = (string)$value;
4082				}
4083				elseif (is_null($value))
4084				{
4085					$aScalarArgs[$sArgName] = null;
4086				}
4087				elseif (is_array($value))
4088				{
4089					$aScalarArgs[$sArgName] = $value;
4090				}
4091			}
4092		}
4093
4094		return static::AddMagicPlaceholders($aScalarArgs);
4095	}
4096
4097	/**
4098	 * @param array $aPlaceholders The array into which standard placeholders should be added
4099	 *
4100	 * @return array of placeholder (or name->object()) => value (or object)
4101	 */
4102	public static function AddMagicPlaceholders($aPlaceholders)
4103	{
4104		// Add standard magic arguments
4105		//
4106		$aPlaceholders['current_contact_id'] = UserRights::GetContactId(); // legacy
4107
4108		$oUser = UserRights::GetUserObject();
4109		if (!is_null($oUser))
4110		{
4111			$aPlaceholders['current_user->object()'] = $oUser;
4112
4113			$oContact = UserRights::GetContactObject();
4114			if (!is_null($oContact))
4115			{
4116				$aPlaceholders['current_contact->object()'] = $oContact;
4117			}
4118		}
4119
4120		return $aPlaceholders;
4121	}
4122
4123	/**
4124	 * @param \DBSearch $oFilter
4125	 *
4126	 * @return array
4127	 */
4128	public static function MakeModifierProperties($oFilter)
4129	{
4130		// Compute query modifiers properties (can be set in the search itself, by the context, etc.)
4131		//
4132		$aModifierProperties = array();
4133		foreach(MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier)
4134		{
4135			// Lowest precedence: the application context
4136			$aPluginProps = ApplicationContext::GetPluginProperties($sPluginClass);
4137			// Highest precedence: programmatically specified (or OQL)
4138			foreach($oFilter->GetModifierProperties($sPluginClass) as $sProp => $value)
4139			{
4140				$aPluginProps[$sProp] = $value;
4141			}
4142			if (count($aPluginProps) > 0)
4143			{
4144				$aModifierProperties[$sPluginClass] = $aPluginProps;
4145			}
4146		}
4147		return $aModifierProperties;
4148	}
4149
4150
4151	/**
4152	 * Special processing for the hierarchical keys stored as nested sets
4153	 *
4154	 * @param int $iId integer The identifier of the parent
4155	 * @param \AttributeDefinition $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key
4156	 * @param string The name of the database table containing the hierarchical key
4157	 *
4158	 * @throws \MySQLException
4159	 */
4160	public static function HKInsertChildUnder($iId, $oAttDef, $sTable)
4161	{
4162		// Get the parent id.right value
4163		if ($iId == 0)
4164		{
4165			// No parent, insert completely at the right of the tree
4166			$sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`";
4167			$aRes = CMDBSource::QueryToArray($sSQL);
4168			if (count($aRes) == 0)
4169			{
4170				$iMyRight = 1;
4171			}
4172			else
4173			{
4174				$iMyRight = $aRes[0]['max'] + 1;
4175			}
4176		}
4177		else
4178		{
4179			$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".$iId;
4180			$iMyRight = CMDBSource::QueryToScalar($sSQL);
4181			$sSQLUpdateRight = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` + 2 WHERE `".$oAttDef->GetSQLRight()."` >= $iMyRight";
4182			CMDBSource::Query($sSQLUpdateRight);
4183			$sSQLUpdateLeft = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` + 2 WHERE `".$oAttDef->GetSQLLeft()."` > $iMyRight";
4184			CMDBSource::Query($sSQLUpdateLeft);
4185		}
4186		return array($oAttDef->GetSQLRight() => $iMyRight + 1, $oAttDef->GetSQLLeft() => $iMyRight);
4187	}
4188
4189	/**
4190	 * Special processing for the hierarchical keys stored as nested sets: temporary remove the branch
4191	 *
4192	 * @param int $iMyLeft
4193	 * @param int $iMyRight
4194	 * @param \AttributeDefinition $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key
4195	 * @param string The name of the database table containing the hierarchical key
4196	 *
4197	 * @throws \MySQLException
4198	 * @throws \MySQLHasGoneAwayException
4199	 */
4200	public static function HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable)
4201	{
4202		$iDelta = $iMyRight - $iMyLeft + 1;
4203		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = $iMyLeft - `".$oAttDef->GetSQLRight()."`, `".$oAttDef->GetSQLLeft()."` = $iMyLeft - `".$oAttDef->GetSQLLeft();
4204		$sSQL .= "` WHERE  `".$oAttDef->GetSQLLeft()."`> $iMyLeft AND `".$oAttDef->GetSQLRight()."`< $iMyRight";
4205		CMDBSource::Query($sSQL);
4206		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` - $iDelta WHERE `".$oAttDef->GetSQLLeft()."` > $iMyRight";
4207		CMDBSource::Query($sSQL);
4208		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` - $iDelta WHERE `".$oAttDef->GetSQLRight()."` > $iMyRight";
4209		CMDBSource::Query($sSQL);
4210	}
4211
4212	/**
4213	 * Special processing for the hierarchical keys stored as nested sets: replug the temporary removed branch
4214	 *
4215	 * @param integer $iNewLeft
4216	 * @param integer $iNewRight
4217	 * @param \AttributeDefinition $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key
4218	 * @param string $sTable string The name of the database table containing the hierarchical key
4219	 *
4220	 * @throws \MySQLException
4221	 * @throws \MySQLHasGoneAwayException
4222	 */
4223	public static function HKReplugBranch($iNewLeft, $iNewRight, $oAttDef, $sTable)
4224	{
4225		$iDelta = $iNewRight - $iNewLeft + 1;
4226		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` + $iDelta WHERE `".$oAttDef->GetSQLLeft()."` > $iNewLeft";
4227		CMDBSource::Query($sSQL);
4228		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` + $iDelta WHERE `".$oAttDef->GetSQLRight()."` >= $iNewLeft";
4229		CMDBSource::Query($sSQL);
4230		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = $iNewLeft - `".$oAttDef->GetSQLRight()."`, `".$oAttDef->GetSQLLeft()."` = $iNewLeft - `".$oAttDef->GetSQLLeft()."` WHERE `".$oAttDef->GetSQLRight()."`< 0";
4231		CMDBSource::Query($sSQL);
4232	}
4233
4234	/**
4235	 * Check (and updates if needed) the hierarchical keys
4236	 *
4237	 * @param boolean $bDiagnosticsOnly If true only a diagnostic pass will be run, returning true or false
4238	 * @param boolean $bVerbose Displays some information about what is done/what needs to be done
4239	 * @param boolean $bForceComputation If true, the _left and _right parameters will be recomputed even if some
4240	 *     values already exist in the DB
4241	 *
4242	 * @throws \CoreException
4243	 * @throws \Exception
4244	 */
4245	public static function CheckHKeys($bDiagnosticsOnly = false, $bVerbose = false, $bForceComputation = false)
4246	{
4247		$bChangeNeeded = false;
4248		foreach(self::GetClasses() as $sClass)
4249		{
4250			if (!self::HasTable($sClass))
4251			{
4252				continue;
4253			}
4254
4255			foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
4256			{
4257				// Check (once) all the attributes that are hierarchical keys
4258				if ((self::GetAttributeOrigin($sClass, $sAttCode) == $sClass) && $oAttDef->IsHierarchicalKey())
4259				{
4260					if ($bVerbose)
4261					{
4262						echo "The attribute $sAttCode from $sClass is a hierarchical key.\n";
4263					}
4264					$bResult = self::HKInit($sClass, $sAttCode, $bDiagnosticsOnly, $bVerbose, $bForceComputation);
4265					$bChangeNeeded |= $bResult;
4266					if ($bVerbose && !$bResult)
4267					{
4268						echo "Ok, the attribute $sAttCode from class $sClass seems up to date.\n";
4269					}
4270				}
4271			}
4272		}
4273		return $bChangeNeeded;
4274	}
4275
4276	/**
4277	 * Initializes (i.e converts) a hierarchy stored using a 'parent_id' external key
4278	 * into a hierarchy stored with a HierarchicalKey, by initializing the _left and _right values
4279	 * to correspond to the existing hierarchy in the database
4280	 *
4281	 * @param string $sClass Name of the class to process
4282	 * @param string $sAttCode Code of the attribute to process
4283	 * @param boolean $bDiagnosticsOnly If true only a diagnostic pass will be run, returning true or false
4284	 * @param boolean $bVerbose Displays some information about what is done/what needs to be done
4285	 * @param boolean $bForceComputation If true, the _left and _right parameters will be recomputed even if some
4286	 *     values already exist in the DB
4287	 *
4288	 * @return boolean true if an update is needed (diagnostics only) / was performed
4289	 * @throws \Exception
4290	 * @throws \CoreException
4291	 */
4292	public static function HKInit($sClass, $sAttCode, $bDiagnosticsOnly = false, $bVerbose = false, $bForceComputation = false)
4293	{
4294		$idx = 1;
4295		$bUpdateNeeded = $bForceComputation;
4296		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
4297		$sTable = self::DBGetTable($sClass, $sAttCode);
4298		if ($oAttDef->IsHierarchicalKey())
4299		{
4300			// Check if some values already exist in the table for the _right value, if so, do nothing
4301			$sRight = $oAttDef->GetSQLRight();
4302			$sSQL = "SELECT MAX(`$sRight`) AS MaxRight FROM `$sTable`";
4303			$iMaxRight = CMDBSource::QueryToScalar($sSQL);
4304			$sSQL = "SELECT COUNT(*) AS Count FROM `$sTable`"; // Note: COUNT(field) returns zero if the given field contains only NULLs
4305			$iCount = CMDBSource::QueryToScalar($sSQL);
4306			if (!$bForceComputation && ($iCount != 0) && ($iMaxRight == 0))
4307			{
4308				$bUpdateNeeded = true;
4309				if ($bVerbose)
4310				{
4311					echo "The table '$sTable' must be updated to compute the fields $sRight and ".$oAttDef->GetSQLLeft()."\n";
4312				}
4313			}
4314			if ($bForceComputation && !$bDiagnosticsOnly)
4315			{
4316				echo "Rebuilding the fields $sRight and ".$oAttDef->GetSQLLeft()." from table '$sTable'...\n";
4317			}
4318			if ($bUpdateNeeded && !$bDiagnosticsOnly)
4319			{
4320				try
4321				{
4322					CMDBSource::Query('START TRANSACTION');
4323					self::HKInitChildren($sTable, $sAttCode, $oAttDef, 0, $idx);
4324					CMDBSource::Query('COMMIT');
4325					if ($bVerbose)
4326					{
4327						echo "Ok, table '$sTable' successfully updated.\n";
4328					}
4329				}
4330				catch (Exception $e)
4331				{
4332					CMDBSource::Query('ROLLBACK');
4333					throw new Exception("An error occured (".$e->getMessage().") while initializing the hierarchy for ($sClass, $sAttCode). The database was not modified.");
4334				}
4335			}
4336		}
4337		return $bUpdateNeeded;
4338	}
4339
4340	/**
4341	 * Recursive helper function called by HKInit
4342	 *
4343	 * @param string $sTable
4344	 * @param string $sAttCode
4345	 * @param \AttributeDefinition $oAttDef
4346	 * @param int $iId
4347	 * @param int $iCurrIndex
4348	 *
4349	 * @throws \MySQLException
4350	 * @throws \MySQLHasGoneAwayException
4351	 */
4352	protected static function HKInitChildren($sTable, $sAttCode, $oAttDef, $iId, &$iCurrIndex)
4353	{
4354		$sSQL = "SELECT id FROM `$sTable` WHERE `$sAttCode` = $iId";
4355		$aRes = CMDBSource::QueryToArray($sSQL);
4356		$sLeft = $oAttDef->GetSQLLeft();
4357		$sRight = $oAttDef->GetSQLRight();
4358		foreach($aRes as $aValues)
4359		{
4360			$iChildId = $aValues['id'];
4361			$iLeft = $iCurrIndex++;
4362			//FIXME calling ourselves but no return statement in this method ?!!???
4363			$aChildren = self::HKInitChildren($sTable, $sAttCode, $oAttDef, $iChildId, $iCurrIndex);
4364			$iRight = $iCurrIndex++;
4365			$sSQL = "UPDATE `$sTable` SET `$sLeft` = $iLeft, `$sRight` = $iRight WHERE id= $iChildId";
4366			CMDBSource::Query($sSQL);
4367		}
4368	}
4369
4370	/**
4371	 * Update the meta enums
4372	 *
4373	 * @param boolean $bVerbose Displays some information about what is done/what needs to be done
4374	 *
4375	 * @throws \CoreException
4376	 * @throws \Exception
4377	 *
4378	 * @see AttributeMetaEnum::MapValue that must be aligned with the above implementation
4379	 */
4380	public static function RebuildMetaEnums($bVerbose = false)
4381	{
4382		foreach(self::GetClasses() as $sClass)
4383		{
4384			if (!self::HasTable($sClass))
4385			{
4386				continue;
4387			}
4388
4389			foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
4390			{
4391				// Check (once) all the attributes that are hierarchical keys
4392				if ((self::GetAttributeOrigin($sClass, $sAttCode) == $sClass) && $oAttDef instanceof AttributeEnum)
4393				{
4394					if (isset(self::$m_aEnumToMeta[$sClass][$sAttCode]))
4395					{
4396						foreach(self::$m_aEnumToMeta[$sClass][$sAttCode] as $sMetaAttCode => $oMetaAttDef)
4397						{
4398							$aMetaValues = array(); // array of (metavalue => array of values)
4399							foreach($oAttDef->GetAllowedValues() as $sCode => $sLabel)
4400							{
4401								$aMappingData = $oMetaAttDef->GetMapRule($sClass);
4402								if ($aMappingData == null)
4403								{
4404									$sMetaValue = $oMetaAttDef->GetDefaultValue();
4405								}
4406								else
4407								{
4408									if (array_key_exists($sCode, $aMappingData['values']))
4409									{
4410										$sMetaValue = $aMappingData['values'][$sCode];
4411									}
4412									elseif ($oMetaAttDef->GetDefaultValue() != '')
4413									{
4414										$sMetaValue = $oMetaAttDef->GetDefaultValue();
4415									}
4416									else
4417									{
4418										throw new Exception('MetaModel::RebuildMetaEnums(): mapping not found for value "'.$sCode.'"" in '.$sClass.', on attribute '.self::GetAttributeOrigin($sClass, $oMetaAttDef->GetCode()).'::'.$oMetaAttDef->GetCode());
4419									}
4420								}
4421								$aMetaValues[$sMetaValue][] = $sCode;
4422							}
4423							foreach($aMetaValues as $sMetaValue => $aEnumValues)
4424							{
4425								$sMetaTable = self::DBGetTable($sClass, $sMetaAttCode);
4426								$sEnumTable = self::DBGetTable($sClass);
4427								$aColumns = array_keys($oMetaAttDef->GetSQLColumns());
4428								$sMetaColumn = reset($aColumns);
4429								$aColumns = array_keys($oAttDef->GetSQLColumns());
4430								$sEnumColumn = reset($aColumns);
4431								$sValueList = implode(', ', CMDBSource::Quote($aEnumValues));
4432								$sSql = "UPDATE `$sMetaTable` JOIN `$sEnumTable` ON `$sEnumTable`.id = `$sMetaTable`.id SET `$sMetaTable`.`$sMetaColumn` = '$sMetaValue' WHERE `$sEnumTable`.`$sEnumColumn` IN ($sValueList) AND `$sMetaTable`.`$sMetaColumn` != '$sMetaValue'";
4433								if ($bVerbose)
4434								{
4435									echo "Executing query: $sSql\n";
4436								}
4437								CMDBSource::Query($sSql);
4438							}
4439						}
4440					}
4441				}
4442			}
4443		}
4444	}
4445
4446
4447	/**
4448	 * @param boolean $bDiagnostics
4449	 * @param boolean $bVerbose
4450	 *
4451	 * @return bool
4452	 * @throws \OQLException
4453	 */
4454	public static function CheckDataSources($bDiagnostics, $bVerbose)
4455	{
4456		$sOQL = 'SELECT SynchroDataSource';
4457		$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL));
4458		$bFixNeeded = false;
4459		if ($bVerbose && $oSet->Count() == 0)
4460		{
4461			echo "There are no Data Sources in the database.\n";
4462		}
4463		while ($oSource = $oSet->Fetch())
4464		{
4465			if ($bVerbose)
4466			{
4467				echo "Checking Data Source '".$oSource->GetName()."'...\n";
4468				$bFixNeeded = $bFixNeeded | $oSource->CheckDBConsistency($bDiagnostics, $bVerbose);
4469			}
4470		}
4471		if (!$bFixNeeded && $bVerbose)
4472		{
4473			echo "Ok.\n";
4474		}
4475
4476		return $bFixNeeded;
4477	}
4478
4479	/**
4480	 * @param array $aAliases
4481	 * @param string $sNewName
4482	 * @param string $sRealName
4483	 *
4484	 * @return string
4485	 * @throws \CoreException
4486	 */
4487	public static function GenerateUniqueAlias(&$aAliases, $sNewName, $sRealName)
4488	{
4489		if (!array_key_exists($sNewName, $aAliases))
4490		{
4491			$aAliases[$sNewName] = $sRealName;
4492			return $sNewName;
4493		}
4494
4495		for($i = 1; $i < 100; $i++)
4496		{
4497			$sAnAlias = $sNewName.$i;
4498			if (!array_key_exists($sAnAlias, $aAliases))
4499			{
4500				// Create that new alias
4501				$aAliases[$sAnAlias] = $sRealName;
4502				return $sAnAlias;
4503			}
4504		}
4505		throw new CoreException('Failed to create an alias', array('aliases' => $aAliases, 'new' => $sNewName));
4506	}
4507
4508	/**
4509	 * @param bool $bExitOnError
4510	 *
4511	 * @throws \CoreException
4512	 * @throws \DictExceptionMissingString
4513	 * @throws \Exception
4514	 */
4515	public static function CheckDefinitions($bExitOnError = true)
4516	{
4517		if (count(self::GetClasses()) == 0)
4518		{
4519			throw new CoreException("MetaModel::InitClasses() has not been called, or no class has been declared ?!?!");
4520		}
4521
4522		$aErrors = array();
4523		$aSugFix = array();
4524		foreach(self::GetClasses() as $sClass)
4525		{
4526			$sTable = self::DBGetTable($sClass);
4527			$sTableLowercase = strtolower($sTable);
4528			if ($sTableLowercase != $sTable)
4529			{
4530				$aErrors[$sClass][] = "Table name '".$sTable."' has upper case characters. You might encounter issues when moving your installation between Linux and Windows.";
4531				$aSugFix[$sClass][] = "Use '$sTableLowercase' instead. Step 1: If already installed, then rename manually in the DB: RENAME TABLE `$sTable` TO `{$sTableLowercase}_tempname`, `{$sTableLowercase}_tempname` TO `$sTableLowercase`; Step 2: Rename the table in the datamodel and compile the application. Note: the MySQL statement provided in step 1 has been designed to be compatible with Windows or Linux.";
4532			}
4533
4534			$aNameSpec = self::GetNameSpec($sClass);
4535			foreach($aNameSpec[1] as $i => $sAttCode)
4536			{
4537				if (!self::IsValidAttCode($sClass, $sAttCode))
4538				{
4539					$aErrors[$sClass][] = "Unknown attribute code '".$sAttCode."' for the name definition";
4540					$aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass));
4541				}
4542			}
4543
4544			foreach(self::GetReconcKeys($sClass) as $sReconcKeyAttCode)
4545			{
4546				if (!empty($sReconcKeyAttCode) && !self::IsValidAttCode($sClass, $sReconcKeyAttCode))
4547				{
4548					$aErrors[$sClass][] = "Unknown attribute code '".$sReconcKeyAttCode."' in the list of reconciliation keys";
4549					$aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass));
4550				}
4551			}
4552
4553			$bHasWritableAttribute = false;
4554			foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
4555			{
4556				// It makes no sense to check the attributes again and again in the subclasses
4557				if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass)
4558				{
4559					continue;
4560				}
4561
4562				if ($oAttDef->IsExternalKey())
4563				{
4564					if (!self::IsValidClass($oAttDef->GetTargetClass()))
4565					{
4566						$aErrors[$sClass][] = "Unknown class '".$oAttDef->GetTargetClass()."' for the external key '$sAttCode'";
4567						$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetClasses())."}";
4568					}
4569				}
4570				elseif ($oAttDef->IsExternalField())
4571				{
4572					$sKeyAttCode = $oAttDef->GetKeyAttCode();
4573					if (!self::IsValidAttCode($sClass, $sKeyAttCode) || !self::IsValidKeyAttCode($sClass, $sKeyAttCode))
4574					{
4575						$aErrors[$sClass][] = "Unknown key attribute code '".$sKeyAttCode."' for the external field $sAttCode";
4576						$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetKeysList($sClass))."}";
4577					}
4578					else
4579					{
4580						$oKeyAttDef = self::GetAttributeDef($sClass, $sKeyAttCode);
4581						$sTargetClass = $oKeyAttDef->GetTargetClass();
4582						$sExtAttCode = $oAttDef->GetExtAttCode();
4583						if (!self::IsValidAttCode($sTargetClass, $sExtAttCode))
4584						{
4585							$aErrors[$sClass][] = "Unknown key attribute code '".$sExtAttCode."' for the external field $sAttCode";
4586							$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetKeysList($sTargetClass))."}";
4587						}
4588					}
4589				}
4590				else
4591				{
4592					if ($oAttDef->IsLinkSet())
4593					{
4594						// Do nothing...
4595					}
4596					else
4597					{
4598						if ($oAttDef instanceof AttributeStopWatch)
4599						{
4600							$aThresholds = $oAttDef->ListThresholds();
4601							if (is_array($aThresholds))
4602							{
4603								foreach($aThresholds as $iPercent => $aDef)
4604								{
4605									if (array_key_exists('highlight', $aDef))
4606									{
4607										if (!array_key_exists('code', $aDef['highlight']))
4608										{
4609											$aErrors[$sClass][] = "The 'code' element is missing for the 'highlight' property of the $iPercent% threshold in the attribute: '$sAttCode'.";
4610											$aSugFix[$sClass][] = "Add a 'code' entry specifying the value of the highlight code for this threshold.";
4611										}
4612										else
4613										{
4614											$aScale = self::GetHighlightScale($sClass);
4615											if (!array_key_exists($aDef['highlight']['code'], $aScale))
4616											{
4617												$aErrors[$sClass][] = "'{$aDef['highlight']['code']}' is not a valid value for the 'code' element of the $iPercent% threshold in the attribute: '$sAttCode'.";
4618												$aSugFix[$sClass][] = "The possible highlight codes for this class are: ".implode(', ', array_keys($aScale)).".";
4619											}
4620										}
4621									}
4622								}
4623							}
4624						}
4625						else // standard attributes
4626						{
4627							// Check that the default values definition is a valid object!
4628							$oValSetDef = $oAttDef->GetValuesDef();
4629							if (!is_null($oValSetDef) && !$oValSetDef instanceof ValueSetDefinition)
4630							{
4631								$aErrors[$sClass][] = "Allowed values for attribute $sAttCode is not of the relevant type";
4632								$aSugFix[$sClass][] = "Please set it as an instance of a ValueSetDefinition object.";
4633							}
4634							else
4635							{
4636								// Default value must be listed in the allowed values (if defined)
4637								$aAllowedValues = self::GetAllowedValues_att($sClass, $sAttCode);
4638								if (!is_null($aAllowedValues))
4639								{
4640									$sDefaultValue = $oAttDef->GetDefaultValue();
4641									if (is_string($sDefaultValue) && !array_key_exists($sDefaultValue, $aAllowedValues))
4642									{
4643										$aErrors[$sClass][] = "Default value '".$sDefaultValue."' for attribute $sAttCode is not an allowed value";
4644										$aSugFix[$sClass][] = "Please pickup the default value out of {'".implode(", ", array_keys($aAllowedValues))."'}";
4645									}
4646								}
4647							}
4648						}
4649					}
4650				}
4651				// Check dependencies
4652				if ($oAttDef->IsWritable())
4653				{
4654					$bHasWritableAttribute = true;
4655					foreach($oAttDef->GetPrerequisiteAttributes() as $sDependOnAttCode)
4656					{
4657						if (!self::IsValidAttCode($sClass, $sDependOnAttCode))
4658						{
4659							$aErrors[$sClass][] = "Unknown attribute code '".$sDependOnAttCode."' in the list of prerequisite attributes";
4660							$aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass));
4661						}
4662					}
4663				}
4664			}
4665			foreach(self::GetClassFilterDefs($sClass) as $sFltCode => $oFilterDef)
4666			{
4667				if (method_exists($oFilterDef, '__GetRefAttribute'))
4668				{
4669					$oAttDef = $oFilterDef->__GetRefAttribute();
4670					if (!self::IsValidAttCode($sClass, $oAttDef->GetCode()))
4671					{
4672						$aErrors[$sClass][] = "Wrong attribute code '".$oAttDef->GetCode()."' (wrong class) for the \"basic\" filter $sFltCode";
4673						$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}";
4674					}
4675				}
4676			}
4677
4678			// Lifecycle
4679			//
4680			$sStateAttCode = self::GetStateAttributeCode($sClass);
4681			if (strlen($sStateAttCode) > 0)
4682			{
4683				// Lifecycle - check that the state attribute does exist as an attribute
4684				if (!self::IsValidAttCode($sClass, $sStateAttCode))
4685				{
4686					$aErrors[$sClass][] = "Unknown attribute code '".$sStateAttCode."' for the state definition";
4687					$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}";
4688				}
4689				else
4690				{
4691					// Lifecycle - check that there is a value set constraint on the state attribute
4692					$aAllowedValuesRaw = self::GetAllowedValues_att($sClass, $sStateAttCode);
4693					$aStates = array_keys(self::EnumStates($sClass));
4694					if (is_null($aAllowedValuesRaw))
4695					{
4696						$aErrors[$sClass][] = "Attribute '".$sStateAttCode."' will reflect the state of the object. It must be restricted to a set of values";
4697						$aSugFix[$sClass][] = "Please define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')]";
4698					}
4699					else
4700					{
4701						$aAllowedValues = array_keys($aAllowedValuesRaw);
4702
4703						// Lifecycle - check the the state attribute allowed values are defined states
4704						foreach($aAllowedValues as $sValue)
4705						{
4706							if (!in_array($sValue, $aStates))
4707							{
4708								$aErrors[$sClass][] = "Attribute '".$sStateAttCode."' (object state) has an allowed value ($sValue) which is not a known state";
4709								$aSugFix[$sClass][] = "You may define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')], or reconsider the list of states";
4710							}
4711						}
4712
4713						// Lifecycle - check that defined states are allowed values
4714						foreach($aStates as $sStateValue)
4715						{
4716							if (!in_array($sStateValue, $aAllowedValues))
4717							{
4718								$aErrors[$sClass][] = "Attribute '".$sStateAttCode."' (object state) has a state ($sStateValue) which is not an allowed value";
4719								$aSugFix[$sClass][] = "You may define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')], or reconsider the list of states";
4720							}
4721						}
4722					}
4723
4724					// Lifecycle - check that the action handlers are defined
4725					foreach(self::EnumStates($sClass) as $sStateCode => $aStateDef)
4726					{
4727						foreach(self::EnumTransitions($sClass, $sStateCode) as $sStimulusCode => $aTransitionDef)
4728						{
4729							foreach($aTransitionDef['actions'] as $actionHandler)
4730							{
4731								if (is_string($actionHandler))
4732								{
4733									if (!method_exists($sClass, $actionHandler))
4734									{
4735										$aErrors[$sClass][] = "Unknown function '$actionHandler' in transition [$sStateCode/$sStimulusCode] for state attribute '$sStateAttCode'";
4736										$aSugFix[$sClass][] = "Specify a function which prototype is in the form [public function $actionHandler(\$sStimulusCode){return true;}]";
4737									}
4738								}
4739								else // if(is_array($actionHandler))
4740								{
4741									$sActionHandler = $actionHandler['verb'];
4742									if (!method_exists($sClass, $sActionHandler))
4743									{
4744										$aErrors[$sClass][] = "Unknown function '$sActionHandler' in transition [$sStateCode/$sStimulusCode] for state attribute '$sStateAttCode'";
4745										$aSugFix[$sClass][] = "Specify a function which prototype is in the form [public function $sActionHandler(...){return true;}]";
4746									}
4747								}
4748							}
4749						}
4750						if (array_key_exists('highlight', $aStateDef))
4751						{
4752							if (!array_key_exists('code', $aStateDef['highlight']))
4753							{
4754								$aErrors[$sClass][] = "The 'code' element is missing for the 'highlight' property of state: '$sStateCode'.";
4755								$aSugFix[$sClass][] = "Add a 'code' entry specifying the value of the highlight code for this state.";
4756							}
4757							else
4758							{
4759								$aScale = self::GetHighlightScale($sClass);
4760								if (!array_key_exists($aStateDef['highlight']['code'], $aScale))
4761								{
4762									$aErrors[$sClass][] = "'{$aStateDef['highlight']['code']}' is not a valid value for the 'code' element in the 'highlight' property of state: '$sStateCode'.";
4763									$aSugFix[$sClass][] = "The possible highlight codes for this class are: ".implode(', ', array_keys($aScale)).".";
4764								}
4765							}
4766						}
4767					}
4768				}
4769			}
4770
4771			if ($bHasWritableAttribute)
4772			{
4773				if (!self::HasTable($sClass))
4774				{
4775					$aErrors[$sClass][] = "No table has been defined for this class";
4776					$aSugFix[$sClass][] = "Either define a table name or move the attributes elsewhere";
4777				}
4778			}
4779
4780
4781			// ZList
4782			//
4783			foreach(self::EnumZLists() as $sListCode)
4784			{
4785				foreach(self::FlattenZList(self::GetZListItems($sClass, $sListCode)) as $sMyAttCode)
4786				{
4787					if (!self::IsValidAttCode($sClass, $sMyAttCode))
4788					{
4789						$aErrors[$sClass][] = "Unknown attribute code '".$sMyAttCode."' from ZList '$sListCode'";
4790						$aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}";
4791					}
4792				}
4793			}
4794
4795			// Check SQL columns uniqueness
4796			//
4797			if (self::HasTable($sClass))
4798			{
4799				$aTableColumns = array(); // array of column => attcode (the column is used by this attribute)
4800				$aTableColumns[self::DBGetKey($sClass)] = 'id';
4801
4802				// Check that SQL columns are declared only once
4803				//
4804				foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
4805				{
4806					// Skip this attribute if not originaly defined in this class
4807					if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass)
4808					{
4809						continue;
4810					}
4811
4812					foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType)
4813					{
4814						if (array_key_exists($sField, $aTableColumns))
4815						{
4816							$aErrors[$sClass][] = "Column '$sField' declared for attribute $sAttCode, but already used for attribute ".$aTableColumns[$sField];
4817							$aSugFix[$sClass][] = "Please find another name for the SQL column";
4818						}
4819						else
4820						{
4821							$aTableColumns[$sField] = $sAttCode;
4822						}
4823					}
4824				}
4825			}
4826		} // foreach class
4827
4828		if (count($aErrors) > 0)
4829		{
4830			echo "<div style=\"width:100%;padding:10px;background:#FFAAAA;display:;\">";
4831			echo "<h3>Business model inconsistencies have been found</h3>\n";
4832			// #@# later -> this is the responsibility of the caller to format the output
4833			foreach($aErrors as $sClass => $aMessages)
4834			{
4835				echo "<p>Wrong declaration for class <b>$sClass</b></p>\n";
4836				echo "<ul class=\"treeview\">\n";
4837				$i = 0;
4838				foreach($aMessages as $sMsg)
4839				{
4840					echo "<li>$sMsg ({$aSugFix[$sClass][$i]})</li>\n";
4841					$i++;
4842				}
4843				echo "</ul>\n";
4844			}
4845			if ($bExitOnError)
4846			{
4847				echo "<p>Aborting...</p>\n";
4848			}
4849			echo "</div>\n";
4850			if ($bExitOnError)
4851			{
4852				exit;
4853			}
4854		}
4855	}
4856
4857	/**
4858	 * @param string $sRepairUrl
4859	 * @param string $sSQLStatementArgName
4860	 * @param string[] $aSQLFixes
4861	 */
4862	public static function DBShowApplyForm($sRepairUrl, $sSQLStatementArgName, $aSQLFixes)
4863	{
4864		if (empty($sRepairUrl))
4865		{
4866			return;
4867		}
4868
4869		// By design, some queries might be blank, we have to ignore them
4870		$aCleanFixes = array();
4871		foreach($aSQLFixes as $sSQLFix)
4872		{
4873			if (!empty($sSQLFix))
4874			{
4875				$aCleanFixes[] = $sSQLFix;
4876			}
4877		}
4878		if (count($aCleanFixes) == 0)
4879		{
4880			return;
4881		}
4882
4883		echo "<form action=\"$sRepairUrl\" method=\"POST\">\n";
4884		echo "   <input type=\"hidden\" name=\"$sSQLStatementArgName\" value=\"".htmlentities(implode("##SEP##", $aCleanFixes), ENT_QUOTES, 'UTF-8')."\">\n";
4885		echo "   <input type=\"submit\" value=\" Apply changes (".count($aCleanFixes)." queries) \">\n";
4886		echo "</form>\n";
4887	}
4888
4889	/**
4890	 * @param bool $bMustBeComplete
4891	 *
4892	 * @return bool returns true if at least one table exists
4893	 * @throws \CoreException
4894	 * @throws \MySQLException
4895	 */
4896	public static function DBExists($bMustBeComplete = true)
4897	{
4898		if (!CMDBSource::IsDB(self::$m_sDBName))
4899		{
4900			return false;
4901		}
4902		CMDBSource::SelectDB(self::$m_sDBName);
4903
4904		$aFound = array();
4905		$aMissing = array();
4906		foreach(self::DBEnumTables() as $sTable => $aClasses)
4907		{
4908			if (CMDBSource::IsTable($sTable))
4909			{
4910				$aFound[] = $sTable;
4911			}
4912			else
4913			{
4914				$aMissing[] = $sTable;
4915			}
4916		}
4917
4918		if (count($aFound) == 0)
4919		{
4920			// no expected table has been found
4921			return false;
4922		}
4923		else
4924		{
4925			if (count($aMissing) == 0)
4926			{
4927				// the database is complete (still, could be some fields missing!)
4928				return true;
4929			}
4930			else
4931			{
4932				// not all the tables, could be an older version
4933				return !$bMustBeComplete;
4934			}
4935		}
4936	}
4937
4938	/**
4939	 * Do drop only tables corresponding to the sub-database (table prefix)
4940	 * then possibly drop the DB itself (if no table remain)
4941	 */
4942	public static function DBDrop()
4943	{
4944		$bDropEntireDB = true;
4945
4946		if (!empty(self::$m_sTablePrefix))
4947		{
4948			foreach(CMDBSource::EnumTables() as $sTable)
4949			{
4950				// perform a case insensitive test because on Windows the table names become lowercase :-(
4951				if (strtolower(substr($sTable, 0, strlen(self::$m_sTablePrefix))) == strtolower(self::$m_sTablePrefix))
4952				{
4953					CMDBSource::DropTable($sTable);
4954				}
4955				else
4956				{
4957					// There is at least one table which is out of the scope of the current application
4958					$bDropEntireDB = false;
4959				}
4960			}
4961		}
4962
4963		if ($bDropEntireDB)
4964		{
4965			CMDBSource::DropDB(self::$m_sDBName);
4966		}
4967	}
4968
4969
4970	/**
4971	 * @param callable $aCallback
4972	 *
4973	 * @throws \MySQLException
4974	 * @throws \MySQLHasGoneAwayException
4975	 * @throws \CoreException
4976	 * @throws \Exception
4977	 */
4978	public static function DBCreate($aCallback = null)
4979	{
4980		// Note: we have to check if the DB does exist, because we may share the DB
4981		//       with other applications (in which case the DB does exist, not the tables with the given prefix)
4982		if (!CMDBSource::IsDB(self::$m_sDBName))
4983		{
4984			CMDBSource::CreateDB(self::$m_sDBName);
4985		}
4986		self::DBCreateTables($aCallback);
4987		self::DBCreateViews();
4988	}
4989
4990	/**
4991	 * @param callable $aCallback
4992	 *
4993	 * @throws \CoreException
4994	 */
4995	protected static function DBCreateTables($aCallback = null)
4996	{
4997		list($aErrors, $aSugFix, $aCondensedQueries) = self::DBCheckFormat();
4998
4999		//$sSQL = implode('; ', $aCondensedQueries); Does not work - multiple queries not allowed
5000		foreach($aCondensedQueries as $sQuery)
5001		{
5002			$fStart = microtime(true);
5003			CMDBSource::CreateTable($sQuery);
5004			$fDuration = microtime(true) - $fStart;
5005			if ($aCallback != null)
5006			{
5007				call_user_func($aCallback, $sQuery, $fDuration);
5008			}
5009		}
5010	}
5011
5012	/**
5013	 * @throws \CoreException
5014	 * @throws \Exception
5015	 * @throws \MissingQueryArgument
5016	 */
5017	protected static function DBCreateViews()
5018	{
5019		list($aErrors, $aSugFix) = self::DBCheckViews();
5020
5021		foreach($aSugFix as $sClass => $aTarget)
5022		{
5023			foreach($aTarget as $aQueries)
5024			{
5025				foreach($aQueries as $sQuery)
5026				{
5027					if (!empty($sQuery))
5028					{
5029						// forces a refresh of cached information
5030						CMDBSource::CreateTable($sQuery);
5031					}
5032				}
5033			}
5034		}
5035	}
5036
5037	/**
5038	 * @return array
5039	 * @throws \CoreException
5040	 * @throws \MySQLException
5041	 */
5042	public static function DBDump()
5043	{
5044		$aDataDump = array();
5045		foreach(self::DBEnumTables() as $sTable => $aClasses)
5046		{
5047			$aRows = CMDBSource::DumpTable($sTable);
5048			$aDataDump[$sTable] = $aRows;
5049		}
5050		return $aDataDump;
5051	}
5052
5053	/**
5054	 * Determines wether the target DB is frozen or not
5055	 *
5056	 * @return bool
5057	 */
5058	public static function DBIsReadOnly()
5059	{
5060		// Improvement: check the mySQL variable -> Read-only
5061
5062		if (utils::IsArchiveMode())
5063		{
5064			return true;
5065		}
5066		if (UserRights::IsAdministrator())
5067		{
5068			return (!self::DBHasAccess(ACCESS_ADMIN_WRITE));
5069		}
5070		else
5071		{
5072			return (!self::DBHasAccess(ACCESS_USER_WRITE));
5073		}
5074	}
5075
5076	/**
5077	 * @param int $iRequested
5078	 *
5079	 * @return bool
5080	 */
5081	public static function DBHasAccess($iRequested = ACCESS_FULL)
5082	{
5083		$iMode = self::$m_oConfig->Get('access_mode');
5084		if (($iMode & $iRequested) == 0)
5085		{
5086			return false;
5087		}
5088
5089		return true;
5090	}
5091
5092	/**
5093	 * @param string $sKey
5094	 * @param string $sValueFromOldSystem
5095	 * @param string $sDefaultValue
5096	 * @param boolean $bNotInDico
5097	 *
5098	 * @return string
5099	 * @throws \DictExceptionMissingString
5100	 */
5101	protected static function MakeDictEntry($sKey, $sValueFromOldSystem, $sDefaultValue, &$bNotInDico)
5102	{
5103		$sValue = Dict::S($sKey, 'x-no-nothing');
5104		if ($sValue == 'x-no-nothing')
5105		{
5106			$bNotInDico = true;
5107			$sValue = $sValueFromOldSystem;
5108			if (strlen($sValue) == 0)
5109			{
5110				$sValue = $sDefaultValue;
5111			}
5112		}
5113		return "	'$sKey' => '".str_replace("'", "\\'", $sValue)."',\n";
5114	}
5115
5116	/**
5117	 * @param string $sModules
5118	 * @param string $sOutputFilter
5119	 *
5120	 * @return string
5121	 * @throws \CoreException
5122	 * @throws \DictExceptionMissingString
5123	 * @throws \Exception
5124	 */
5125	public static function MakeDictionaryTemplate($sModules = '', $sOutputFilter = 'NotInDictionary')
5126	{
5127		$sRes = '';
5128
5129		$sRes .= "// Dictionnay conventions\n";
5130		$sRes .= htmlentities("// Class:<class_name>\n", ENT_QUOTES, 'UTF-8');
5131		$sRes .= htmlentities("// Class:<class_name>+\n", ENT_QUOTES, 'UTF-8');
5132		$sRes .= htmlentities("// Class:<class_name>/Attribute:<attribute_code>\n", ENT_QUOTES, 'UTF-8');
5133		$sRes .= htmlentities("// Class:<class_name>/Attribute:<attribute_code>+\n", ENT_QUOTES, 'UTF-8');
5134		$sRes .= htmlentities("// Class:<class_name>/Attribute:<attribute_code>/Value:<value>\n", ENT_QUOTES, 'UTF-8');
5135		$sRes .= htmlentities("// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+\n", ENT_QUOTES, 'UTF-8');
5136		$sRes .= htmlentities("// Class:<class_name>/Stimulus:<stimulus_code>\n", ENT_QUOTES, 'UTF-8');
5137		$sRes .= htmlentities("// Class:<class_name>/Stimulus:<stimulus_code>+\n", ENT_QUOTES, 'UTF-8');
5138		$sRes .= "\n";
5139
5140		// Note: I did not use EnumCategories(), because a given class maybe found in several categories
5141		// Need to invent the "module", to characterize the origins of a class
5142		if (strlen($sModules) == 0)
5143		{
5144			$aModules = array('bizmodel', 'core/cmdb', 'gui', 'application', 'addon/userrights');
5145		}
5146		else
5147		{
5148			$aModules = explode(', ', $sModules);
5149		}
5150
5151		$sRes .= "//////////////////////////////////////////////////////////////////////\n";
5152		$sRes .= "// Note: The classes have been grouped by categories: ".implode(', ', $aModules)."\n";
5153		$sRes .= "//////////////////////////////////////////////////////////////////////\n";
5154
5155		foreach($aModules as $sCategory)
5156		{
5157			$sRes .= "//////////////////////////////////////////////////////////////////////\n";
5158			$sRes .= "// Classes in '<em>$sCategory</em>'\n";
5159			$sRes .= "//////////////////////////////////////////////////////////////////////\n";
5160			$sRes .= "//\n";
5161			$sRes .= "\n";
5162			foreach(self::GetClasses($sCategory) as $sClass)
5163			{
5164				if (!self::HasTable($sClass))
5165				{
5166					continue;
5167				}
5168
5169				$bNotInDico = false;
5170
5171				$sClassRes = "//\n";
5172				$sClassRes .= "// Class: $sClass\n";
5173				$sClassRes .= "//\n";
5174				$sClassRes .= "\n";
5175				$sClassRes .= "Dict::Add('EN US', 'English', 'English', array(\n";
5176				$sClassRes .= self::MakeDictEntry("Class:$sClass", self::GetName_Obsolete($sClass), $sClass, $bNotInDico);
5177				$sClassRes .= self::MakeDictEntry("Class:$sClass+", self::GetClassDescription_Obsolete($sClass), '', $bNotInDico);
5178				foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
5179				{
5180					// Skip this attribute if not originaly defined in this class
5181					if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass)
5182					{
5183						continue;
5184					}
5185
5186					$sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode", $oAttDef->GetLabel_Obsolete(), $sAttCode, $bNotInDico);
5187					$sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode+", $oAttDef->GetDescription_Obsolete(), '', $bNotInDico);
5188					if ($oAttDef instanceof AttributeEnum)
5189					{
5190						if (self::GetStateAttributeCode($sClass) == $sAttCode)
5191						{
5192							foreach(self::EnumStates($sClass) as $sStateCode => $aStateData)
5193							{
5194								if (array_key_exists('label', $aStateData))
5195								{
5196									$sValue = $aStateData['label'];
5197								}
5198								else
5199								{
5200									$sValue = MetaModel::GetStateLabel($sClass, $sStateCode);
5201								}
5202								if (array_key_exists('description', $aStateData))
5203								{
5204									$sValuePlus = $aStateData['description'];
5205								}
5206								else
5207								{
5208									$sValuePlus = MetaModel::GetStateDescription($sClass, $sStateCode);
5209								}
5210								$sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sStateCode", $sValue, '', $bNotInDico);
5211								$sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sStateCode+", $sValuePlus, '', $bNotInDico);
5212							}
5213						}
5214						else
5215						{
5216							foreach($oAttDef->GetAllowedValues() as $sKey => $value)
5217							{
5218								$sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sKey", $value, '', $bNotInDico);
5219								$sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sKey+", $value, '', $bNotInDico);
5220							}
5221						}
5222					}
5223				}
5224				foreach(self::EnumStimuli($sClass) as $sStimulusCode => $oStimulus)
5225				{
5226					$sClassRes .= self::MakeDictEntry("Class:$sClass/Stimulus:$sStimulusCode", $oStimulus->GetLabel_Obsolete(), '', $bNotInDico);
5227					$sClassRes .= self::MakeDictEntry("Class:$sClass/Stimulus:$sStimulusCode+", $oStimulus->GetDescription_Obsolete(), '', $bNotInDico);
5228				}
5229
5230				$sClassRes .= "));\n";
5231				$sClassRes .= "\n";
5232
5233				if ($bNotInDico || ($sOutputFilter != 'NotInDictionary'))
5234				{
5235					$sRes .= $sClassRes;
5236				}
5237			}
5238		}
5239
5240		return $sRes;
5241	}
5242
5243
5244	/**
5245	 * @return array
5246	 * @throws \CoreException
5247	 * @throws \Exception
5248	 */
5249	public static function DBCheckFormat()
5250	{
5251		$aErrors = array();
5252		$aSugFix = array();
5253
5254		$sAlterDBMetaData = CMDBSource::DBCheckCharsetAndCollation();
5255
5256		// A new way of representing things to be done - quicker to execute !
5257		$aCreateTable = array(); // array of <table> => <table options>
5258		$aCreateTableItems = array(); // array of <table> => array of <create definition>
5259		$aAlterTableMetaData = array();
5260		$aAlterTableItems = array(); // array of <table> => <alter specification>
5261		$aPostTableAlteration = array(); // array of <table> => post alteration queries
5262
5263		foreach(self::GetClasses() as $sClass)
5264		{
5265			if (!self::HasTable($sClass))
5266			{
5267				continue;
5268			}
5269
5270			// Check that the table exists
5271			//
5272			$sTable = self::DBGetTable($sClass);
5273			$aSugFix[$sClass]['*First'] = array();
5274
5275			$aTableInfo = CMDBSource::GetTableInfo($sTable);
5276
5277			$bTableToCreate = false;
5278			$sKeyField = self::DBGetKey($sClass);
5279			$sDbCharset = DEFAULT_CHARACTER_SET;
5280			$sDbCollation = DEFAULT_COLLATION;
5281			$sAutoIncrement = (self::IsAutoIncrementKey($sClass) ? "AUTO_INCREMENT" : "");
5282			$sKeyFieldDefinition = "`$sKeyField` INT(11) NOT NULL $sAutoIncrement PRIMARY KEY";
5283			$aTableInfo['Indexes']['PRIMARY']['used'] = true;
5284			if (!CMDBSource::IsTable($sTable))
5285			{
5286				$bTableToCreate = true;
5287				$aErrors[$sClass]['*'][] = "table '$sTable' could not be found in the DB";
5288				$aSugFix[$sClass]['*'][] = "CREATE TABLE `$sTable` ($sKeyFieldDefinition) ENGINE = ".MYSQL_ENGINE." CHARACTER SET $sDbCharset COLLATE $sDbCollation";
5289				$aCreateTable[$sTable] = "ENGINE = ".MYSQL_ENGINE." CHARACTER SET $sDbCharset COLLATE $sDbCollation";
5290				$aCreateTableItems[$sTable][$sKeyField] = $sKeyFieldDefinition;
5291			}
5292			// Check that the key field exists
5293			//
5294			elseif (!CMDBSource::IsField($sTable, $sKeyField))
5295			{
5296				$aErrors[$sClass]['id'][] = "key '$sKeyField' (table $sTable) could not be found";
5297				$aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable` ADD $sKeyFieldDefinition";
5298				if (!$bTableToCreate)
5299				{
5300					$aAlterTableItems[$sTable][$sKeyField] = "ADD $sKeyFieldDefinition";
5301				}
5302			}
5303			else
5304			{
5305				// Check the key field properties
5306				//
5307				if (!CMDBSource::IsKey($sTable, $sKeyField))
5308				{
5309					$aErrors[$sClass]['id'][] = "key '$sKeyField' is not a key for table '$sTable'";
5310					$aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable`, DROP PRIMARY KEY, ADD PRIMARY key(`$sKeyField`)";
5311					if (!$bTableToCreate)
5312					{
5313						$aAlterTableItems[$sTable][$sKeyField] = "CHANGE `$sKeyField` $sKeyFieldDefinition";
5314					}
5315				}
5316				if (self::IsAutoIncrementKey($sClass) && !CMDBSource::IsAutoIncrement($sTable, $sKeyField))
5317				{
5318					$aErrors[$sClass]['id'][] = "key '$sKeyField' (table $sTable) is not automatically incremented";
5319					$aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable` CHANGE `$sKeyField` $sKeyFieldDefinition";
5320					if (!$bTableToCreate)
5321					{
5322						$aAlterTableItems[$sTable][$sKeyField] = "CHANGE `$sKeyField` $sKeyFieldDefinition";
5323					}
5324				}
5325			}
5326
5327			if (!$bTableToCreate)
5328			{
5329				$sAlterTableMetaDataQuery = CMDBSource::DBCheckTableCharsetAndCollation($sTable);
5330				if (!empty($sAlterTableMetaDataQuery))
5331				{
5332					$aAlterTableMetaData[$sTable] = $sAlterTableMetaDataQuery;
5333				}
5334			}
5335
5336			// Check that any defined field exists
5337			//
5338			$aTableInfo['Fields'][$sKeyField]['used'] = true;
5339			$aFriendlynameAttcodes = self::GetFriendlyNameAttributeCodeList($sClass);
5340			foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
5341			{
5342				if (!$oAttDef->CopyOnAllTables())
5343				{
5344					// Skip this attribute if not originaly defined in this class
5345					if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass)
5346					{
5347						continue;
5348					}
5349				}
5350				foreach($oAttDef->GetSQLColumns(true) as $sField => $sDBFieldSpec)
5351				{
5352					// Keep track of columns used by iTop
5353					$aTableInfo['Fields'][$sField]['used'] = true;
5354
5355					$bIndexNeeded = $oAttDef->RequiresIndex();
5356					$bFullTextIndexNeeded = false;
5357					if (!$bIndexNeeded)
5358					{
5359						// Add an index on the columns of the friendlyname
5360						if (in_array($sField, $aFriendlynameAttcodes))
5361						{
5362							$bIndexNeeded = true;
5363						}
5364					}
5365					else
5366					{
5367						if ($oAttDef->RequiresFullTextIndex())
5368						{
5369							$bFullTextIndexNeeded = true;
5370						}
5371					}
5372
5373					$sFieldDefinition = "`$sField` $sDBFieldSpec";
5374					if (!CMDBSource::IsField($sTable, $sField))
5375					{
5376						$aErrors[$sClass][$sAttCode][] = "field '$sField' could not be found in table '$sTable'";
5377						$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD $sFieldDefinition";
5378
5379						if ($bTableToCreate)
5380						{
5381							$aCreateTableItems[$sTable][$sField] = $sFieldDefinition;
5382						}
5383						else
5384						{
5385							$aAlterTableItems[$sTable][$sField] = "ADD $sFieldDefinition";
5386						}
5387
5388						if ($bIndexNeeded)
5389						{
5390							$aTableInfo['Indexes'][$sField]['used'] = true;
5391							$sIndexName = $sField;
5392							$sColumns = '`'.$sField.'`';
5393
5394							if ($bFullTextIndexNeeded)
5395							{
5396								$sIndexType = 'FULLTEXT INDEX';
5397							}
5398							else
5399							{
5400								$sIndexType = 'INDEX';
5401								$aColumns = array($sField);
5402								$aLength = self::DBGetIndexesLength($sClass, $aColumns, $aTableInfo);
5403								if (!is_null($aLength[0]))
5404								{
5405									$sColumns .= ' ('.$aLength[0].')';
5406								}
5407							}
5408							$sSugFix = "ALTER TABLE `$sTable` ADD $sIndexType `$sIndexName` ($sColumns)";
5409							$aSugFix[$sClass][$sAttCode][] = $sSugFix;
5410							if ($bFullTextIndexNeeded)
5411							{
5412								// MySQL does not support multi fulltext index creation in a single query (mysql_errno = 1795)
5413								$aPostTableAlteration[$sTable][] = $sSugFix;
5414							}
5415							elseif ($bTableToCreate)
5416							{
5417								$aCreateTableItems[$sTable][] = "$sIndexType `$sIndexName` ($sColumns)";
5418							}
5419							else
5420							{
5421								$aAlterTableItems[$sTable][] = "ADD $sIndexType `$sIndexName` ($sColumns)";
5422							}
5423						}
5424
5425					}
5426					else
5427					{
5428						// Create indexes (external keys only... so far)
5429						// (drop before change, add after change)
5430						$sSugFixAfterChange = '';
5431						$sAlterTableItemsAfterChange = '';
5432						if ($bIndexNeeded)
5433						{
5434							$aTableInfo['Indexes'][$sField]['used'] = true;
5435
5436							if ($bFullTextIndexNeeded)
5437							{
5438								$sIndexType = 'FULLTEXT INDEX';
5439								$aColumns = null;
5440								$aLength = null;
5441							}
5442							else
5443							{
5444								$sIndexType = 'INDEX';
5445								$aColumns = array($sField);
5446								$aLength = self::DBGetIndexesLength($sClass, $aColumns, $aTableInfo);
5447							}
5448
5449							if (!CMDBSource::HasIndex($sTable, $sField, $aColumns, $aLength))
5450							{
5451								$sIndexName = $sField;
5452								$sColumns = '`'.$sField.'`';
5453								if (!is_null($aLength[0]))
5454								{
5455									$sColumns .= ' ('.$aLength[0].')';
5456								}
5457
5458								$aErrors[$sClass][$sAttCode][] = "Foreign key '$sField' in table '$sTable' should have an index";
5459								if (CMDBSource::HasIndex($sTable, $sField))
5460								{
5461									$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` DROP INDEX `$sIndexName`";
5462									$aAlterTableItems[$sTable][] = "DROP INDEX `$sIndexName`";
5463								}
5464								$sSugFixAfterChange = "ALTER TABLE `$sTable` ADD $sIndexType `$sIndexName` ($sColumns)";
5465								$sAlterTableItemsAfterChange = "ADD $sIndexType `$sIndexName` ($sColumns)";
5466							}
5467						}
5468
5469						// The field already exists, does it have the relevant properties?
5470						//
5471						$bToBeChanged = false;
5472						$sActualFieldSpec = CMDBSource::GetFieldSpec($sTable, $sField);
5473						if (strcasecmp($sDBFieldSpec, $sActualFieldSpec) != 0)
5474						{
5475							$bToBeChanged = true;
5476							$aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' has a wrong type: found '$sActualFieldSpec' while expecting '$sDBFieldSpec'";
5477						}
5478						if ($bToBeChanged)
5479						{
5480							$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` CHANGE `$sField` $sFieldDefinition";
5481							$aAlterTableItems[$sTable][$sField] = "CHANGE `$sField` $sFieldDefinition";
5482						}
5483
5484						// Create indexes (external keys only... so far)
5485						//
5486						if (!empty($sSugFixAfterChange))
5487						{
5488							$aSugFix[$sClass][$sAttCode][] = $sSugFixAfterChange;
5489							if ($bFullTextIndexNeeded)
5490							{
5491								// MySQL does not support multi fulltext index creation in a single query (mysql_errno = 1795)
5492								$aPostTableAlteration[$sTable][] = $sSugFixAfterChange;
5493							}
5494							else
5495							{
5496								$aAlterTableItems[$sTable][] = $sAlterTableItemsAfterChange;
5497							}
5498						}
5499					}
5500				}
5501			}
5502
5503			// Check indexes
5504			foreach(self::DBGetIndexes($sClass) as $aColumns)
5505			{
5506				$sIndexId = implode('_', $aColumns);
5507
5508				if (isset($aTableInfo['Indexes'][$sIndexId]['used']) && $aTableInfo['Indexes'][$sIndexId]['used'])
5509				{
5510					continue;
5511				}
5512
5513				$aLength = self::DBGetIndexesLength($sClass, $aColumns, $aTableInfo);
5514				$aTableInfo['Indexes'][$sIndexId]['used'] = true;
5515
5516				if (!CMDBSource::HasIndex($sTable, $sIndexId, $aColumns, $aLength))
5517				{
5518					$sColumns = '';
5519
5520					for ($i = 0; $i < count($aColumns); $i++)
5521					{
5522						if (!empty($sColumns))
5523						{
5524							$sColumns .= ', ';
5525						}
5526						$sColumns .= '`'.$aColumns[$i].'`';
5527						if (!is_null($aLength[$i]))
5528						{
5529							$sColumns .= ' ('.$aLength[$i].')';
5530						}
5531					}
5532					if (CMDBSource::HasIndex($sTable, $sIndexId))
5533					{
5534						$aErrors[$sClass]['*'][] = "Wrong index '$sIndexId' ($sColumns) in table '$sTable'";
5535						$aSugFix[$sClass]['*First'][] = "ALTER TABLE `$sTable` DROP INDEX `$sIndexId`";
5536						$aSugFix[$sClass]['*'][] = "ALTER TABLE `$sTable` ADD INDEX `$sIndexId` ($sColumns)";
5537					}
5538					else
5539					{
5540						$aErrors[$sClass]['*'][] = "Missing index '$sIndexId' ($sColumns) in table '$sTable'";
5541						$aSugFix[$sClass]['*'][] = "ALTER TABLE `$sTable` ADD INDEX `$sIndexId` ($sColumns)";
5542					}
5543					if ($bTableToCreate)
5544					{
5545						$aCreateTableItems[$sTable][] = "INDEX `$sIndexId` ($sColumns)";
5546					}
5547					else
5548					{
5549						if (CMDBSource::HasIndex($sTable, $sIndexId))
5550						{
5551							// Add the drop before CHARSET alteration
5552							if (!isset($aAlterTableItems[$sTable]))
5553							{
5554								$aAlterTableItems[$sTable] = array();
5555							}
5556							array_unshift($aAlterTableItems[$sTable], "DROP INDEX `$sIndexId`");
5557						}
5558						$aAlterTableItems[$sTable][] = "ADD INDEX `$sIndexId` ($sColumns)";
5559					}
5560				}
5561			}
5562
5563			// Find out unused columns
5564			//
5565			foreach($aTableInfo['Fields'] as $sField => $aFieldData)
5566			{
5567				if (!isset($aFieldData['used']) || !$aFieldData['used'])
5568				{
5569					$aErrors[$sClass]['*'][] = "Column '$sField' in table '$sTable' is not used";
5570					if (!CMDBSource::IsNullAllowed($sTable, $sField))
5571					{
5572						// Allow null values so that new record can be inserted
5573						// without specifying the value of this unknown column
5574						$sFieldDefinition = "`$sField` ".CMDBSource::GetFieldType($sTable, $sField).' NULL';
5575						$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` CHANGE `$sField` $sFieldDefinition";
5576						$aAlterTableItems[$sTable][$sField] = "CHANGE `$sField` $sFieldDefinition";
5577					}
5578					$aSugFix[$sClass][$sAttCode][] = "-- Recommended action: ALTER TABLE `$sTable` DROP `$sField`";
5579				}
5580			}
5581
5582			// Find out unused indexes
5583			//
5584			foreach($aTableInfo['Indexes'] as $sIndexId => $aIndexData)
5585			{
5586				if (!isset($aIndexData['used']) || !$aIndexData['used'])
5587				{
5588					$aErrors[$sClass]['*'][] = "Index '$sIndexId' in table '$sTable' is not used and will be removed";
5589					$aSugFix[$sClass]['*First'][] = "ALTER TABLE `$sTable` DROP INDEX `$sIndexId`";
5590					// Add the drop before CHARSET alteration
5591					if (!isset($aAlterTableItems[$sTable]))
5592					{
5593						$aAlterTableItems[$sTable] = array();
5594					}
5595					array_unshift($aAlterTableItems[$sTable], "DROP INDEX `$sIndexId`");
5596				}
5597			}
5598
5599			if (empty($aSugFix[$sClass]['*First'])) unset($aSugFix[$sClass]['*First']);
5600		}
5601
5602		$aCondensedQueries = array();
5603		if (!empty($sAlterDBMetaData))
5604		{
5605			$aCondensedQueries[] = $sAlterDBMetaData;
5606		}
5607		foreach($aCreateTable as $sTable => $sTableOptions)
5608		{
5609			$sTableItems = implode(', ', $aCreateTableItems[$sTable]);
5610			$aCondensedQueries[] = "CREATE TABLE `$sTable` ($sTableItems) $sTableOptions";
5611		}
5612		foreach ($aAlterTableMetaData as $sTableAlterQuery)
5613		{
5614			$aCondensedQueries[] = $sTableAlterQuery;
5615		}
5616		foreach ($aAlterTableItems as $sTable => $aChangeList)
5617		{
5618			$sChangeList = implode(', ', $aChangeList);
5619			$aCondensedQueries[] = "ALTER TABLE `$sTable` $sChangeList";
5620		}
5621		foreach($aPostTableAlteration  as $sTable => $aChangeList)
5622		{
5623			$aCondensedQueries = array_merge($aCondensedQueries, $aChangeList);
5624		}
5625
5626		return array($aErrors, $aSugFix, $aCondensedQueries);
5627	}
5628
5629
5630	/**
5631	 * @return array
5632	 * @throws \CoreException
5633	 * @throws \Exception
5634	 * @throws \MissingQueryArgument
5635	 */
5636	public static function DBCheckViews()
5637	{
5638		$aErrors = array();
5639		$aSugFix = array();
5640
5641		// Reporting views (must be created after any other table)
5642		//
5643		foreach(self::GetClasses('bizmodel') as $sClass)
5644		{
5645			$sView = self::DBGetView($sClass);
5646			if (CMDBSource::IsTable($sView))
5647			{
5648				// Check that the view is complete
5649				//
5650				// Note: checking the list of attributes is not enough because the columns can be stable while the SELECT is not stable
5651				//       Example: new way to compute the friendly name
5652				//       The correct comparison algorithm is to compare the queries,
5653				//       by using "SHOW CREATE VIEW" (MySQL 5.0.1 required) or to look into INFORMATION_SCHEMA/views
5654				//       both requiring some privileges
5655				// Decision: to simplify, let's consider the views as being wrong anytime
5656				// Rework the view
5657				//
5658				$oFilter = new DBObjectSearch($sClass, '');
5659				$oFilter->AllowAllData();
5660				$sSQL = $oFilter->MakeSelectQuery();
5661				$aErrors[$sClass]['*'][] = "Redeclare view '$sView' (systematic - to support an eventual change in the friendly name computation)";
5662				$aSugFix[$sClass]['*'][] = "ALTER VIEW `$sView` AS $sSQL";
5663			}
5664			else
5665			{
5666				// Create the view
5667				//
5668				$oFilter = new DBObjectSearch($sClass, '');
5669				$oFilter->AllowAllData();
5670				$sSQL = $oFilter->MakeSelectQuery();
5671				$aErrors[$sClass]['*'][] = "Missing view for class: $sClass";
5672				$aSugFix[$sClass]['*'][] = "DROP VIEW IF EXISTS `$sView`";
5673				$aSugFix[$sClass]['*'][] = "CREATE VIEW `$sView` AS $sSQL";
5674			}
5675		}
5676		return array($aErrors, $aSugFix);
5677	}
5678
5679	/**
5680	 * @param string $sSelWrongRecs
5681	 * @param string $sErrorDesc
5682	 * @param string $sClass
5683	 * @param array $aErrorsAndFixes
5684	 * @param int $iNewDelCount
5685	 * @param array $aPlannedDel
5686	 * @param bool $bProcessingFriends
5687	 *
5688	 * @throws \CoreException
5689	 */
5690	private static function DBCheckIntegrity_Check2Delete($sSelWrongRecs, $sErrorDesc, $sClass, &$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel, $bProcessingFriends = false)
5691	{
5692		$sRootClass = self::GetRootClass($sClass);
5693		$sTable = self::DBGetTable($sClass);
5694		$sKeyField = self::DBGetKey($sClass);
5695
5696		if (array_key_exists($sTable, $aPlannedDel) && count($aPlannedDel[$sTable]) > 0)
5697		{
5698			$sSelWrongRecs .= " AND maintable.`$sKeyField` NOT IN ('".implode("', '", $aPlannedDel[$sTable])."')";
5699		}
5700		$aWrongRecords = CMDBSource::QueryToCol($sSelWrongRecs, "id");
5701		if (count($aWrongRecords) == 0)
5702		{
5703			return;
5704		}
5705
5706		if (!array_key_exists($sRootClass, $aErrorsAndFixes))
5707		{
5708			$aErrorsAndFixes[$sRootClass] = array();
5709		}
5710		if (!array_key_exists($sTable, $aErrorsAndFixes[$sRootClass]))
5711		{
5712			$aErrorsAndFixes[$sRootClass][$sTable] = array();
5713		}
5714
5715		foreach($aWrongRecords as $iRecordId)
5716		{
5717			if (array_key_exists($iRecordId, $aErrorsAndFixes[$sRootClass][$sTable]))
5718			{
5719				switch ($aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action'])
5720				{
5721					case 'Delete':
5722						// Already planned for a deletion
5723						// Let's concatenate the errors description together
5724						$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] .= ', '.$sErrorDesc;
5725						break;
5726
5727					case 'Update':
5728						// Let's plan a deletion
5729						break;
5730				}
5731			}
5732			else
5733			{
5734				$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] = $sErrorDesc;
5735			}
5736
5737			if (!$bProcessingFriends)
5738			{
5739				if (!array_key_exists($sTable, $aPlannedDel) || !in_array($iRecordId, $aPlannedDel[$sTable]))
5740				{
5741					// Something new to be deleted...
5742					$iNewDelCount++;
5743				}
5744			}
5745
5746			$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action'] = 'Delete';
5747			$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] = array();
5748			$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Pass'] = 123;
5749			$aPlannedDel[$sTable][] = $iRecordId;
5750		}
5751
5752		// Now make sure that we would delete the records of the other tables for that class
5753		//
5754		if (!$bProcessingFriends)
5755		{
5756			$sDeleteKeys = "'".implode("', '", $aWrongRecords)."'";
5757			foreach(self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL) as $sFriendClass)
5758			{
5759				$sFriendTable = self::DBGetTable($sFriendClass);
5760				$sFriendKey = self::DBGetKey($sFriendClass);
5761
5762				// skip the current table
5763				if ($sFriendTable == $sTable)
5764				{
5765					continue;
5766				}
5767
5768				$sFindRelatedRec = "SELECT DISTINCT maintable.`$sFriendKey` AS id FROM `$sFriendTable` AS maintable WHERE maintable.`$sFriendKey` IN ($sDeleteKeys)";
5769				self::DBCheckIntegrity_Check2Delete($sFindRelatedRec,
5770					"Cascading deletion of record in friend table `<em>$sTable</em>`", $sFriendClass, $aErrorsAndFixes,
5771					$iNewDelCount, $aPlannedDel,
5772					true);
5773			}
5774		}
5775	}
5776
5777	/**
5778	 * @param string $sSelWrongRecs
5779	 * @param string $sErrorDesc
5780	 * @param string $sColumn
5781	 * @param string $sNewValue
5782	 * @param string $sClass
5783	 * @param array $aErrorsAndFixes
5784	 * @param int $iNewDelCount
5785	 * @param array $aPlannedDel
5786	 *
5787	 * @throws \CoreException
5788	 */
5789	private static function DBCheckIntegrity_Check2Update($sSelWrongRecs, $sErrorDesc, $sColumn, $sNewValue, $sClass, &$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel)
5790	{
5791		$sRootClass = self::GetRootClass($sClass);
5792		$sTable = self::DBGetTable($sClass);
5793		$sKeyField = self::DBGetKey($sClass);
5794
5795		if (array_key_exists($sTable, $aPlannedDel) && count($aPlannedDel[$sTable]) > 0)
5796		{
5797			$sSelWrongRecs .= " AND maintable.`$sKeyField` NOT IN ('".implode("', '", $aPlannedDel[$sTable])."')";
5798		}
5799		$aWrongRecords = CMDBSource::QueryToCol($sSelWrongRecs, "id");
5800		if (count($aWrongRecords) == 0)
5801		{
5802			return;
5803		}
5804
5805		if (!array_key_exists($sRootClass, $aErrorsAndFixes))
5806		{
5807			$aErrorsAndFixes[$sRootClass] = array();
5808		}
5809		if (!array_key_exists($sTable, $aErrorsAndFixes[$sRootClass]))
5810		{
5811			$aErrorsAndFixes[$sRootClass][$sTable] = array();
5812		}
5813
5814		foreach($aWrongRecords as $iRecordId)
5815		{
5816			if (array_key_exists($iRecordId, $aErrorsAndFixes[$sRootClass][$sTable]))
5817			{
5818				$sAction = $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action'];
5819
5820				if ($sAction == 'Delete')
5821				{
5822					// No need to update, the record will be deleted!
5823				}
5824
5825				if ($sAction == 'Update')
5826				{
5827					// Already planned for an update
5828					// Add this new update spec to the list
5829					$bFoundSameSpec = false;
5830					foreach ($aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] as $aUpdateSpec)
5831					{
5832						if (($sColumn == $aUpdateSpec['column']) && ($sNewValue == $aUpdateSpec['newvalue']))
5833						{
5834							$bFoundSameSpec = true;
5835						}
5836					}
5837					if (!$bFoundSameSpec)
5838					{
5839						$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'][] = (array(
5840							'column' => $sColumn,
5841							'newvalue' => $sNewValue
5842						));
5843						$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] .= ', '.$sErrorDesc;
5844					}
5845				}
5846			}
5847			else
5848			{
5849				$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] = $sErrorDesc;
5850				$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action'] = 'Update';
5851				$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] = array(array('column' => $sColumn, 'newvalue' => $sNewValue));
5852				$aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Pass'] = 123;
5853			}
5854
5855		}
5856	}
5857
5858	/**
5859	 * @param array $aErrorsAndFixes
5860	 * @param int $iNewDelCount
5861	 * @param array $aPlannedDel
5862	 *
5863	 * @throws \CoreException
5864	 * @throws \Exception
5865	 */
5866	public static function DBCheckIntegrity_SinglePass(&$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel)
5867	{
5868		foreach(self::GetClasses() as $sClass)
5869		{
5870			if (!self::HasTable($sClass))
5871			{
5872				continue;
5873			}
5874			$sRootClass = self::GetRootClass($sClass);
5875			$sTable = self::DBGetTable($sClass);
5876			$sKeyField = self::DBGetKey($sClass);
5877
5878			if (!self::IsStandaloneClass($sClass))
5879			{
5880				if (self::IsRootClass($sClass))
5881				{
5882					// Check that the final class field contains the name of a class which inherited from the current class
5883					//
5884					$sFinalClassField = self::DBGetClassField($sClass);
5885
5886					$aAllowedValues = self::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL);
5887					$sAllowedValues = implode(",", CMDBSource::Quote($aAllowedValues, true));
5888
5889					$sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` AS maintable WHERE `$sFinalClassField` NOT IN ($sAllowedValues)";
5890					self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "final class (field `<em>$sFinalClassField</em>`) is wrong (expected a value in {".$sAllowedValues."})", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
5891				}
5892				else
5893				{
5894					$sRootTable = self::DBGetTable($sRootClass);
5895					$sRootKey = self::DBGetKey($sRootClass);
5896					$sFinalClassField = self::DBGetClassField($sRootClass);
5897
5898					$aExpectedClasses = self::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL);
5899					$sExpectedClasses = implode(",", CMDBSource::Quote($aExpectedClasses, true));
5900
5901					// Check that any record found here has its counterpart in the root table
5902					// and which refers to a child class
5903					//
5904					$sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` as maintable LEFT JOIN `$sRootTable` ON maintable.`$sKeyField` = `$sRootTable`.`$sRootKey` AND `$sRootTable`.`$sFinalClassField` IN ($sExpectedClasses) WHERE `$sRootTable`.`$sRootKey` IS NULL";
5905					self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Found a record in `<em>$sTable</em>`, but no counterpart in root table `<em>$sRootTable</em>` (inc. records pointing to a class in {".$sExpectedClasses."})", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
5906
5907					// Check that any record found in the root table and referring to a child class
5908					// has its counterpart here (detect orphan nodes -root or in the middle of the hierarchy)
5909					//
5910					$sSelWrongRecs = "SELECT DISTINCT maintable.`$sRootKey` AS id FROM `$sRootTable` AS maintable LEFT JOIN `$sTable` ON maintable.`$sRootKey` = `$sTable`.`$sKeyField` WHERE `$sTable`.`$sKeyField` IS NULL AND maintable.`$sFinalClassField` IN ($sExpectedClasses)";
5911					self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Found a record in root table `<em>$sRootTable</em>`, but no counterpart in table `<em>$sTable</em>` (root records pointing to a class in {".$sExpectedClasses."})", $sRootClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
5912				}
5913			}
5914
5915			foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
5916			{
5917				// Skip this attribute if not defined in this table
5918				if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass)
5919				{
5920					continue;
5921				}
5922
5923				if ($oAttDef->IsExternalKey())
5924				{
5925					// Check that any external field is pointing to an existing object
5926					//
5927					$sRemoteClass = $oAttDef->GetTargetClass();
5928					$sRemoteTable = self::DBGetTable($sRemoteClass);
5929					$sRemoteKey = self::DBGetKey($sRemoteClass);
5930
5931					$aCols = $oAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc())
5932					$sExtKeyField = current($aCols); // get the first column for an external key
5933
5934					// Note: a class/table may have an external key on itself
5935					$sSelBase = "SELECT DISTINCT maintable.`$sKeyField` AS id, maintable.`$sExtKeyField` AS extkey FROM `$sTable` AS maintable LEFT JOIN `$sRemoteTable` ON maintable.`$sExtKeyField` = `$sRemoteTable`.`$sRemoteKey`";
5936
5937					$sSelWrongRecs = $sSelBase." WHERE `$sRemoteTable`.`$sRemoteKey` IS NULL";
5938					if ($oAttDef->IsNullAllowed())
5939					{
5940						// Exclude the records pointing to 0/null from the errors
5941						$sSelWrongRecs .= " AND maintable.`$sExtKeyField` IS NOT NULL";
5942						$sSelWrongRecs .= " AND maintable.`$sExtKeyField` != 0";
5943						self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record pointing to (external key '<em>$sAttCode</em>') non existing objects", $sExtKeyField, 'null', $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
5944					}
5945					else
5946					{
5947						self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Record pointing to (external key '<em>$sAttCode</em>') non existing objects", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
5948					}
5949
5950					// Do almost the same, taking into account the records planned for deletion
5951					if (array_key_exists($sRemoteTable, $aPlannedDel) && count($aPlannedDel[$sRemoteTable]) > 0)
5952					{
5953						// This could be done by the mean of a 'OR ... IN (aIgnoreRecords)
5954						// but in that case you won't be able to track the root cause (cascading)
5955						$sSelWrongRecs = $sSelBase." WHERE maintable.`$sExtKeyField` IN ('".implode("', '", $aPlannedDel[$sRemoteTable])."')";
5956						if ($oAttDef->IsNullAllowed())
5957						{
5958							// Exclude the records pointing to 0/null from the errors
5959							$sSelWrongRecs .= " AND maintable.`$sExtKeyField` IS NOT NULL";
5960							$sSelWrongRecs .= " AND maintable.`$sExtKeyField` != 0";
5961							self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record pointing to (external key '<em>$sAttCode</em>') a record planned for deletion", $sExtKeyField, 'null', $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
5962						}
5963						else
5964						{
5965							self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Record pointing to (external key '<em>$sAttCode</em>') a record planned for deletion", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
5966						}
5967					}
5968				}
5969				else
5970				{
5971					if ($oAttDef->IsBasedOnDBColumns())
5972					{
5973						// Check that the values fit the allowed values
5974						//
5975						$aAllowedValues = self::GetAllowedValues_att($sClass, $sAttCode);
5976						if (!is_null($aAllowedValues) && count($aAllowedValues) > 0)
5977						{
5978							$sExpectedValues = implode(",", CMDBSource::Quote(array_keys($aAllowedValues), true));
5979
5980							$aCols = $oAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc())
5981							$sMyAttributeField = current($aCols); // get the first column for the moment
5982							$sDefaultValue = $oAttDef->GetDefaultValue();
5983							$sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` AS maintable WHERE maintable.`$sMyAttributeField` NOT IN ($sExpectedValues)";
5984							self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record having a column ('<em>$sAttCode</em>') with an unexpected value", $sMyAttributeField, CMDBSource::Quote($sDefaultValue), $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
5985						}
5986					}
5987				}
5988			}
5989		}
5990	}
5991
5992	/**
5993	 * @param string $sRepairUrl
5994	 * @param string $sSQLStatementArgName
5995	 *
5996	 * @throws \CoreException
5997	 * @throws \CoreWarning
5998	 * @throws \Exception
5999	 */
6000	public static function DBCheckIntegrity($sRepairUrl = "", $sSQLStatementArgName = "")
6001	{
6002		// Records in error, and action to be taken: delete or update
6003		// by RootClass/Table/Record
6004		$aErrorsAndFixes = array();
6005
6006		// Records to be ignored in the current/next pass
6007		// by Table = array of RecordId
6008		$aPlannedDel = array();
6009
6010		// Count of errors in the next pass: no error means that we can leave...
6011		$iErrorCount = 0;
6012		// Limit in case of a bug in the algorythm
6013		$iLoopCount = 0;
6014
6015		$iNewDelCount = 1; // startup...
6016		while ($iNewDelCount > 0)
6017		{
6018			$iNewDelCount = 0;
6019			self::DBCheckIntegrity_SinglePass($aErrorsAndFixes, $iNewDelCount, $aPlannedDel);
6020			$iErrorCount += $iNewDelCount;
6021
6022			// Safety net #1 - limit the planned deletions
6023			//
6024			$iMaxDel = 1000;
6025			$iPlannedDel = 0;
6026			foreach($aPlannedDel as $sTable => $aPlannedDelOnTable)
6027			{
6028				$iPlannedDel += count($aPlannedDelOnTable);
6029			}
6030			if ($iPlannedDel > $iMaxDel)
6031			{
6032				throw new CoreWarning("DB Integrity Check safety net - Exceeding the limit of $iMaxDel planned record deletion");
6033			}
6034			// Safety net #2 - limit the iterations
6035			//
6036			$iLoopCount++;
6037			$iMaxLoops = 10;
6038			if ($iLoopCount > $iMaxLoops)
6039			{
6040				throw new CoreWarning("DB Integrity Check safety net - Reached the limit of $iMaxLoops loops");
6041			}
6042		}
6043
6044		// Display the results
6045		//
6046		$iIssueCount = 0;
6047		$aFixesDelete = array();
6048		$aFixesUpdate = array();
6049
6050		foreach($aErrorsAndFixes as $sRootClass => $aTables)
6051		{
6052			foreach($aTables as $sTable => $aRecords)
6053			{
6054				foreach($aRecords as $iRecord => $aError)
6055				{
6056					$sAction = $aError['Action'];
6057					$sReason = $aError['Reason'];
6058
6059					switch ($sAction)
6060					{
6061						case 'Delete':
6062							$sActionDetails = "";
6063							$aFixesDelete[$sTable][] = $iRecord;
6064							break;
6065
6066						case 'Update':
6067							$aUpdateDesc = array();
6068							foreach($aError['Action_Details'] as $aUpdateSpec)
6069							{
6070								$aUpdateDesc[] = $aUpdateSpec['column']." -&gt; ".$aUpdateSpec['newvalue'];
6071								$aFixesUpdate[$sTable][$aUpdateSpec['column']][$aUpdateSpec['newvalue']][] = $iRecord;
6072							}
6073							$sActionDetails = "Set ".implode(", ", $aUpdateDesc);
6074
6075							break;
6076
6077						default:
6078							$sActionDetails = "bug: unknown action '$sAction'";
6079					}
6080					$aIssues[] = "$sRootClass / $sTable / $iRecord / $sReason / $sAction / $sActionDetails";
6081					$iIssueCount++;
6082				}
6083			}
6084		}
6085
6086		if ($iIssueCount > 0)
6087		{
6088			// Build the queries to fix in the database
6089			//
6090			// First step, be able to get class data out of the table name
6091			// Could be optimized, because we've made the job earlier... but few benefits, so...
6092			$aTable2ClassProp = array();
6093			foreach(self::GetClasses() as $sClass)
6094			{
6095				if (!self::HasTable($sClass))
6096				{
6097					continue;
6098				}
6099
6100				$sRootClass = self::GetRootClass($sClass);
6101				$sTable = self::DBGetTable($sClass);
6102				$sKeyField = self::DBGetKey($sClass);
6103
6104				$aErrorsAndFixes[$sRootClass][$sTable] = array();
6105				$aTable2ClassProp[$sTable] = array('rootclass' => $sRootClass, 'class' => $sClass, 'keyfield' => $sKeyField);
6106			}
6107			// Second step, build a flat list of SQL queries
6108			$aSQLFixes = array();
6109			$iPlannedUpdate = 0;
6110			foreach($aFixesUpdate as $sTable => $aColumns)
6111			{
6112				foreach($aColumns as $sColumn => $aNewValues)
6113				{
6114					foreach($aNewValues as $sNewValue => $aRecords)
6115					{
6116						$iPlannedUpdate += count($aRecords);
6117						$sWrongRecords = "'".implode("', '", $aRecords)."'";
6118						$sKeyField = $aTable2ClassProp[$sTable]['keyfield'];
6119
6120						$aSQLFixes[] = "UPDATE `$sTable` SET `$sColumn` = $sNewValue WHERE `$sKeyField` IN ($sWrongRecords)";
6121					}
6122				}
6123			}
6124			$iPlannedDel = 0;
6125			foreach($aFixesDelete as $sTable => $aRecords)
6126			{
6127				$iPlannedDel += count($aRecords);
6128				$sWrongRecords = "'".implode("', '", $aRecords)."'";
6129				$sKeyField = $aTable2ClassProp[$sTable]['keyfield'];
6130
6131				$aSQLFixes[] = "DELETE FROM `$sTable` WHERE `$sKeyField` IN ($sWrongRecords)";
6132			}
6133
6134			// Report the results
6135			//
6136			echo "<div style=\"width:100%;padding:10px;background:#FFAAAA;display:;\">";
6137			echo "<h3>Database corruption error(s): $iErrorCount issues have been encountered. $iPlannedDel records will be deleted, $iPlannedUpdate records will be updated:</h3>\n";
6138			// #@# later -> this is the responsibility of the caller to format the output
6139			echo "<ul class=\"treeview\">\n";
6140			foreach($aIssues as $sIssueDesc)
6141			{
6142				echo "<li>$sIssueDesc</li>\n";
6143			}
6144			echo "</ul>\n";
6145			self::DBShowApplyForm($sRepairUrl, $sSQLStatementArgName, $aSQLFixes);
6146			echo "<p>Aborting...</p>\n";
6147			echo "</div>\n";
6148			exit;
6149		}
6150	}
6151
6152	/**
6153	 * @param string|Config $config config file content or {@link Config} object
6154	 * @param bool $bModelOnly
6155	 * @param bool $bAllowCache
6156	 * @param bool $bTraceSourceFiles
6157	 * @param string $sEnvironment
6158	 *
6159	 * @throws \MySQLException
6160	 * @throws \CoreException
6161	 * @throws \DictExceptionUnknownLanguage
6162	 * @throws \Exception
6163	 */
6164	public static function Startup($config, $bModelOnly = false, $bAllowCache = true, $bTraceSourceFiles = false, $sEnvironment = 'production')
6165	{
6166		self::$m_sEnvironment = $sEnvironment;
6167
6168		if (!defined('MODULESROOT'))
6169		{
6170			define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
6171
6172			self::$m_bTraceSourceFiles = $bTraceSourceFiles;
6173
6174			// $config can be either a filename, or a Configuration object (volatile!)
6175			if ($config instanceof Config)
6176			{
6177				self::LoadConfig($config, $bAllowCache);
6178			}
6179			else
6180			{
6181				self::LoadConfig(new Config($config), $bAllowCache);
6182			}
6183
6184			if ($bModelOnly)
6185			{
6186				return;
6187			}
6188		}
6189
6190		CMDBSource::SelectDB(self::$m_sDBName);
6191
6192        foreach(MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass)
6193        {
6194            $oPHPClass::OnMetaModelStarted();
6195        }
6196
6197		ExpressionCache::Warmup();
6198	}
6199
6200	/**
6201	 * @param Config $oConfiguration
6202	 * @param bool $bAllowCache
6203	 *
6204	 * @throws \CoreException
6205	 * @throws \DictExceptionUnknownLanguage
6206	 * @throws \Exception
6207	 * @throws \MySQLException
6208	 */
6209	public static function LoadConfig($oConfiguration, $bAllowCache = false)
6210	{
6211		self::$m_oConfig = $oConfiguration;
6212
6213		// Set log ASAP
6214		if (self::$m_oConfig->GetLogGlobal())
6215		{
6216			if (self::$m_oConfig->GetLogIssue())
6217			{
6218				self::$m_bLogIssue = true;
6219				IssueLog::Enable(APPROOT.'log/error.log');
6220			}
6221			self::$m_bLogNotification = self::$m_oConfig->GetLogNotification();
6222			self::$m_bLogWebService = self::$m_oConfig->GetLogWebService();
6223
6224			ToolsLog::Enable(APPROOT.'log/tools.log');
6225		}
6226		else
6227		{
6228			self::$m_bLogIssue = false;
6229			self::$m_bLogNotification = false;
6230			self::$m_bLogWebService = false;
6231		}
6232
6233		ExecutionKPI::EnableDuration(self::$m_oConfig->Get('log_kpi_duration'));
6234		ExecutionKPI::EnableMemory(self::$m_oConfig->Get('log_kpi_memory'));
6235		ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id'));
6236
6237		self::$m_bSkipCheckToWrite = self::$m_oConfig->Get('skip_check_to_write');
6238		self::$m_bSkipCheckExtKeys = self::$m_oConfig->Get('skip_check_ext_keys');
6239
6240		self::$m_bUseAPCCache = $bAllowCache
6241			&& self::$m_oConfig->Get('apc_cache.enabled')
6242			&& function_exists('apc_fetch')
6243			&& function_exists('apc_store');
6244
6245		DBSearch::EnableQueryCache(self::$m_oConfig->GetQueryCacheEnabled(), self::$m_bUseAPCCache, self::$m_oConfig->Get('apc_cache.query_ttl'));
6246		DBSearch::EnableQueryTrace(self::$m_oConfig->GetLogQueries());
6247		DBSearch::EnableQueryIndentation(self::$m_oConfig->Get('query_indentation_enabled'));
6248		DBSearch::EnableOptimizeQuery(self::$m_oConfig->Get('query_optimization_enabled'));
6249
6250		// PHP timezone first...
6251		//
6252		$sPHPTimezone = self::$m_oConfig->Get('timezone');
6253		if ($sPHPTimezone == '')
6254		{
6255			// Leave as is... up to the admin to set a value somewhere...
6256			//$sPHPTimezone = date_default_timezone_get();
6257		}
6258		else
6259		{
6260			date_default_timezone_set($sPHPTimezone);
6261		}
6262
6263		// Note: load the dictionary as soon as possible, because it might be
6264		//       needed when some error occur
6265		$sAppIdentity = 'itop-'.MetaModel::GetEnvironmentId();
6266		if (self::$m_bUseAPCCache)
6267		{
6268			Dict::EnableCache($sAppIdentity);
6269		}
6270		require_once(APPROOT.'env-'.self::$m_sEnvironment.'/dictionaries/languages.php');
6271
6272		// Set the default language...
6273		Dict::SetDefaultLanguage(self::$m_oConfig->GetDefaultLanguage());
6274
6275		// Romain: this is the only way I've found to cope with the fact that
6276		//         classes have to be derived from cmdbabstract (to be editable in the UI)
6277		require_once(APPROOT.'/application/cmdbabstract.class.inc.php');
6278
6279		require_once(APPROOT.'core/autoload.php');
6280		require_once(APPROOT.'env-'.self::$m_sEnvironment.'/autoload.php');
6281
6282		foreach(self::$m_oConfig->GetAddons() as $sModule => $sToInclude)
6283		{
6284			self::IncludeModule($sToInclude, 'addons');
6285		}
6286
6287		$sSource = self::$m_oConfig->Get('db_name');
6288		$sTablePrefix = self::$m_oConfig->Get('db_subname');
6289
6290		if (self::$m_bUseAPCCache)
6291		{
6292			$oKPI = new ExecutionKPI();
6293			// Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter
6294			//
6295			$sOqlAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-metamodel';
6296			$result = apc_fetch($sOqlAPCCacheId);
6297
6298			if (is_array($result))
6299			{
6300				// todo - verifier que toutes les classes mentionnees ici sont chargees dans InitClasses()
6301				self::$m_aExtensionClasses = $result['m_aExtensionClasses'];
6302				self::$m_Category2Class = $result['m_Category2Class'];
6303				self::$m_aRootClasses = $result['m_aRootClasses'];
6304				self::$m_aParentClasses = $result['m_aParentClasses'];
6305				self::$m_aChildClasses = $result['m_aChildClasses'];
6306				self::$m_aClassParams = $result['m_aClassParams'];
6307				self::$m_aAttribDefs = $result['m_aAttribDefs'];
6308				self::$m_aAttribOrigins = $result['m_aAttribOrigins'];
6309				self::$m_aIgnoredAttributes = $result['m_aIgnoredAttributes'];
6310				self::$m_aFilterDefs = $result['m_aFilterDefs'];
6311				self::$m_aFilterOrigins = $result['m_aFilterOrigins'];
6312				self::$m_aListInfos = $result['m_aListInfos'];
6313				self::$m_aListData = $result['m_aListData'];
6314				self::$m_aRelationInfos = $result['m_aRelationInfos'];
6315				self::$m_aStates = $result['m_aStates'];
6316				self::$m_aStimuli = $result['m_aStimuli'];
6317				self::$m_aTransitions = $result['m_aTransitions'];
6318				self::$m_aHighlightScales = $result['m_aHighlightScales'];
6319				self::$m_aEnumToMeta = $result['m_aEnumToMeta'];
6320			}
6321			$oKPI->ComputeAndReport('Metamodel APC (fetch + read)');
6322		}
6323
6324		if (count(self::$m_aAttribDefs) == 0)
6325		{
6326			// The includes have been included, let's browse the existing classes and
6327			// develop some data based on the proposed model
6328			$oKPI = new ExecutionKPI();
6329
6330			self::InitClasses($sTablePrefix);
6331
6332			$oKPI->ComputeAndReport('Initialization of Data model structures');
6333			if (self::$m_bUseAPCCache)
6334			{
6335				$oKPI = new ExecutionKPI();
6336
6337				$aCache = array();
6338				$aCache['m_aExtensionClasses'] = self::$m_aExtensionClasses;
6339				$aCache['m_Category2Class'] = self::$m_Category2Class;
6340				$aCache['m_aRootClasses'] = self::$m_aRootClasses; // array of "classname" => "rootclass"
6341				$aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass")
6342				$aCache['m_aChildClasses'] = self::$m_aChildClasses; // array of ("classname" => array of "childclass")
6343				$aCache['m_aClassParams'] = self::$m_aClassParams; // array of ("classname" => array of class information)
6344				$aCache['m_aAttribDefs'] = self::$m_aAttribDefs; // array of ("classname" => array of attributes)
6345				$aCache['m_aAttribOrigins'] = self::$m_aAttribOrigins; // array of ("classname" => array of ("attcode"=>"sourceclass"))
6346				$aCache['m_aIgnoredAttributes'] = self::$m_aIgnoredAttributes; //array of ("classname" => array of ("attcode")
6347				$aCache['m_aFilterDefs'] = self::$m_aFilterDefs; // array of ("classname" => array filterdef)
6348				$aCache['m_aFilterOrigins'] = self::$m_aFilterOrigins; // array of ("classname" => array of ("attcode"=>"sourceclass"))
6349				$aCache['m_aListInfos'] = self::$m_aListInfos; // array of ("listcode" => various info on the list, common to every classes)
6350				$aCache['m_aListData'] = self::$m_aListData; // array of ("classname" => array of "listcode" => list)
6351				$aCache['m_aRelationInfos'] = self::$m_aRelationInfos; // array of ("relcode" => various info on the list, common to every classes)
6352				$aCache['m_aStates'] = self::$m_aStates; // array of ("classname" => array of "statecode"=>array('label'=>..., attribute_inherit=> attribute_list=>...))
6353				$aCache['m_aStimuli'] = self::$m_aStimuli; // array of ("classname" => array of ("stimuluscode"=>array('label'=>...)))
6354				$aCache['m_aTransitions'] = self::$m_aTransitions; // array of ("classname" => array of ("statcode_from"=>array of ("stimuluscode" => array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD)))
6355				$aCache['m_aHighlightScales'] = self::$m_aHighlightScales; // array of ("classname" => array of higlightcodes)))
6356				$aCache['m_aEnumToMeta'] = self::$m_aEnumToMeta;
6357				apc_store($sOqlAPCCacheId, $aCache);
6358				$oKPI->ComputeAndReport('Metamodel APC (store)');
6359			}
6360		}
6361
6362		self::$m_sDBName = $sSource;
6363		self::$m_sTablePrefix = $sTablePrefix;
6364
6365		CMDBSource::InitFromConfig(self::$m_oConfig);
6366		// Later when timezone implementation is correctly done: CMDBSource::SetTimezone($sDBTimezone);
6367	}
6368
6369	/**
6370	 * @param string $sModule
6371	 * @param string $sProperty
6372	 * @param $defaultvalue
6373	 *
6374	 * @return mixed
6375	 */
6376	public static function GetModuleSetting($sModule, $sProperty, $defaultvalue = null)
6377	{
6378		return self::$m_oConfig->GetModuleSetting($sModule, $sProperty, $defaultvalue);
6379	}
6380
6381	/**
6382	 * @param string $sModule
6383	 * @param string $sProperty
6384	 * @param $defaultvalue
6385	 *
6386	 * @return ??
6387	 */
6388	public static function GetModuleParameter($sModule, $sProperty, $defaultvalue = null)
6389	{
6390		$value = $defaultvalue;
6391		if (!self::$m_aModulesParameters[$sModule] == null)
6392		{
6393			$value = self::$m_aModulesParameters[$sModule]->Get($sProperty, $defaultvalue);
6394		}
6395		return $value;
6396	}
6397
6398	/**
6399	 * @return Config
6400	 */
6401	public static function GetConfig()
6402	{
6403		return self::$m_oConfig;
6404	}
6405
6406	/**
6407	 * @return string The environment in which the model has been loaded (e.g. 'production')
6408	 */
6409	public static function GetEnvironment()
6410	{
6411		return self::$m_sEnvironment;
6412	}
6413
6414	/**
6415	 * @return string
6416	 */
6417	public static function GetEnvironmentId()
6418	{
6419		return md5(APPROOT).'-'.self::$m_sEnvironment;
6420	}
6421
6422	/** @var array */
6423	protected static $m_aExtensionClasses = array();
6424
6425	/**
6426	 * @param string $sToInclude
6427	 * @param string $sModuleType
6428	 *
6429	 * @throws \CoreException
6430	 */
6431	public static function IncludeModule($sToInclude, $sModuleType = null)
6432	{
6433		$sFirstChar = substr($sToInclude, 0, 1);
6434		$sSecondChar = substr($sToInclude, 1, 1);
6435		if (($sFirstChar != '/') && ($sFirstChar != '\\') && ($sSecondChar != ':'))
6436		{
6437			// It is a relative path, prepend APPROOT
6438			if (substr($sToInclude, 0, 3) == '../')
6439			{
6440				// Preserve compatibility with config files written before 1.0.1
6441				// Replace '../' by '<root>/'
6442				$sFile = APPROOT.'/'.substr($sToInclude, 3);
6443			}
6444			else
6445			{
6446				$sFile = APPROOT.'/'.$sToInclude;
6447			}
6448		}
6449		else
6450		{
6451			// Leave as is - should be an absolute path
6452			$sFile = $sToInclude;
6453		}
6454		if (!file_exists($sFile))
6455		{
6456			$sConfigFile = self::$m_oConfig->GetLoadedFile();
6457			if ($sModuleType == null)
6458			{
6459				throw new CoreException("Include: unable to load the file '$sFile'");
6460			}
6461			else
6462			{
6463				if (strlen($sConfigFile) > 0)
6464				{
6465					throw new CoreException('Include: wrong file name in configuration file', array('config file' => $sConfigFile, 'section' => $sModuleType, 'filename' => $sFile));
6466				}
6467				else
6468				{
6469					// The configuration is in memory only
6470					throw new CoreException('Include: wrong file name in configuration file (in memory)', array('section' => $sModuleType, 'filename' => $sFile));
6471				}
6472			}
6473		}
6474
6475		// Note: We do not expect the modules to output characters while loading them.
6476		//       Therefore, and because unexpected characters can corrupt the output,
6477		//       they must be trashed here.
6478		//       Additionnaly, pages aiming at delivering data in their output can call WebPage::TrashUnexpectedOutput()
6479		//       to get rid of chars that could be generated during the execution of the code
6480		ob_start();
6481		require_once($sFile);
6482		$sPreviousContent = ob_get_clean();
6483		if (self::$m_oConfig->Get('debug_report_spurious_chars'))
6484		{
6485			if ($sPreviousContent != '')
6486			{
6487				IssueLog::Error("Spurious characters injected by '$sFile'");
6488			}
6489		}
6490	}
6491
6492	// Building an object
6493	//
6494	//
6495	/** @var array */
6496	private static $aQueryCacheGetObject = array();
6497	/** @var array */
6498	private static $aQueryCacheGetObjectHits = array();
6499
6500	/**
6501	 * @return string
6502	 */
6503	public static function GetQueryCacheStatus()
6504	{
6505		$aRes = array();
6506		$iTotalHits = 0;
6507		foreach(self::$aQueryCacheGetObjectHits as $sClassSign => $iHits)
6508		{
6509			$aRes[] = "$sClassSign: $iHits";
6510			$iTotalHits += $iHits;
6511		}
6512		return $iTotalHits.' ('.implode(', ', $aRes).')';
6513	}
6514
6515	/**
6516	 * @param string $sClass
6517	 * @param int $iKey
6518	 * @param bool $bMustBeFound
6519	 * @param bool $bAllowAllData if true then no rights filtering
6520	 * @param array $aModifierProperties
6521	 *
6522	 * @return string[] column name / value array
6523	 * @throws CoreException if no result found and $bMustBeFound=true
6524	 * @throws \Exception
6525	 *
6526	 * @see utils::PushArchiveMode() to enable search on archived objects
6527	 */
6528	public static function MakeSingleRow($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null)
6529	{
6530		// Build the query cache signature
6531		//
6532		$sQuerySign = $sClass;
6533		if ($bAllowAllData)
6534		{
6535			$sQuerySign .= '_all_';
6536		}
6537		if (is_array($aModifierProperties) && (count($aModifierProperties) > 0))
6538		{
6539			array_multisort($aModifierProperties);
6540			$sModifierProperties = json_encode($aModifierProperties);
6541			$sQuerySign .= '_all_'.md5($sModifierProperties);
6542		}
6543		$sQuerySign .= utils::IsArchiveMode() ? '_arch_' : '';
6544
6545		if (!array_key_exists($sQuerySign, self::$aQueryCacheGetObject))
6546		{
6547			// NOTE: Quick and VERY dirty caching mechanism which relies on
6548			//       the fact that the string '987654321' will never appear in the
6549			//       standard query
6550			//       This could be simplified a little, relying solely on the query cache,
6551			//       but this would slow down -by how much time?- the application
6552			$oFilter = new DBObjectSearch($sClass);
6553			$oFilter->AddCondition('id', 987654321, '=');
6554			if ($aModifierProperties)
6555			{
6556				foreach($aModifierProperties as $sPluginClass => $aProperties)
6557				{
6558					foreach($aProperties as $sProperty => $value)
6559					{
6560						$oFilter->SetModifierProperty($sPluginClass, $sProperty, $value);
6561					}
6562				}
6563			}
6564			if ($bAllowAllData)
6565			{
6566				$oFilter->AllowAllData();
6567			}
6568			$oFilter->NoContextParameters();
6569			$sSQL = $oFilter->MakeSelectQuery();
6570			self::$aQueryCacheGetObject[$sQuerySign] = $sSQL;
6571			self::$aQueryCacheGetObjectHits[$sQuerySign] = 0;
6572		}
6573		else
6574		{
6575			$sSQL = self::$aQueryCacheGetObject[$sQuerySign];
6576			self::$aQueryCacheGetObjectHits[$sQuerySign] += 1;
6577		}
6578		$sSQL = str_replace(CMDBSource::Quote(987654321), CMDBSource::Quote($iKey), $sSQL);
6579		$res = CMDBSource::Query($sSQL);
6580
6581		$aRow = CMDBSource::FetchArray($res);
6582		CMDBSource::FreeResult($res);
6583
6584		if ($bMustBeFound && empty($aRow))
6585		{
6586			throw new CoreException("No result for the single row query: '$sSQL'");
6587		}
6588
6589		return $aRow;
6590	}
6591
6592	/**
6593	 * Converts a column name / value array to a {@link DBObject}
6594	 *
6595	 * @param string $sClass
6596	 * @param string[] $aRow column name / value array
6597	 * @param string $sClassAlias
6598	 * @param string[] $aAttToLoad
6599	 * @param array $aExtendedDataSpec
6600	 *
6601	 * @return DBObject
6602	 * @throws CoreUnexpectedValue if finalClass attribute wasn't specified but is needed
6603	 * @throws CoreException if finalClass cannot be found
6604	 */
6605	public static function GetObjectByRow($sClass, $aRow, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null)
6606	{
6607		self::_check_subclass($sClass);
6608
6609		if (strlen($sClassAlias) == 0)
6610		{
6611			$sClassAlias = $sClass;
6612		}
6613
6614		// Compound objects: if available, get the final object class
6615		//
6616		if (!array_key_exists($sClassAlias."finalclass", $aRow))
6617		{
6618			// Either this is a bug (forgot to specify a root class with a finalclass field
6619			// Or this is the expected behavior, because the object is not made of several tables
6620			if (self::IsAbstract($sClass))
6621			{
6622				throw new CoreUnexpectedValue("Querying the abstract '$sClass' class without finalClass attribute");
6623			}
6624			if (self::HasChildrenClasses($sClass))
6625			{
6626				throw new CoreUnexpectedValue("Querying the '$sClass' class without the finalClass attribute, whereas this class has  children");
6627			}
6628		}
6629		elseif (empty($aRow[$sClassAlias."finalclass"]))
6630		{
6631			// The data is missing in the DB
6632			// @#@ possible improvement: check that the class is valid !
6633			$sRootClass = self::GetRootClass($sClass);
6634			$sFinalClassField = self::DBGetClassField($sRootClass);
6635			throw new CoreException("Empty class name for object $sClass::{$aRow["id"]} (root class '$sRootClass', field '{$sFinalClassField}' is empty)");
6636		}
6637		else
6638		{
6639			// do the job for the real target class
6640			$sClass = $aRow[$sClassAlias."finalclass"];
6641		}
6642		return new $sClass($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec);
6643	}
6644
6645	/**
6646	 * Search for the specified class and id.
6647	 *
6648	 * @param string $sClass
6649	 * @param int $iKey id value of the object to retrieve
6650	 * @param bool $bMustBeFound see throws ArchivedObjectException
6651	 * @param bool $bAllowAllData if true then no rights filtering
6652	 * @param null $aModifierProperties
6653	 *
6654	 * @return DBObject|null null if : (the object is not found) or (archive mode disabled and object is archived and
6655	 *     $bMustBeFound=false)
6656	 * @throws CoreException if no result found and $bMustBeFound=true
6657	 * @throws ArchivedObjectException if archive mode disabled and result is archived and $bMustBeFound=true
6658	 * @throws \Exception
6659	 *
6660	 * @see MetaModel::GetObjectWithArchive to get object even if it's archived
6661	 * @see utils::PushArchiveMode() to enable search on archived objects
6662	 */
6663	public static function GetObject($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null)
6664	{
6665		$oObject = self::GetObjectWithArchive($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties);
6666
6667		if (empty($oObject))
6668		{
6669			return null;
6670		}
6671
6672		if (!utils::IsArchiveMode() && $oObject->IsArchived())
6673		{
6674			if ($bMustBeFound)
6675			{
6676				throw new ArchivedObjectException("The object $sClass::$iKey is archived");
6677			}
6678			else
6679			{
6680				return null;
6681			}
6682		}
6683
6684		return $oObject;
6685	}
6686
6687	/**
6688	 * Search for the specified class and id. If the object is archived it will be returned anyway (this is for pre-2.4
6689	 * module compatibility, see N.1108)
6690	 *
6691	 * @param string $sClass
6692	 * @param int $iKey
6693	 * @param bool $bMustBeFound
6694	 * @param bool $bAllowAllData
6695	 * @param array $aModifierProperties
6696	 *
6697	 * @return DBObject|null
6698	 * @throws CoreException if no result found and $bMustBeFound=true
6699	 * @throws \Exception
6700	 *
6701	 * @since 2.4 introduction of the archive functionalities
6702	 *
6703	 * @see MetaModel::GetObject() same but returns null or ArchivedObjectFoundException if object exists but is
6704	 *     archived
6705	 */
6706	public static function GetObjectWithArchive($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null)
6707	{
6708		self::_check_subclass($sClass);
6709
6710		utils::PushArchiveMode(true);
6711		try
6712		{
6713			$aRow = self::MakeSingleRow($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties);
6714		}
6715		catch(Exception $e)
6716		{
6717			// In the finally block we will pop the pushed archived mode
6718			// otherwise the application stays in ArchiveMode true which has caused hazardious behavior!
6719			throw $e;
6720		}
6721		finally
6722		{
6723			utils::PopArchiveMode();
6724		}
6725
6726		if (empty($aRow))
6727		{
6728			return null;
6729		}
6730
6731		return self::GetObjectByRow($sClass, $aRow); // null should not be returned, this is handled in the callee
6732	}
6733
6734	/**
6735	 * @param string $sClass
6736	 * @param string $sName
6737	 * @param bool $bMustBeFound
6738	 *
6739	 * @return \DBObject|null
6740	 * @throws \CoreException
6741	 */
6742	public static function GetObjectByName($sClass, $sName, $bMustBeFound = true)
6743	{
6744		self::_check_subclass($sClass);
6745
6746		$oObjSearch = new DBObjectSearch($sClass);
6747		$oObjSearch->AddNameCondition($sName);
6748		$oSet = new DBObjectSet($oObjSearch);
6749		if ($oSet->Count() != 1)
6750		{
6751			if ($bMustBeFound)
6752			{
6753				throw new CoreException('Failed to get an object by its name', array('class' => $sClass, 'name' => $sName));
6754			}
6755			return null;
6756		}
6757
6758		return $oSet->fetch();
6759	}
6760
6761	/** @var array */
6762	static protected $m_aCacheObjectByColumn = array();
6763
6764	/**
6765	 * @param string $sClass
6766	 * @param string $sAttCode
6767	 * @param $value
6768	 * @param bool $bMustBeFoundUnique
6769	 *
6770	 * @return \DBObject
6771	 * @throws \CoreException
6772	 * @throws \Exception
6773	 */
6774	public static function GetObjectByColumn($sClass, $sAttCode, $value, $bMustBeFoundUnique = true)
6775	{
6776		if (!isset(self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value]))
6777		{
6778			self::_check_subclass($sClass);
6779
6780			$oObjSearch = new DBObjectSearch($sClass);
6781			$oObjSearch->AddCondition($sAttCode, $value, '=');
6782			$oSet = new DBObjectSet($oObjSearch);
6783			if ($oSet->Count() == 1)
6784			{
6785				self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value] = $oSet->fetch();
6786			}
6787			else
6788			{
6789				if ($bMustBeFoundUnique)
6790				{
6791					throw new CoreException('Failed to get an object by column', array('class' => $sClass, 'attcode' => $sAttCode, 'value' => $value, 'matches' => $oSet->Count()));
6792				}
6793				self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value] = null;
6794			}
6795		}
6796
6797		return self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value];
6798	}
6799
6800	/**
6801	 * @param string $sQuery
6802	 * @param array $aParams
6803	 * @param bool $bAllowAllData
6804	 *
6805	 * @return \DBObject
6806	 * @throws \OQLException
6807	 */
6808	public static function GetObjectFromOQL($sQuery, $aParams = null, $bAllowAllData = false)
6809	{
6810		$oFilter = DBObjectSearch::FromOQL($sQuery, $aParams);
6811		if ($bAllowAllData)
6812		{
6813			$oFilter->AllowAllData();
6814		}
6815		$oSet = new DBObjectSet($oFilter);
6816
6817		return $oSet->Fetch();
6818	}
6819
6820	/**
6821	 * @param string $sTargetClass
6822	 * @param int $iKey
6823	 *
6824	 * @return string
6825	 * @throws \ArchivedObjectException
6826	 * @throws \CoreException
6827	 * @throws \DictExceptionMissingString
6828	 * @throws \OQLException
6829	 * @throws \Exception
6830	 */
6831	public static function GetHyperLink($sTargetClass, $iKey)
6832	{
6833		if ($iKey < 0)
6834		{
6835			return "$sTargetClass: $iKey (invalid value)";
6836		}
6837		$oObj = self::GetObject($sTargetClass, $iKey, false);
6838		if (is_null($oObj))
6839		{
6840			// Whatever we are looking for, the root class is the key to search for
6841			$sRootClass = self::GetRootClass($sTargetClass);
6842			$oSearch = DBObjectSearch::FromOQL('SELECT CMDBChangeOpDelete WHERE objclass = :objclass AND objkey = :objkey', array('objclass' => $sRootClass, 'objkey' => $iKey));
6843			$oSet = new DBObjectSet($oSearch);
6844			$oRecord = $oSet->Fetch();
6845			// An empty fname is obtained with iTop < 2.0
6846			if (is_null($oRecord) || (strlen(trim($oRecord->Get('fname'))) == 0))
6847			{
6848				$sName = Dict::Format('Core:UnknownObjectLabel', $sTargetClass, $iKey);
6849				$sTitle = Dict::S('Core:UnknownObjectTip');
6850			}
6851			else
6852			{
6853				$sName = $oRecord->Get('fname');
6854				$sTitle = Dict::Format('Core:DeletedObjectTip', $oRecord->Get('date'), $oRecord->Get('userinfo'));
6855			}
6856			return '<span class="itop-deleted-object" title="'.htmlentities($sTitle, ENT_QUOTES, 'UTF-8').'">'.htmlentities($sName, ENT_QUOTES, 'UTF-8').'</span>';
6857		}
6858		return $oObj->GetHyperLink();
6859	}
6860
6861	/**
6862	 * @param string $sClass
6863	 * @param array|null $aValues array of attcode => value
6864	 *
6865	 * @return DBObject
6866	 * @throws \CoreException
6867	 */
6868	public static function NewObject($sClass, $aValues = null)
6869	{
6870		self::_check_subclass($sClass);
6871		$oRet = new $sClass();
6872		if (is_array($aValues))
6873		{
6874			foreach($aValues as $sAttCode => $value)
6875			{
6876				$oRet->Set($sAttCode, $value);
6877			}
6878		}
6879
6880		return $oRet;
6881	}
6882
6883	/**
6884	 * @param string $sClass
6885	 *
6886	 * @return int
6887	 * @throws \CoreException
6888	 */
6889	public static function GetNextKey($sClass)
6890	{
6891		$sRootClass = MetaModel::GetRootClass($sClass);
6892		$sRootTable = MetaModel::DBGetTable($sRootClass);
6893
6894		return CMDBSource::GetNextInsertId($sRootTable);
6895	}
6896
6897	/**
6898	 * Deletion of records, bypassing {@link DBObject::DBDelete} !!!
6899	 * It is NOT recommended to use this shortcut
6900	 * In particular, it will not work
6901	 *  - if the class is not a final class
6902	 *  - if the class has a hierarchical key (need to rebuild the indexes)
6903	 *  - if the class overload DBDelete !
6904	 *
6905	 * @todo: protect it against forbidden usages (in such a case, delete objects one by one)
6906	 *
6907	 * @param \DBObjectSearch $oFilter
6908	 *
6909	 * @throws \MySQLException
6910	 * @throws \MySQLHasGoneAwayException
6911	 */
6912	public static function BulkDelete(DBObjectSearch $oFilter)
6913	{
6914		$sSQL = $oFilter->MakeDeleteQuery();
6915		if (!self::DBIsReadOnly())
6916		{
6917			CMDBSource::Query($sSQL);
6918		}
6919	}
6920
6921	/**
6922	 * @param DBObjectSearch $oFilter
6923	 * @param array $aValues array of attcode => value
6924	 *
6925	 * @return int Modified objects
6926	 * @throws \MySQLException
6927	 * @throws \MySQLHasGoneAwayException
6928	 */
6929	public static function BulkUpdate(DBObjectSearch $oFilter, array $aValues)
6930	{
6931		// $aValues is an array of $sAttCode => $value
6932		$sSQL = $oFilter->MakeUpdateQuery($aValues);
6933		if (!self::DBIsReadOnly())
6934		{
6935			CMDBSource::Query($sSQL);
6936		}
6937		return CMDBSource::AffectedRows();
6938	}
6939
6940	/**
6941	 * Helper to remove selected objects without calling any handler
6942	 * Surpasses BulkDelete as it can handle abstract classes, but has the other limitation as it bypasses standard
6943	 * objects handlers
6944	 *
6945	 * @param string $oFilter Scope of objects to wipe out
6946	 *
6947	 * @return int The count of deleted objects
6948	 * @throws \CoreException
6949	 */
6950	public static function PurgeData($oFilter)
6951	{
6952		$sTargetClass = $oFilter->GetClass();
6953		$oSet = new DBObjectSet($oFilter);
6954		$oSet->OptimizeColumnLoad(array($sTargetClass => array('finalclass')));
6955		$aIdToClass = $oSet->GetColumnAsArray('finalclass', true);
6956
6957		$aIds = array_keys($aIdToClass);
6958		if (count($aIds) > 0)
6959		{
6960			$aQuotedIds = CMDBSource::Quote($aIds);
6961			$sIdList = implode(',', $aQuotedIds);
6962			$aTargetClasses = array_merge(
6963				self::EnumChildClasses($sTargetClass, ENUM_CHILD_CLASSES_ALL),
6964				self::EnumParentClasses($sTargetClass, ENUM_PARENT_CLASSES_EXCLUDELEAF)
6965			);
6966			foreach($aTargetClasses as $sSomeClass)
6967			{
6968				$sTable = MetaModel::DBGetTable($sSomeClass);
6969				$sPKField = MetaModel::DBGetKey($sSomeClass);
6970
6971				$sDeleteSQL = "DELETE FROM `$sTable` WHERE `$sPKField` IN ($sIdList)";
6972				CMDBSource::DeleteFrom($sDeleteSQL);
6973			}
6974		}
6975		return count($aIds);
6976	}
6977
6978	// Links
6979	//
6980	//
6981	/**
6982	 * @param string $sClass
6983	 *
6984	 * @return array
6985	 * @throws \CoreException
6986	 */
6987	public static function EnumReferencedClasses($sClass)
6988	{
6989		self::_check_subclass($sClass);
6990
6991		// 1-N links (referenced by my class), returns an array of sAttCode=>sClass
6992		$aResult = array();
6993		foreach(self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef)
6994		{
6995			if ($oAttDef->IsExternalKey())
6996			{
6997				$aResult[$sAttCode] = $oAttDef->GetTargetClass();
6998			}
6999		}
7000
7001		return $aResult;
7002	}
7003
7004	/**
7005	 * @param string $sClass
7006	 * @param bool $bSkipLinkingClasses
7007	 * @param bool $bInnerJoinsOnly
7008	 *
7009	 * @return array
7010	 * @throws \CoreException
7011	 */
7012	public static function EnumReferencingClasses($sClass, $bSkipLinkingClasses = false, $bInnerJoinsOnly = false)
7013	{
7014		self::_check_subclass($sClass);
7015
7016		if ($bSkipLinkingClasses)
7017		{
7018			$aLinksClasses = self::EnumLinksClasses();
7019		}
7020
7021		// 1-N links (referencing my class), array of sClass => array of sAttcode
7022		$aResult = array();
7023		foreach(self::$m_aAttribDefs as $sSomeClass => $aClassAttributes)
7024		{
7025			if ($bSkipLinkingClasses && in_array($sSomeClass, $aLinksClasses))
7026			{
7027				continue;
7028			}
7029
7030			$aExtKeys = array();
7031			foreach($aClassAttributes as $sAttCode => $oAttDef)
7032			{
7033				if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass)
7034				{
7035					continue;
7036				}
7037				if ($oAttDef->IsExternalKey() && (self::IsParentClass($oAttDef->GetTargetClass(), $sClass)))
7038				{
7039					if ($bInnerJoinsOnly && $oAttDef->IsNullAllowed())
7040					{
7041						continue;
7042					}
7043					// Ok, I want this one
7044					$aExtKeys[$sAttCode] = $oAttDef;
7045				}
7046			}
7047			if (count($aExtKeys) != 0)
7048			{
7049				$aResult[$sSomeClass] = $aExtKeys;
7050			}
7051		}
7052		return $aResult;
7053	}
7054
7055	/**
7056	 * @deprecated It is not recommended to use this function: call {@link MetaModel::GetLinkClasses} instead !
7057	 * The only difference with EnumLinkingClasses is the output format
7058	 *
7059	 * @return string[] classes having at least two external keys (thus too many classes as compared to GetLinkClasses)
7060	 *
7061	 * @see MetaModel::GetLinkClasses
7062	 */
7063	public static function EnumLinksClasses()
7064	{
7065		// Returns a flat array of classes having at least two external keys
7066		$aResult = array();
7067		foreach(self::$m_aAttribDefs as $sSomeClass => $aClassAttributes)
7068		{
7069			$iExtKeyCount = 0;
7070			foreach($aClassAttributes as $sAttCode => $oAttDef)
7071			{
7072				if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass)
7073				{
7074					continue;
7075				}
7076				if ($oAttDef->IsExternalKey())
7077				{
7078					$iExtKeyCount++;
7079				}
7080			}
7081			if ($iExtKeyCount >= 2)
7082			{
7083				$aResult[] = $sSomeClass;
7084			}
7085		}
7086		return $aResult;
7087	}
7088
7089	/**
7090	 * @deprecated It is not recommended to use this function: call {@link MetaModel::GetLinkClasses} instead !
7091	 * The only difference with EnumLinksClasses is the output format
7092	 *
7093	 * @param string $sClass
7094	 *
7095	 * @return string[] classes having at least two external keys (thus too many classes as compared to GetLinkClasses)
7096	 * @throws \CoreException
7097	 *
7098	 * @see MetaModel::GetLinkClasses
7099	 */
7100	public static function EnumLinkingClasses($sClass = "")
7101	{
7102		// N-N links, array of sLinkClass => (array of sAttCode=>sClass)
7103		$aResult = array();
7104		foreach(self::EnumLinksClasses() as $sSomeClass)
7105		{
7106			$aTargets = array();
7107			$bFoundClass = false;
7108			foreach(self::ListAttributeDefs($sSomeClass) as $sAttCode => $oAttDef)
7109			{
7110				if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass)
7111				{
7112					continue;
7113				}
7114				if ($oAttDef->IsExternalKey())
7115				{
7116					$sRemoteClass = $oAttDef->GetTargetClass();
7117					if (empty($sClass))
7118					{
7119						$aTargets[$sAttCode] = $sRemoteClass;
7120					}
7121					elseif ($sClass == $sRemoteClass)
7122					{
7123						$bFoundClass = true;
7124					}
7125					else
7126					{
7127						$aTargets[$sAttCode] = $sRemoteClass;
7128					}
7129				}
7130			}
7131			if (empty($sClass) || $bFoundClass)
7132			{
7133				$aResult[$sSomeClass] = $aTargets;
7134			}
7135		}
7136		return $aResult;
7137	}
7138
7139	/**
7140	 * This function has two siblings that will be soon deprecated:
7141	 * {@link MetaModel::EnumLinkingClasses} and {@link MetaModel::EnumLinkClasses}
7142	 *
7143	 * Using GetLinkClasses is the recommended way to determine if a class is
7144	 * actually an N-N relation because it is based on the decision made by the
7145	 * designer the data model
7146	 *
7147	 * @return array external key code => target class
7148	 * @throws \CoreException
7149	 */
7150	public static function GetLinkClasses()
7151	{
7152		$aRet = array();
7153		foreach(self::GetClasses() as $sClass)
7154		{
7155			if (isset(self::$m_aClassParams[$sClass]["is_link"]))
7156			{
7157				if (self::$m_aClassParams[$sClass]["is_link"])
7158				{
7159					$aExtKeys = array();
7160					foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
7161					{
7162						if ($oAttDef->IsExternalKey())
7163						{
7164							$aExtKeys[$sAttCode] = $oAttDef->GetTargetClass();
7165						}
7166					}
7167					$aRet[$sClass] = $aExtKeys;
7168				}
7169			}
7170		}
7171
7172		return $aRet;
7173	}
7174
7175	/**
7176	 * @param string $sLinkClass
7177	 * @param string $sAttCode
7178	 *
7179	 * @return string
7180	 * @throws \CoreException
7181	 * @throws \Exception
7182	 */
7183	public static function GetLinkLabel($sLinkClass, $sAttCode)
7184	{
7185		self::_check_subclass($sLinkClass);
7186
7187		// e.g. "supported by" (later: $this->GetLinkLabel(), computed on link data!)
7188		return self::GetLabel($sLinkClass, $sAttCode);
7189	}
7190
7191	/**
7192	 * Replaces all the parameters by the values passed in the hash array
7193	 *
7194	 * @param string $sInput
7195	 * @param array $aParams
7196	 *
7197	 * @return mixed
7198	 */
7199	static public function ApplyParams($sInput, $aParams)
7200	{
7201		$aParams = static::AddMagicPlaceholders($aParams);
7202
7203		// Declare magic parameters
7204		$aParams['APP_URL'] = utils::GetAbsoluteUrlAppRoot();
7205		$aParams['MODULES_URL'] = utils::GetAbsoluteUrlModulesRoot();
7206
7207		$aSearches = array();
7208		$aReplacements = array();
7209		foreach($aParams as $sSearch => $replace)
7210		{
7211			// Some environment parameters are objects, we just need scalars
7212			if (is_object($replace))
7213			{
7214				$iPos = strpos($sSearch, '->object()');
7215				if ($iPos !== false)
7216				{
7217					// Expand the parameters for the object
7218					$sName = substr($sSearch, 0, $iPos);
7219					$aRegExps = array(
7220                        '/(\\$)'.$sName.'-(>|&gt;)([^\\$]+)\\$/', // Support both syntaxes: $this->xxx$ or $this-&gt;xxx$ for HTML compatibility
7221                        '/(%24)'.$sName.'-(>|&gt;)([^%24]+)%24/', // Support for urlencoded in HTML attributes (%20this-&gt;xxx%20)
7222                    );
7223					foreach($aRegExps as $sRegExp)
7224                    {
7225                        if(preg_match_all($sRegExp, $sInput, $aMatches))
7226                        {
7227                            foreach($aMatches[3] as $idx => $sPlaceholderAttCode)
7228                            {
7229                                try
7230                                {
7231                                    $sReplacement = $replace->GetForTemplate($sPlaceholderAttCode);
7232                                    if($sReplacement !== null)
7233                                    {
7234                                        $aReplacements[] = $sReplacement;
7235                                        $aSearches[] = $aMatches[1][$idx] . $sName . '-' . $aMatches[2][$idx] . $sPlaceholderAttCode . $aMatches[1][$idx];
7236                                    }
7237                                }
7238                                catch(Exception $e)
7239                                {
7240                                    // No replacement will occur
7241                                }
7242                            }
7243                        }
7244                    }
7245				}
7246				else
7247				{
7248					continue; // Ignore this non-scalar value
7249				}
7250			}
7251			else
7252			{
7253				$aSearches[] = '$'.$sSearch.'$';
7254				$aReplacements[] = (string)$replace;
7255			}
7256		}
7257		return str_replace($aSearches, $aReplacements, $sInput);
7258	}
7259
7260	/**
7261	 * @param string $sInterface
7262	 *
7263	 * @return array classes=>instance implementing the given interface
7264	 */
7265	public static function EnumPlugins($sInterface)
7266	{
7267		if (array_key_exists($sInterface, self::$m_aExtensionClasses))
7268		{
7269			return self::$m_aExtensionClasses[$sInterface];
7270		}
7271		else
7272		{
7273			return array();
7274		}
7275	}
7276
7277	/**
7278	 * @param string $sInterface
7279	 * @param string $sClassName
7280	 *
7281	 * @return mixed the instance of the specified plug-ins for the given interface
7282	 */
7283	public static function GetPlugins($sInterface, $sClassName)
7284	{
7285		$oInstance = null;
7286		if (array_key_exists($sInterface, self::$m_aExtensionClasses))
7287		{
7288			if (array_key_exists($sClassName, self::$m_aExtensionClasses[$sInterface]))
7289			{
7290				return self::$m_aExtensionClasses[$sInterface][$sClassName];
7291			}
7292		}
7293
7294		return $oInstance;
7295	}
7296
7297	/**
7298	 * @param string $sEnvironment
7299	 *
7300	 * @return array
7301	 */
7302	public static function GetCacheEntries($sEnvironment = null)
7303	{
7304		if (is_null($sEnvironment))
7305		{
7306			$sEnvironment = MetaModel::GetEnvironmentId();
7307		}
7308		$aEntries = array();
7309		$aCacheUserData = apc_cache_info_compat();
7310		if (is_array($aCacheUserData) && isset($aCacheUserData['cache_list']))
7311		{
7312			$sPrefix = 'itop-'.$sEnvironment.'-';
7313
7314			foreach($aCacheUserData['cache_list'] as $i => $aEntry)
7315			{
7316				$sEntryKey = array_key_exists('info', $aEntry) ? $aEntry['info'] : $aEntry['key'];
7317				if (strpos($sEntryKey, $sPrefix) === 0)
7318				{
7319					$sCleanKey = substr($sEntryKey, strlen($sPrefix));
7320					$aEntries[$sCleanKey] = $aEntry;
7321					$aEntries[$sCleanKey]['info'] = $sEntryKey;
7322				}
7323			}
7324		}
7325
7326		return $aEntries;
7327	}
7328
7329	/**
7330	 * @param string $sEnvironmentId
7331	 */
7332	public static function ResetCache($sEnvironmentId = null)
7333	{
7334		if (is_null($sEnvironmentId))
7335		{
7336			$sEnvironmentId = MetaModel::GetEnvironmentId();
7337		}
7338
7339		$sAppIdentity = 'itop-'.$sEnvironmentId;
7340		require_once(APPROOT.'/core/dict.class.inc.php');
7341		Dict::ResetCache($sAppIdentity);
7342
7343		if (function_exists('apc_delete'))
7344		{
7345			foreach(self::GetCacheEntries($sEnvironmentId) as $sKey => $aAPCInfo)
7346			{
7347				$sAPCKey = $aAPCInfo['info'];
7348				apc_delete($sAPCKey);
7349			}
7350		}
7351
7352		require_once(APPROOT.'core/userrights.class.inc.php');
7353		UserRights::FlushPrivileges();
7354	}
7355
7356	/**
7357	 * Given a field spec, get the most relevant (unique) representation
7358	 * Examples for a user request:
7359	 * - friendlyname => ref
7360	 * - org_name => org_id->name
7361	 * - org_id_friendlyname => org_id=>name
7362	 * - caller_name => caller_id->name
7363	 * - caller_id_friendlyname => caller_id->friendlyname
7364	 *
7365	 * @param string $sClass
7366	 * @param string $sField
7367	 *
7368	 * @return string
7369	 * @throws \CoreException
7370	 * @throws \DictExceptionMissingString
7371	 * @throws \Exception
7372	 */
7373	public static function NormalizeFieldSpec($sClass, $sField)
7374	{
7375		$sRet = $sField;
7376
7377		if ($sField == 'id')
7378		{
7379			$sRet = 'id';
7380		}
7381		elseif ($sField == 'friendlyname')
7382		{
7383			$sFriendlyNameAttCode = static::GetFriendlyNameAttributeCode($sClass);
7384			if (!is_null($sFriendlyNameAttCode))
7385			{
7386				// The friendly name is made of a single attribute
7387				$sRet = $sFriendlyNameAttCode;
7388			}
7389		}
7390		else
7391		{
7392			$oAttDef = static::GetAttributeDef($sClass, $sField);
7393			if ($oAttDef->IsExternalField())
7394			{
7395				if ($oAttDef->IsFriendlyName())
7396				{
7397					$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $oAttDef->GetKeyAttCode());
7398					$sRemoteClass = $oKeyAttDef->GetTargetClass();
7399					$sFriendlyNameAttCode = static::GetFriendlyNameAttributeCode($sRemoteClass);
7400					if (is_null($sFriendlyNameAttCode))
7401					{
7402						// The friendly name is made of several attributes
7403						$sRet = $oAttDef->GetKeyAttCode().'->friendlyname';
7404					}
7405					else
7406					{
7407						// The friendly name is made of a single attribute
7408						$sRet = $oAttDef->GetKeyAttCode().'->'.$sFriendlyNameAttCode;
7409					}
7410				}
7411				else
7412				{
7413					$sRet = $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode();
7414				}
7415			}
7416		}
7417		return $sRet;
7418	}
7419
7420}
7421
7422
7423// Standard attribute lists
7424MetaModel::RegisterZList("noneditable", array("description" => "non editable fields", "type" => "attributes"));
7425
7426MetaModel::RegisterZList("details", array("description" => "All attributes to be displayed for the 'details' of an object", "type" => "attributes"));
7427MetaModel::RegisterZList("list", array("description" => "All attributes to be displayed for a list of objects", "type" => "attributes"));
7428MetaModel::RegisterZList("preview", array("description" => "All attributes visible in preview mode", "type" => "attributes"));
7429
7430MetaModel::RegisterZList("standard_search", array("description" => "List of criteria for the standard search", "type" => "filters"));
7431MetaModel::RegisterZList("advanced_search", array("description" => "List of criteria for the advanced search", "type" => "filters"));
7432MetaModel::RegisterZList("default_search", array("description" => "List of criteria displayed by default during search", "type" => "filters"));
7433