1<?php
2// Copyright (C) 2010-2018 Combodo SARL
3//
4//   This file is part of iTop.
5//
6//   iTop is free software; you can redistribute it and/or modify
7//   it under the terms of the GNU Affero General Public License as published by
8//   the Free Software Foundation, either version 3 of the License, or
9//   (at your option) any later version.
10//
11//   iTop is distributed in the hope that it will be useful,
12//   but WITHOUT ANY WARRANTY; without even the implied warranty of
13//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//   GNU Affero General Public License for more details.
15//
16//   You should have received a copy of the GNU Affero General Public License
17//   along with iTop. If not, see <http://www.gnu.org/licenses/>
18
19/**
20 * All objects to be displayed in the application (either as a list or as details)
21 * must implement this interface.
22 */
23interface iDisplay
24{
25
26	/**
27	 * Maps the given context parameter name to the appropriate filter/search code for this class
28	 * @param string $sContextParam Name of the context parameter, i.e. 'org_id'
29	 * @return string Filter code, i.e. 'customer_id'
30	 */
31	public static function MapContextParam($sContextParam);
32	/**
33	 * This function returns a 'hilight' CSS class, used to hilight a given row in a table
34	 * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL,
35	 * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE
36	 * To Be overridden by derived classes
37	 * @param void
38	 * @return String The desired higlight class for the object/row
39	 */
40	public function GetHilightClass();
41	/**
42	 * Returns the relative path to the page that handles the display of the object
43	 * @return string
44	 */
45	public static function GetUIPage();
46	/**
47	 * Displays the details of the object
48	 */
49	public function DisplayDetails(WebPage $oPage, $bEditMode = false);
50}
51
52/**
53 * Class dbObject: the root of persistent classes
54 *
55 * @copyright   Copyright (C) 2010-2016 Combodo SARL
56 * @license     http://opensource.org/licenses/AGPL-3.0
57 */
58
59require_once('metamodel.class.php');
60require_once('deletionplan.class.inc.php');
61require_once('mutex.class.inc.php');
62
63
64/**
65 * A persistent object, as defined by the metamodel
66 *
67 * @package     iTopORM
68 */
69abstract class DBObject implements iDisplay
70{
71	private static $m_aMemoryObjectsByClass = array();
72
73	/** @var array class => array of ('table' => array of (array of <sql_value>)) */
74	private static $m_aBulkInsertItems = array();
75	/** @var array class => array of ('table' => array of <sql_column>) */
76	private static $m_aBulkInsertCols = array();
77  	private static $m_bBulkInsert = false;
78
79	/** @var bool true IIF the object is mapped to a DB record */
80	protected $m_bIsInDB = false;
81	protected $m_iKey = null;
82	private $m_aCurrValues = array();
83	protected $m_aOrigValues = array();
84
85	protected $m_aExtendedData = null;
86
87	private $m_bDirty = false; // Means: "a modification is ongoing"
88										// The object may have incorrect external keys, then any attempt of reload must be avoided
89	/**
90	 * @var boolean|null true if the object has been verified and is consistent with integrity rules
91	 *                   if null, then the check has to be performed again to know the status
92	 * @see CheckToWrite()
93	 */
94	private $m_bCheckStatus = null;
95	/**
96	 * @var null|boolean true if cannot be saved because of security reason
97	 * @see CheckToWrite()
98	 */
99	protected $m_bSecurityIssue = null;
100	/**
101	 * @var null|string[] list of issues preventing object save
102	 * @see CheckToWrite()
103	 */
104	protected $m_aCheckIssues = null;
105	/**
106	 * @var null|string[] list of warnings throws during object save
107	 * @see CheckToWrite()
108	 * @since 2.6 N°659 uniqueness constraints
109	 */
110	protected $m_aCheckWarnings = null;
111	protected $m_aDeleteIssues = null;
112
113	private $m_bFullyLoaded = false; // Compound objects can be partially loaded
114	private $m_aLoadedAtt = array(); // Compound objects can be partially loaded, array of sAttCode
115	protected $m_aTouchedAtt = array(); // list of (potentially) modified sAttCodes
116	protected $m_aModifiedAtt = array(); // real modification status: for each attCode can be: unset => don't know, true => modified, false => not modified (the same value as the original value was set)
117	protected $m_aSynchroData = null; // Set of Synch data related to this object
118	protected $m_sHighlightCode = null;
119	protected $m_aCallbacks = array();
120
121	// Use the MetaModel::NewObject to build an object (do we have to force it?)
122	public function __construct($aRow = null, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null)
123	{
124		if (!empty($aRow))
125		{
126			$this->FromRow($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec);
127			$this->m_bFullyLoaded = $this->IsFullyLoaded();
128			$this->m_aTouchedAtt = array();
129			$this->m_aModifiedAtt = array();
130			return;
131		}
132		// Creation of a brand new object
133		//
134
135		$this->m_iKey = self::GetNextTempId(get_class($this));
136
137		// set default values
138		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
139		{
140			$this->m_aCurrValues[$sAttCode] = $this->GetDefaultValue($sAttCode);
141			$this->m_aOrigValues[$sAttCode] = null;
142			if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
143			{
144				// This field has to be read from the DB
145				// Leave the flag unset (optimization)
146			}
147			else
148			{
149				// No need to trigger a reload for that attribute
150				// Let's consider it as being already fully loaded
151				$this->m_aLoadedAtt[$sAttCode] = true;
152			}
153		}
154
155		$this->UpdateMetaAttributes();
156	}
157
158	/**
159	 * Update meta-attributes depending on the given attribute list
160	 *
161	 * @param array|null $aAttCodes List of att codes
162	 *
163	 * @throws \CoreException
164	 */
165	protected function UpdateMetaAttributes($aAttCodes = null)
166	{
167		if (is_null($aAttCodes))
168		{
169			$aAttCodes = MetaModel::GetAttributesList(get_class($this));
170		}
171		foreach ($aAttCodes as $sAttCode)
172		{
173			foreach (MetaModel::ListMetaAttributes(get_class($this), $sAttCode) as $sMetaAttCode => $oMetaAttDef)
174			{
175				/** @var \AttributeMetaEnum $oMetaAttDef */
176				$this->_Set($sMetaAttCode, $oMetaAttDef->MapValue($this));
177			}
178		}
179	}
180
181	// Read-only <=> Written once (archive)
182	public function RegisterAsDirty()
183	{
184		// While the object may be written to the DB, it is NOT possible to reload it
185		// or at least not possible to reload it the same way
186		$this->m_bDirty = true;
187	}
188
189	public function IsNew()
190	{
191		return (!$this->m_bIsInDB);
192	}
193
194	// Returns an Id for memory objects
195	static protected function GetNextTempId($sClass)
196	{
197		$sRootClass = MetaModel::GetRootClass($sClass);
198		if (!array_key_exists($sRootClass, self::$m_aMemoryObjectsByClass))
199		{
200			self::$m_aMemoryObjectsByClass[$sRootClass] = 0;
201		}
202		self::$m_aMemoryObjectsByClass[$sRootClass]++;
203		return (- self::$m_aMemoryObjectsByClass[$sRootClass]);
204	}
205
206	public function __toString()
207	{
208        $sRet = '';
209        $sClass = get_class($this);
210        $sRootClass = MetaModel::GetRootClass($sClass);
211        $iPKey = $this->GetKey();
212        $sFriendlyname = $this->Get('friendlyname');
213        $sRet .= "<b title=\"$sRootClass\">$sClass</b>::$iPKey ($sFriendlyname)<br/>\n";
214        return $sRet;
215	}
216
217	// Restore initial values... mmmm, to be discussed
218	public function DBRevert()
219	{
220		$this->Reload();
221	}
222
223	protected function IsFullyLoaded()
224	{
225		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
226		{
227			if (!$oAttDef->LoadInObject()) continue;
228			if (!isset($this->m_aLoadedAtt[$sAttCode]) || !$this->m_aLoadedAtt[$sAttCode])
229			{
230				return false;
231			}
232		}
233		return true;
234	}
235
236	/**
237	 * @param bool $bAllowAllData DEPRECATED: the reload must never fail!
238	 *
239	 * @throws CoreException
240	 * @internal
241	 */
242	public function Reload($bAllowAllData = false)
243	{
244		assert($this->m_bIsInDB);
245		$aRow = MetaModel::MakeSingleRow(get_class($this), $this->m_iKey, false /* must be found */, true /* AllowAllData */);
246		if (empty($aRow))
247		{
248			throw new CoreException("Failed to reload object of class '".get_class($this)."', id = ".$this->m_iKey);
249		}
250		$this->FromRow($aRow);
251
252		// Process linked set attributes
253		//
254		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
255		{
256			if (!$oAttDef->IsLinkSet()) continue;
257
258			$this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue($this);
259			$this->m_aOrigValues[$sAttCode] = clone $this->m_aCurrValues[$sAttCode];
260			$this->m_aLoadedAtt[$sAttCode] = true;
261		}
262
263		$this->m_bFullyLoaded = true;
264		$this->m_aTouchedAtt = array();
265		$this->m_aModifiedAtt = array();
266	}
267
268	protected function FromRow($aRow, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null)
269	{
270		if (strlen($sClassAlias) == 0)
271		{
272			// Default to the current class
273			$sClassAlias = get_class($this);
274		}
275
276		$this->m_iKey = null;
277		$this->m_bIsInDB = true;
278		$this->m_aCurrValues = array();
279		$this->m_aOrigValues = array();
280		$this->m_aLoadedAtt = array();
281		$this->m_bCheckStatus = true;
282
283		// Get the key
284		//
285		$sKeyField = $sClassAlias."id";
286		if (!array_key_exists($sKeyField, $aRow))
287		{
288			// #@# Bug ?
289			throw new CoreException("Missing key for class '".get_class($this)."'");
290		}
291
292		$iPKey = $aRow[$sKeyField];
293		if (!self::IsValidPKey($iPKey))
294		{
295			if (is_null($iPKey))
296			{
297				throw new CoreException("Missing object id in query result (found null)");
298			}
299			else
300			{
301				throw new CoreException("An object id must be an integer value ($iPKey)");
302			}
303		}
304		$this->m_iKey = $iPKey;
305
306		// Build the object from an array of "attCode"=>"value")
307		//
308		$bFullyLoaded = true; // ... set to false if any attribute is not found
309		if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad))
310		{
311			$aAttList = MetaModel::ListAttributeDefs(get_class($this));
312		}
313		else
314		{
315			$aAttList = $aAttToLoad[$sClassAlias];
316		}
317
318		foreach($aAttList as $sAttCode=>$oAttDef)
319		{
320			// Skip links (could not be loaded by the mean of this query)
321			if ($oAttDef->IsLinkSet()) continue;
322
323			if (!$oAttDef->LoadInObject()) continue;
324
325			unset($value);
326			$bIsDefined = false;
327			if ($oAttDef->LoadFromDB())
328			{
329				// Note: we assume that, for a given attribute, if it can be loaded,
330				// then one column will be found with an empty suffix, the others have a suffix
331				// Take care: the function isset will return false in case the value is null,
332				// which is something that could happen on open joins
333				$sAttRef = $sClassAlias.$sAttCode;
334
335				if (array_key_exists($sAttRef, $aRow))
336				{
337					$value = $oAttDef->FromSQLToValue($aRow, $sAttRef);
338					$bIsDefined = true;
339				}
340			}
341			else
342			{
343				/** @var \AttributeCustomFields $oAttDef */
344				$value = $oAttDef->ReadValue($this);
345				$bIsDefined = true;
346			}
347
348			if ($bIsDefined)
349			{
350				$this->m_aCurrValues[$sAttCode] = $value;
351				if (is_object($value))
352				{
353					$this->m_aOrigValues[$sAttCode] = clone $value;
354				}
355				else
356				{
357					$this->m_aOrigValues[$sAttCode] = $value;
358				}
359				$this->m_aLoadedAtt[$sAttCode] = true;
360			}
361			else
362			{
363				// This attribute was expected and not found in the query columns
364				$bFullyLoaded = false;
365			}
366		}
367
368		// Load extended data
369		if ($aExtendedDataSpec != null)
370		{
371			$aExtendedDataSpec['table'];
372			foreach($aExtendedDataSpec['fields'] as $sColumn)
373			{
374				$sColRef = $sClassAlias.'_extdata_'.$sColumn;
375				if (array_key_exists($sColRef, $aRow))
376				{
377					$this->m_aExtendedData[$sColumn] = $aRow[$sColRef];
378				}
379			}
380		}
381		return $bFullyLoaded;
382	}
383
384	protected function _Set($sAttCode, $value)
385	{
386		$this->m_aCurrValues[$sAttCode] = $value;
387		$this->m_aTouchedAtt[$sAttCode] = true;
388		unset($this->m_aModifiedAtt[$sAttCode]);
389	}
390
391	public function Set($sAttCode, $value)
392	{
393		if ($sAttCode == 'finalclass')
394		{
395			// Ignore it - this attribute is set upon object creation and that's it
396			return false;
397		}
398
399		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
400
401		if (!$oAttDef->IsWritable())
402		{
403			$sClass = get_class($this);
404			throw new Exception("Attempting to set the value on the read-only attribute $sClass::$sAttCode");
405		}
406
407		if ($this->m_bIsInDB && !$this->m_bFullyLoaded && !$this->m_bDirty)
408		{
409			// First time Set is called... ensure that the object gets fully loaded
410			// Otherwise we would lose the values on a further Reload
411			//           + consistency does not make sense !
412			$this->Reload();
413		}
414
415		if ($oAttDef->IsExternalKey())
416		{
417			if (is_object($value))
418			{
419				// Setting an external key with a whole object (instead of just an ID)
420				// let's initialize also the external fields that depend on it
421				// (useful when building objects in memory and not from a query)
422				/** @var \AttributeExternalKey $oAttDef */
423				if ( (get_class($value) != $oAttDef->GetTargetClass()) && (!is_subclass_of($value, $oAttDef->GetTargetClass())))
424				{
425					throw new CoreUnexpectedValue("Trying to set the value of '$sAttCode', to an object of class '".get_class($value)."', whereas it's an ExtKey to '".$oAttDef->GetTargetClass()."'. Ignored");
426				}
427
428				foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
429				{
430					/** @var \AttributeExternalField $oDef */
431					if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode))
432					{
433						/** @var \DBObject $value */
434						$this->m_aCurrValues[$sCode] = $value->Get($oDef->GetExtAttCode());
435						$this->m_aLoadedAtt[$sCode] = true;
436					}
437				}
438			}
439			else if ($this->m_aCurrValues[$sAttCode] != $value)
440			{
441				// Setting an external key, but no any other information is available...
442				// Invalidate the corresponding fields so that they get reloaded in case they are needed (See Get())
443				foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
444				{
445					/** @var \AttributeExternalKey $oDef */
446					if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode))
447					{
448						$this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode);
449						unset($this->m_aLoadedAtt[$sCode]);
450					}
451				}
452			}
453		}
454		if ($oAttDef->IsLinkSet() && ($value != null))
455		{
456			$realvalue = clone $this->m_aCurrValues[$sAttCode];
457			$realvalue->UpdateFromCompleteList($value);
458		}
459		else
460		{
461			$realvalue = $oAttDef->MakeRealValue($value, $this);
462		}
463		$this->_Set($sAttCode, $realvalue);
464
465		$this->UpdateMetaAttributes(array($sAttCode));
466
467		// The object has changed, reset caches
468		$this->m_bCheckStatus = null;
469
470		// Make sure we do not reload it anymore... before saving it
471		$this->RegisterAsDirty();
472
473		// This function is eligible as a lifecycle action: returning true upon success is a must
474		return true;
475	}
476
477	/**
478	 * @param string $sAttCode
479	 * @param mixed $value
480	 *
481	 * @throws \CoreException
482	 * @throws \CoreUnexpectedValue
483	 * @throws \Exception
484	 * @since 2.6
485	 */
486	public function SetIfNull($sAttCode, $value)
487	{
488		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
489		$oCurrentValue = $this->Get($sAttCode);
490		if ($oAttDef->IsNull($oCurrentValue))
491		{
492			$this->Set($sAttCode, $value);
493		}
494	}
495
496	public function SetTrim($sAttCode, $sValue)
497	{
498		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
499		$iMaxSize = $oAttDef->GetMaxSize();
500		if ($iMaxSize && (strlen($sValue) > $iMaxSize))
501		{
502			$sValue = substr($sValue, 0, $iMaxSize);
503		}
504		$this->Set($sAttCode, $sValue);
505	}
506
507	public function GetLabel($sAttCode)
508	{
509		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
510		return $oAttDef->GetLabel();
511	}
512
513	public function Get($sAttCode)
514	{
515		if (($iPos = strpos($sAttCode, '->')) === false)
516		{
517			return $this->GetStrict($sAttCode);
518		}
519		else
520		{
521			$sExtKeyAttCode = substr($sAttCode, 0, $iPos);
522			$sRemoteAttCode = substr($sAttCode, $iPos + 2);
523			if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode))
524			{
525				throw new CoreException("Unknown external key '$sExtKeyAttCode' for the class ".get_class($this));
526			}
527
528			$oExtFieldAtt = MetaModel::FindExternalField(get_class($this), $sExtKeyAttCode, $sRemoteAttCode);
529			if (!is_null($oExtFieldAtt))
530			{
531				/** @var \AttributeExternalField $oExtFieldAtt */
532				return $this->GetStrict($oExtFieldAtt->GetCode());
533			}
534			else
535			{
536				$oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
537				/** @var \AttributeExternalKey $oKeyAttDef */
538				$sRemoteClass = $oKeyAttDef->GetTargetClass();
539				$oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false);
540				if (is_null($oRemoteObj))
541				{
542					return '';
543				}
544				else
545				{
546					return $oRemoteObj->Get($sRemoteAttCode);
547				}
548			}
549		}
550	}
551
552	public function GetStrict($sAttCode)
553	{
554		if ($sAttCode == 'id')
555		{
556			return $this->m_iKey;
557		}
558
559		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
560
561		if (!$oAttDef->LoadInObject())
562		{
563			$value = $oAttDef->GetValue($this);
564		}
565		else
566		{
567			if (isset($this->m_aLoadedAtt[$sAttCode]))
568			{
569				// Standard case... we have the information directly
570			}
571			elseif ($this->m_bIsInDB && !$this->m_bDirty)
572			{
573				// Lazy load (polymorphism): complete by reloading the entire object
574				// #@# non-scalar attributes.... handle that differently?
575				$oKPI = new ExecutionKPI();
576				$this->Reload();
577				$oKPI->ComputeStats('Reload', get_class($this).'/'.$sAttCode);
578			}
579			elseif ($sAttCode == 'friendlyname')
580			{
581				// The friendly name is not computed and the object is dirty
582				// Todo: implement the computation of the friendly name based on sprintf()
583				//
584				$this->m_aCurrValues[$sAttCode] = '';
585			}
586			else
587			{
588				// Not loaded... is it related to an external key?
589				if ($oAttDef->IsExternalField())
590				{
591					// Let's get the object and compute all of the corresponding attributes
592					// (i.e not only the requested attribute)
593					//
594					/** @var \AttributeExternalField $oAttDef */
595					$sExtKeyAttCode = $oAttDef->GetKeyAttCode();
596
597					if (($iRemote = $this->Get($sExtKeyAttCode)) && ($iRemote > 0)) // Objects in memory have negative IDs
598					{
599						$oExtKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
600						// Note: "allow all data" must be enabled because the external fields are always visible
601						//       to the current user even if this is not the case for the remote object
602						//       This is consistent with the behavior of the lists
603						/** @var \AttributeExternalKey $oExtKeyAttDef */
604						$oRemote = MetaModel::GetObject($oExtKeyAttDef->GetTargetClass(), $iRemote, true, true);
605					}
606					else
607					{
608						$oRemote = null;
609					}
610
611					foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
612					{
613						/** @var \AttributeExternalField $oDef */
614						if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sExtKeyAttCode))
615						{
616							if ($oRemote)
617							{
618								$this->m_aCurrValues[$sCode] = $oRemote->Get($oDef->GetExtAttCode());
619							}
620							else
621							{
622								$this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode);
623							}
624							$this->m_aLoadedAtt[$sCode] = true;
625						}
626					}
627				}
628			}
629			$value = $this->m_aCurrValues[$sAttCode];
630		}
631
632		if ($value instanceof ormLinkSet)
633		{
634			$value->Rewind();
635		}
636		return $value;
637	}
638
639	public function GetOriginal($sAttCode)
640	{
641		if (!array_key_exists($sAttCode, MetaModel::ListAttributeDefs(get_class($this))))
642		{
643			throw new CoreException("Unknown attribute code '$sAttCode' for the class ".get_class($this));
644		}
645		$aOrigValues = $this->m_aOrigValues;
646		return isset($aOrigValues[$sAttCode]) ? $aOrigValues[$sAttCode] : null;
647	}
648
649    /**
650     * Returns the default value of the $sAttCode. By default, returns the default value of the AttributeDefinition.
651     * Overridable.
652     *
653     * @param $sAttCode
654     * @return mixed
655     */
656	public function GetDefaultValue($sAttCode)
657    {
658        $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
659        return $oAttDef->GetDefaultValue($this);
660    }
661
662	/**
663	 * Returns data loaded by the mean of a dynamic and explicit JOIN
664	 */
665	public function GetExtendedData()
666	{
667		return $this->m_aExtendedData;
668	}
669
670	/**
671	 * Set the HighlightCode if the given code has a greater rank than the current HilightCode
672	 * @param string $sCode
673	 * @return void
674	 */
675	protected function SetHighlightCode($sCode)
676	{
677		$aHighlightScale = MetaModel::GetHighlightScale(get_class($this));
678		$fCurrentRank = 0.0;
679		if (($this->m_sHighlightCode !== null) && array_key_exists($this->m_sHighlightCode, $aHighlightScale))
680		{
681			$fCurrentRank = $aHighlightScale[$this->m_sHighlightCode]['rank'];
682		}
683
684		if (array_key_exists($sCode, $aHighlightScale))
685		{
686			$fRank = $aHighlightScale[$sCode]['rank'];
687			if ($fRank > $fCurrentRank)
688			{
689				$this->m_sHighlightCode = $sCode;
690			}
691		}
692	}
693
694	/**
695	 * Get the current HighlightCode
696	 * @return string The Hightlight code (null if none set, meaning rank = 0)
697	 */
698	protected function GetHighlightCode()
699	{
700		return $this->m_sHighlightCode;
701	}
702
703	protected function ComputeHighlightCode()
704	{
705		// First if the state defines a HiglightCode, apply it
706		$sState = $this->GetState();
707		if ($sState != '')
708		{
709			$sCode = MetaModel::GetHighlightCode(get_class($this), $sState);
710			$this->SetHighlightCode($sCode);
711		}
712		// The check for each StopWatch if a HighlightCode is effective
713		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
714		{
715			if ($oAttDef instanceof AttributeStopWatch)
716			{
717				$oStopWatch = $this->Get($sAttCode);
718				$sCode = $oStopWatch->GetHighlightCode();
719				if ($sCode !== '')
720				{
721					$this->SetHighlightCode($sCode);
722				}
723			}
724		}
725		return $this->GetHighlightCode();
726	}
727
728	/**
729	 * Updates the value of an external field by (re)loading the object
730	 * corresponding to the external key and getting the value from it
731	 *
732	 * UNUSED ?
733	 *
734	 * @param string $sAttCode Attribute code of the external field to update
735	 * @return void
736	 */
737	protected function UpdateExternalField($sAttCode)
738	{
739		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
740		if ($oAttDef->IsExternalField())
741		{
742			/** @var \AttributeExternalField $oAttDef */
743			$sTargetClass = $oAttDef->GetTargetClass();
744			$objkey = $this->Get($oAttDef->GetKeyAttCode());
745			// Note: "allow all data" must be enabled because the external fields are always visible
746			//       to the current user even if this is not the case for the remote object
747			//       This is consistent with the behavior of the lists
748			$oObj = MetaModel::GetObject($sTargetClass, $objkey, true, true);
749			if (is_object($oObj))
750			{
751				$value = $oObj->Get($oAttDef->GetExtAttCode());
752				$this->Set($sAttCode, $value);
753			}
754		}
755	}
756
757	/**
758	 * Overridable callback, called by \DBObject::DoComputeValues
759	 *
760	 * @api
761	 */
762	public function ComputeValues()
763	{
764	}
765
766	/**
767	 * Compute scalar attributes that depend on any other type of attribute
768	 *
769	 * @throws \CoreException
770	 * @throws \CoreUnexpectedValue
771	 *
772	 * @internal
773	 */
774	final public function DoComputeValues()
775	{
776		// TODO - use a flag rather than checking the call stack -> this will certainly accelerate things
777
778		// First check that we are not currently computing the fields
779		// (yes, we need to do some things like Set/Get to compute the fields which will in turn trigger the update...)
780		foreach (debug_backtrace() as $aCallInfo)
781		{
782			if (!array_key_exists("class", $aCallInfo)) continue;
783			if ($aCallInfo["class"] != get_class($this)) continue;
784			if ($aCallInfo["function"] != "ComputeValues") continue;
785			return; //skip!
786		}
787
788		// Set the "null-not-allowed" datetimes (and dates) whose value is not initialized
789		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
790		{
791			// AttributeDate is derived from AttributeDateTime
792			if (($oAttDef instanceof AttributeDateTime) && (!$oAttDef->IsNullAllowed()) && ($this->Get($sAttCode) == $oAttDef->GetNullValue()))
793			{
794				$this->Set($sAttCode, date($oAttDef->GetInternalFormat()));
795			}
796		}
797
798		$this->ComputeValues();
799	}
800
801	public function GetAsHTML($sAttCode, $bLocalize = true)
802	{
803		$sClass = get_class($this);
804		$oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode);
805
806		if ($oAtt->IsExternalKey(EXTKEY_ABSOLUTE))
807		{
808			//return $this->Get($sAttCode.'_friendlyname');
809			/** @var \AttributeExternalKey $oAtt */
810			$sTargetClass = $oAtt->GetTargetClass(EXTKEY_ABSOLUTE);
811			$iTargetKey = $this->Get($sAttCode);
812			if ($iTargetKey < 0)
813			{
814				// the key points to an object that exists only in memory... no hyperlink points to it yet
815				return '';
816			}
817			else
818			{
819				$sHtmlLabel = htmlentities($this->Get($sAttCode.'_friendlyname'), ENT_QUOTES, 'UTF-8');
820				$bArchived = $this->IsArchived($sAttCode);
821				$bObsolete = $this->IsObsolete($sAttCode);
822				return $this->MakeHyperLink($sTargetClass, $iTargetKey, $sHtmlLabel, null, true, $bArchived, $bObsolete);
823			}
824		}
825
826		// That's a standard attribute (might be an ext field or a direct field, etc.)
827		return $oAtt->GetAsHTML($this->Get($sAttCode), $this, $bLocalize);
828	}
829
830	public function GetEditValue($sAttCode)
831	{
832		$sClass = get_class($this);
833		$oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode);
834
835		if ($oAtt->IsExternalKey())
836		{
837			/** @var \AttributeExternalKey $oAtt */
838			$sTargetClass = $oAtt->GetTargetClass();
839			if ($this->IsNew())
840			{
841				// The current object exists only in memory, don't try to query it in the DB !
842				// instead let's query for the object pointed by the external key, and get its name
843				$targetObjId = $this->Get($sAttCode);
844				$oTargetObj = MetaModel::GetObject($sTargetClass, $targetObjId, false); // false => not sure it exists
845				if (is_object($oTargetObj))
846				{
847					$sEditValue = $oTargetObj->GetName();
848				}
849				else
850				{
851					$sEditValue = 0;
852				}
853			}
854			else
855			{
856				$sEditValue = $this->Get($sAttCode.'_friendlyname');
857			}
858		}
859		else
860		{
861			$sEditValue = $oAtt->GetEditValue($this->Get($sAttCode), $this);
862		}
863		return $sEditValue;
864	}
865
866	public function GetAsXML($sAttCode, $bLocalize = true)
867	{
868		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
869		return $oAtt->GetAsXML($this->Get($sAttCode), $this, $bLocalize);
870	}
871
872	public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false)
873	{
874		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
875		return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText);
876	}
877
878	public function GetOriginalAsHTML($sAttCode, $bLocalize = true)
879	{
880		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
881		return $oAtt->GetAsHTML($this->GetOriginal($sAttCode), $this, $bLocalize);
882	}
883
884	public function GetOriginalAsXML($sAttCode, $bLocalize = true)
885	{
886		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
887		return $oAtt->GetAsXML($this->GetOriginal($sAttCode), $this, $bLocalize);
888	}
889
890	public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false)
891	{
892		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
893		return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText);
894	}
895
896    /**
897     * @param string $sObjClass
898     * @param string $sObjKey
899     * @param string $sHtmlLabel Label with HTML entities escaped (< escaped as &lt;)
900     * @param null $sUrlMakerClass
901     * @param bool|true $bWithNavigationContext
902     * @param bool|false $bArchived
903     * @param bool|false $bObsolete
904     *
905     * @return string
906     * @throws \ArchivedObjectException
907     * @throws \CoreException
908     * @throws \DictExceptionMissingString
909     */
910	public static function MakeHyperLink($sObjClass, $sObjKey, $sHtmlLabel = '', $sUrlMakerClass = null, $bWithNavigationContext = true, $bArchived = false, $bObsolete = false)
911	{
912		if ($sObjKey <= 0) return '<em>'.Dict::S('UI:UndefinedObject').'</em>'; // Objects built in memory have negative IDs
913
914		// Safety net
915		//
916		if (empty($sHtmlLabel))
917		{
918			// If the object if not issued from a query but constructed programmatically
919			// the label may be empty. In this case run a query to get the object's friendly name
920			$oTmpObj = MetaModel::GetObject($sObjClass, $sObjKey, false);
921			if (is_object($oTmpObj))
922			{
923				$sHtmlLabel = $oTmpObj->GetName();
924			}
925			else
926			{
927				// May happen in case the target object is not in the list of allowed values for this attribute
928				$sHtmlLabel = "<em>$sObjClass::$sObjKey</em>";
929			}
930		}
931		$sHint = MetaModel::GetName($sObjClass)."::$sObjKey";
932		$sUrl = ApplicationContext::MakeObjectUrl($sObjClass, $sObjKey, $sUrlMakerClass, $bWithNavigationContext);
933
934		$bClickable = !$bArchived || utils::IsArchiveMode();
935		if ($bArchived)
936		{
937			$sSpanClass = 'archived';
938			$sFA = 'fa-archive object-archived';
939			$sHint = Dict::S('ObjectRef:Archived');
940		}
941		elseif ($bObsolete)
942		{
943			$sSpanClass = 'obsolete';
944			$sFA = 'fa-eye-slash object-obsolete';
945			$sHint = Dict::S('ObjectRef:Obsolete');
946		}
947		else
948		{
949			$sSpanClass = '';
950			$sFA = '';
951		}
952		if ($sFA == '')
953		{
954			$sIcon = '';
955		}
956		else
957		{
958			if ($bClickable)
959			{
960				$sIcon = "<span class=\"object-ref-icon fa $sFA fa-1x fa-fw\"></span>";
961			}
962			else
963			{
964				$sIcon = "<span class=\"object-ref-icon-disabled fa $sFA fa-1x fa-fw\"></span>";
965			}
966		}
967
968		if ($bClickable && (strlen($sUrl) > 0))
969		{
970			$sHLink = "<a class=\"object-ref-link\" href=\"$sUrl\">$sIcon$sHtmlLabel</a>";
971		}
972		else
973		{
974			$sHLink = $sIcon.$sHtmlLabel;
975		}
976		$sRet = "<span class=\"object-ref $sSpanClass\" title=\"$sHint\">$sHLink</span>";
977		return $sRet;
978	}
979
980    /**
981     * @param string $sUrlMakerClass
982     * @param bool $bWithNavigationContext
983     * @param string $sLabel
984     *
985     * @return string
986     * @throws \DictExceptionMissingString
987     */
988	public function GetHyperlink($sUrlMakerClass = null, $bWithNavigationContext = true, $sLabel = null)
989	{
990	    if($sLabel === null)
991        {
992            $sLabel = $this->GetName();
993        }
994		$bArchived = $this->IsArchived();
995		$bObsolete = $this->IsObsolete();
996		return self::MakeHyperLink(get_class($this), $this->GetKey(), $sLabel, $sUrlMakerClass, $bWithNavigationContext, $bArchived, $bObsolete);
997	}
998
999	public static function ComputeStandardUIPage($sClass)
1000	{
1001		static $aUIPagesCache = array(); // Cache to store the php page used to display each class of object
1002		if (!isset($aUIPagesCache[$sClass]))
1003		{
1004			$UIPage = false;
1005			if (is_callable("$sClass::GetUIPage"))
1006			{
1007				$UIPage = eval("return $sClass::GetUIPage();"); // May return false in case of error
1008			}
1009			$aUIPagesCache[$sClass] = $UIPage === false ? './UI.php' : $UIPage;
1010		}
1011		$sPage = $aUIPagesCache[$sClass];
1012		return $sPage;
1013	}
1014
1015	public static function GetUIPage()
1016	{
1017		return 'UI.php';
1018	}
1019
1020
1021	// could be in the metamodel ?
1022	public static function IsValidPKey($value)
1023	{
1024		return ((string)$value === (string)(int)$value);
1025	}
1026
1027	public function GetKey()
1028	{
1029		return $this->m_iKey;
1030	}
1031	public function SetKey($iNewKey)
1032	{
1033		if (!self::IsValidPKey($iNewKey))
1034		{
1035			throw new CoreException("An object id must be an integer value ($iNewKey)");
1036		}
1037
1038		if ($this->m_bIsInDB && !empty($this->m_iKey) && ($this->m_iKey != $iNewKey))
1039		{
1040			throw new CoreException("Changing the key ({$this->m_iKey} to $iNewKey) on an object (class {".get_class($this).") wich already exists in the Database");
1041		}
1042		$this->m_iKey = $iNewKey;
1043	}
1044	/**
1045	 * Get the icon representing this object
1046	 * @param boolean $bImgTag If true the result is a full IMG tag (or an emtpy string if no icon is defined)
1047	 * @return string Either the full IMG tag ($bImgTag == true) or just the URL to the icon file
1048	 */
1049	public function GetIcon($bImgTag = true)
1050	{
1051		$sCode = $this->ComputeHighlightCode();
1052		if($sCode != '')
1053		{
1054			$aHighlightScale = MetaModel::GetHighlightScale(get_class($this));
1055			if (array_key_exists($sCode, $aHighlightScale))
1056			{
1057				$sIconUrl = $aHighlightScale[$sCode]['icon'];
1058				if($bImgTag)
1059				{
1060					return "<img src=\"$sIconUrl\" style=\"vertical-align:middle\"/>";
1061				}
1062				else
1063				{
1064					return $sIconUrl;
1065				}
1066			}
1067		}
1068		return MetaModel::GetClassIcon(get_class($this), $bImgTag);
1069	}
1070
1071	/**
1072	 * Get the name as defined in the dictionary
1073	 * @return string (empty for default name scheme)
1074	 */
1075	public static function GetClassName($sClass)
1076	{
1077		$sStringCode = 'Class:'.$sClass;
1078		return Dict::S($sStringCode, str_replace('_', ' ', $sClass));
1079	}
1080
1081	/**
1082	 * Get the description as defined in the dictionary
1083	 * @param string $sClass
1084	 *
1085	 * @return string
1086	 */
1087	final static public function GetClassDescription($sClass)
1088	{
1089		$sStringCode = 'Class:'.$sClass.'+';
1090		return Dict::S($sStringCode, '');
1091	}
1092
1093	/**
1094	 * Gets the name of an object in a safe manner for displaying inside a web page
1095	 *
1096	 * @return string
1097	 * @throws \CoreException
1098	 */
1099	public function GetName()
1100	{
1101		return htmlentities($this->GetRawName(), ENT_QUOTES, 'UTF-8');
1102	}
1103
1104	/**
1105	 * Gets the raw name of an object, this is not safe for displaying inside a web page
1106	 * since the " < > characters are not escaped and the name may contain some XSS script
1107	 * instructions.
1108	 * Use this function only for internal computations or for an output to a non-HTML destination
1109	 *
1110	 * @return string
1111	 * @throws \CoreException
1112	 */
1113	public function GetRawName()
1114	{
1115		return $this->Get('friendlyname');
1116	}
1117
1118	/**
1119	 * @return mixed|string '' if no state attribute, object representing its value otherwise
1120	 * @throws \CoreException
1121	 * @internal
1122	 */
1123	public function GetState()
1124	{
1125		$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
1126		if (empty($sStateAttCode))
1127		{
1128			return '';
1129		}
1130		else
1131		{
1132			return $this->Get($sStateAttCode);
1133		}
1134	}
1135
1136	public function GetStateLabel()
1137	{
1138		$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
1139		if (empty($sStateAttCode))
1140		{
1141			return '';
1142		}
1143		else
1144		{
1145			$sStateValue = $this->Get($sStateAttCode);
1146			return MetaModel::GetStateLabel(get_class($this), $sStateValue);
1147		}
1148	}
1149
1150	public function GetStateDescription()
1151	{
1152		$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
1153		if (empty($sStateAttCode))
1154		{
1155			return '';
1156		}
1157		else
1158		{
1159			$sStateValue = $this->Get($sStateAttCode);
1160			return MetaModel::GetStateDescription(get_class($this), $sStateValue);
1161		}
1162	}
1163
1164	/**
1165	 * Overridable - Define attributes read-only from the end-user perspective
1166	 *
1167	 * @return array List of attcodes
1168	 */
1169	public static function GetReadOnlyAttributes()
1170	{
1171		return null;
1172	}
1173
1174
1175	/**
1176	 * Overridable - Get predefined objects (could be hardcoded)
1177	 * The predefined objects will be synchronized with the DB at each install/upgrade
1178	 * As soon as a class has predefined objects, then nobody can create nor delete objects
1179	 * @return array An array of id => array of attcode => php value(so-called "real value": integer, string, ormDocument, DBObjectSet, etc.)
1180	 */
1181	public static function GetPredefinedObjects()
1182	{
1183		return null;
1184	}
1185
1186	/**
1187	 * @param string $sAttCode $sAttCode The code of the attribute
1188	 * @param array $aReasons To store the reasons why the attribute is read-only (info about the synchro replicas)
1189	 * @param string $sTargetState The target state in which to evalutate the flags, if empty the current state will be
1190	 *     used
1191	 *
1192	 * @return integer the binary combination of flags for the given attribute in the given state of the object<br>
1193	 *         Values can be one of the OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY, ... (see define in metamodel.class.php)
1194	 * @throws \CoreException
1195	 *
1196	 * @api
1197	 *
1198	 * @see GetInitialStateAttributeFlags for creation
1199	 */
1200	public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '')
1201	{
1202		$iFlags = 0; // By default (if no life cycle) no flag at all
1203
1204		$aReadOnlyAtts = $this->GetReadOnlyAttributes();
1205		if (($aReadOnlyAtts != null) && (in_array($sAttCode, $aReadOnlyAtts)))
1206		{
1207			return OPT_ATT_READONLY;
1208		}
1209
1210		$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
1211		if (!empty($sStateAttCode))
1212		{
1213			if ($sTargetState != '')
1214			{
1215				$iFlags = MetaModel::GetAttributeFlags(get_class($this), $sTargetState, $sAttCode);
1216			}
1217			else
1218			{
1219				$iFlags = MetaModel::GetAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode);
1220			}
1221		}
1222		$aReasons = array();
1223		$iSynchroFlags = 0;
1224		if ($this->InSyncScope())
1225		{
1226			$iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
1227			if ($iSynchroFlags & OPT_ATT_SLAVE)
1228			{
1229				$iSynchroFlags |= OPT_ATT_READONLY;
1230			}
1231		}
1232		return $iFlags | $iSynchroFlags; // Combine both sets of flags
1233	}
1234
1235	/**
1236	 * @param string $sAttCode
1237	 * @param array $aReasons To store the reasons why the attribute is read-only (info about the synchro replicas)
1238	 *
1239	 * @throws \CoreException
1240	 */
1241	public function IsAttributeReadOnlyForCurrentState($sAttCode, &$aReasons = array())
1242	{
1243		$iAttFlags = $this->GetAttributeFlags($sAttCode, $aReasons);
1244
1245		return ($iAttFlags & OPT_ATT_READONLY);
1246	}
1247
1248    /**
1249     * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...)
1250     * for the given attribute in a transition
1251     * @param $sAttCode string $sAttCode The code of the attribute
1252     * @param $sStimulus string The stimulus code to apply
1253     * @param $aReasons array To store the reasons why the attribute is read-only (info about the synchro replicas)
1254     * @param $sOriginState string The state from which to apply $sStimulus, if empty current state will be used
1255     * @return integer Flags: the binary combination of the flags applicable to this attribute
1256     */
1257    public function GetTransitionFlags($sAttCode, $sStimulus, &$aReasons = array(), $sOriginState = '')
1258    {
1259        $iFlags = 0; // By default (if no lifecycle) no flag at all
1260
1261        $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
1262        // If no state attribute, there is no lifecycle
1263        if (empty($sStateAttCode))
1264        {
1265            return $iFlags;
1266        }
1267
1268        // Retrieving current state if necessary
1269        if ($sOriginState === '')
1270        {
1271            $sOriginState = $this->Get($sStateAttCode);
1272        }
1273
1274        // Retrieving attribute flags
1275        $iAttributeFlags = $this->GetAttributeFlags($sAttCode, $aReasons, $sOriginState);
1276
1277        // Retrieving transition flags
1278        $iTransitionFlags = MetaModel::GetTransitionFlags(get_class($this), $sOriginState, $sStimulus, $sAttCode);
1279
1280        // Merging transition flags with attribute flags
1281        $iFlags = $iTransitionFlags | $iAttributeFlags;
1282
1283        return $iFlags;
1284    }
1285
1286    /**
1287     * Returns an array of attribute codes (with their flags) when $sStimulus is applied on the object in the $sOriginState state.
1288     * Note: Attributes (and flags) from the target state and the transition are combined.
1289     *
1290     * @param $sStimulus string
1291     * @param $sOriginState string Default is current state
1292     * @return array
1293     */
1294    public function GetTransitionAttributes($sStimulus, $sOriginState = null)
1295    {
1296        $sObjClass = get_class($this);
1297
1298        // Defining current state as origin state if not specified
1299        if($sOriginState === null)
1300        {
1301            $sOriginState = $this->GetState();
1302        }
1303
1304        $aAttributes = MetaModel::GetTransitionAttributes($sObjClass, $sStimulus, $sOriginState);
1305
1306        return $aAttributes;
1307    }
1308
1309	/**
1310	 * @param string $sAttCode The code of the attribute
1311	 * @param array $aReasons
1312	 *
1313	 * @return integer The binary combination of the flags for the given attribute for the current state of the object
1314	 *         considered as an INITIAL state.<br>
1315	 *         Values can be one of the OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY, ... (see define in metamodel.class.php)
1316	 * @throws \CoreException
1317	 *
1318	 * @api
1319	 *
1320	 * @see GetAttributeFlags when modifying the object
1321	 */
1322	public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = array())
1323	{
1324		$iFlags = 0;
1325		$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
1326		if (!empty($sStateAttCode))
1327		{
1328			$iFlags = MetaModel::GetInitialStateAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode);
1329		}
1330		return $iFlags; // No need to care about the synchro flags since we'll be creating a new object anyway
1331	}
1332
1333	/**
1334	 * Check if the given (or current) value is suitable for the attribute
1335	 *
1336	 * @param $sAttCode
1337	 * @param boolean|string $value true if successfull, the error desciption otherwise
1338	 *
1339	 * @return bool|string
1340	 * @throws \ArchivedObjectException
1341	 * @throws \CoreException
1342	 * @throws \OQLException
1343	 *
1344	 * @internal
1345	 */
1346	public function CheckValue($sAttCode, $value = null)
1347	{
1348		if (!is_null($value))
1349		{
1350			$toCheck = $value;
1351		}
1352		else
1353		{
1354			$toCheck = $this->Get($sAttCode);
1355		}
1356
1357		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
1358		if (!$oAtt->IsWritable())
1359		{
1360			return true;
1361		}
1362		elseif ($oAtt->IsNull($toCheck))
1363		{
1364			if ($oAtt->IsNullAllowed())
1365			{
1366				return true;
1367			}
1368			else
1369			{
1370				return "Null not allowed";
1371			}
1372		}
1373		elseif ($oAtt->IsExternalKey())
1374		{
1375			if (!MetaModel::SkipCheckExtKeys())
1376			{
1377				/** @var \AttributeExternalKey $oAtt */
1378				$sTargetClass = $oAtt->GetTargetClass();
1379				$oTargetObj = MetaModel::GetObject($sTargetClass, $toCheck, false /*must be found*/, true /*allow all data*/);
1380				if (is_null($oTargetObj))
1381				{
1382					return "Target object not found ($sTargetClass::$toCheck)";
1383				}
1384			}
1385			if ($oAtt->IsHierarchicalKey())
1386			{
1387				// This check cannot be deactivated since otherwise the user may break things by a CSV import of a bulk modify
1388				$aValues = $oAtt->GetAllowedValues(array('this' => $this));
1389				if (!array_key_exists($toCheck, $aValues))
1390				{
1391					return "Value not allowed [$toCheck]";
1392				}
1393			}
1394		}
1395		elseif ($oAtt instanceof AttributeTagSet)
1396		{
1397			if (is_string($toCheck))
1398			{
1399				$oTag = new ormTagSet(get_class($this), $sAttCode, $oAtt->GetMaxItems());
1400				try
1401				{
1402					$oTag->SetValues(explode(' ', $toCheck));
1403				} catch (Exception $e)
1404				{
1405					return "Tag value '$toCheck' is not a valid tag list";
1406				}
1407
1408				return true;
1409			}
1410
1411			if ($toCheck instanceof ormTagSet)
1412			{
1413				return true;
1414			}
1415
1416			return "Bad type";
1417		}
1418		elseif ($oAtt instanceof AttributeClassAttCodeSet)
1419		{
1420			if (is_string($toCheck))
1421			{
1422				$oTag = new ormSet(get_class($this), $sAttCode, $oAtt->GetMaxItems());
1423				try
1424				{
1425					$aValues = array();
1426					foreach(explode(',', $toCheck) as $sValue)
1427					{
1428						$aValues[] = trim($sValue);
1429					}
1430					$oTag->SetValues($aValues);
1431				} catch (Exception $e)
1432				{
1433					return "Set value '$toCheck' is not a valid set";
1434				}
1435
1436				return true;
1437			}
1438
1439			if ($toCheck instanceof ormSet)
1440			{
1441				return true;
1442			}
1443
1444			return "Bad type";
1445		}
1446		elseif ($oAtt->IsScalar())
1447		{
1448			$aValues = $oAtt->GetAllowedValues($this->ToArgsForQuery());
1449			if (is_array($aValues) && (count($aValues) > 0))
1450			{
1451				if (!array_key_exists($toCheck, $aValues))
1452				{
1453					return "Value not allowed [$toCheck]";
1454				}
1455			}
1456			if (!is_null($iMaxSize = $oAtt->GetMaxSize()))
1457			{
1458				$iLen = strlen($toCheck);
1459				if ($iLen > $iMaxSize)
1460				{
1461					return "String too long (found $iLen, limited to $iMaxSize)";
1462				}
1463			}
1464			if (!$oAtt->CheckFormat($toCheck))
1465			{
1466				return "Wrong format [$toCheck]";
1467			}
1468		}
1469		else
1470		{
1471			return $oAtt->CheckValue($this, $toCheck);
1472		}
1473		return true;
1474	}
1475
1476	/**
1477	 * check attributes together
1478	 *
1479	 * @return bool
1480	 * @api
1481	 */
1482	public function CheckConsistency()
1483	{
1484		return true;
1485	}
1486
1487	/**
1488	 * @throws \CoreException
1489	 * @throws \OQLException
1490	 * @since 2.6 N°659 uniqueness constraint
1491	 */
1492	protected function DoCheckUniqueness()
1493	{
1494		$sCurrentClass = get_class($this);
1495		$aUniquenessRules = MetaModel::GetUniquenessRules($sCurrentClass);
1496
1497		foreach ($aUniquenessRules as $sUniquenessRuleId => $aUniquenessRuleProperties)
1498		{
1499			if ($aUniquenessRuleProperties['disabled'] === true)
1500			{
1501				continue;
1502			}
1503
1504			$bHasDuplicates = $this->HasObjectsInDbForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties);
1505			if ($bHasDuplicates)
1506			{
1507				$bIsBlockingRule = $aUniquenessRuleProperties['is_blocking'];
1508				if (is_null($bIsBlockingRule))
1509				{
1510					$bIsBlockingRule = true;
1511				}
1512
1513				$sErrorMessage = $this->GetUniquenessRuleMessage($sUniquenessRuleId);
1514
1515				if ($bIsBlockingRule)
1516				{
1517					$this->m_aCheckIssues[] = $sErrorMessage;
1518					continue;
1519				}
1520				$this->m_aCheckWarnings[] = $sErrorMessage;
1521				continue;
1522			}
1523		}
1524	}
1525
1526	/**
1527	 * @param string $sUniquenessRuleId
1528	 *
1529	 * @return string dict key : Class:$sClassName/UniquenessRule:$sUniquenessRuleId
1530	 *          if none then will use Core:UniquenessDefaultError
1531	 *         Dictionary keys can contain "$this" placeholders
1532	 *
1533	 * @since 2.6 N°659 uniqueness constraint
1534	 */
1535	protected function GetUniquenessRuleMessage($sUniquenessRuleId)
1536	{
1537		$sCurrentClass = get_class($this);
1538		$sClass = MetaModel::GetRootClassForUniquenessRule($sUniquenessRuleId, $sCurrentClass);
1539		$sMessageKey = "Class:$sClass/UniquenessRule:$sUniquenessRuleId";
1540		$sTemplate = Dict::S($sMessageKey, '');
1541
1542		if (empty($sTemplate))
1543		{
1544			// we could add also a specific message if user is admin ("dict key is missing")
1545			return Dict::Format('Core:UniquenessDefaultError', $sUniquenessRuleId);
1546		}
1547
1548		$oString = new TemplateString($sTemplate);
1549
1550		return $oString->Render(array('this' => $this));
1551	}
1552
1553	/**
1554	 * @param string $sUniquenessRuleId uniqueness rule ID
1555	 * @param array $aUniquenessRuleProperties uniqueness rule properties
1556	 *
1557	 * @return bool
1558	 * @throws \CoreException
1559	 * @throws \MissingQueryArgument
1560	 * @throws \MySQLException
1561	 * @throws \MySQLHasGoneAwayException
1562	 * @throws \OQLException
1563	 */
1564	protected function HasObjectsInDbForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties)
1565	{
1566		$oUniquenessQuery = $this->GetSearchForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties);
1567		$oUniquenessDuplicates = new DBObjectSet($oUniquenessQuery);
1568		$bHasDuplicates = $oUniquenessDuplicates->CountExceeds(0);
1569
1570		return $bHasDuplicates;
1571	}
1572
1573	/**
1574	 * @param string $sUniquenessRuleId uniqueness rule ID
1575	 * @param array $aUniquenessRuleProperties uniqueness rule properties
1576	 *
1577	 * @return \DBSearch
1578	 * @throws \CoreException
1579	 * @throws \OQLException
1580	 * @since 2.6 N°659 uniqueness constraint
1581	 */
1582	protected function GetSearchForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties)
1583	{
1584		$sRuleRootClass = $aUniquenessRuleProperties['root_class'];
1585		$sOqlUniquenessQuery = "SELECT $sRuleRootClass";
1586		if (!(empty($sUniquenessFilter = $aUniquenessRuleProperties['filter'])))
1587		{
1588			$sOqlUniquenessQuery .= ' WHERE '.$sUniquenessFilter;
1589		}
1590		/** @var \DBObjectSearch $oUniquenessQuery */
1591		$oUniquenessQuery = DBObjectSearch::FromOQL($sOqlUniquenessQuery);
1592
1593		if (!$this->IsNew())
1594		{
1595			$oUniquenessQuery->AddCondition('id', $this->GetKey(), '<>');
1596		}
1597
1598		foreach ($aUniquenessRuleProperties['attributes'] as $sAttributeCode)
1599		{
1600			$attributeValue = $this->Get($sAttributeCode);
1601			$oUniquenessQuery->AddCondition($sAttributeCode, $attributeValue, '=');
1602		}
1603
1604		$aChildClassesWithRuleDisabled = MetaModel::GetChildClassesWithDisabledUniquenessRule($sRuleRootClass, $sUniquenessRuleId);
1605		if (!empty($aChildClassesWithRuleDisabled))
1606		{
1607			$oUniquenessQuery->AddConditionForInOperatorUsingParam('finalclass', $aChildClassesWithRuleDisabled, false);
1608		}
1609
1610		return $oUniquenessQuery;
1611	}
1612
1613	/**
1614	 * Check integrity rules (before inserting or updating the object)
1615	 *
1616	 * Errors should be inserted in {@link $m_aCheckIssues} and {@link $m_aCheckWarnings} arrays
1617	 *
1618	 * @throws \ArchivedObjectException
1619	 * @throws \CoreException
1620	 * @throws \OQLException
1621	 *
1622	 * @api
1623	 */
1624	public function DoCheckToWrite()
1625	{
1626		$this->DoComputeValues();
1627
1628		$this->DoCheckUniqueness();
1629
1630		$aChanges = $this->ListChanges();
1631
1632		foreach($aChanges as $sAttCode => $value)
1633		{
1634			$res = $this->CheckValue($sAttCode);
1635			if ($res !== true)
1636			{
1637				// $res contains the error description
1638				$this->m_aCheckIssues[] = "Unexpected value for attribute '$sAttCode': $res";
1639			}
1640		}
1641		if (count($this->m_aCheckIssues) > 0)
1642		{
1643			// No need to check consistency between attributes if any of them has
1644			// an unexpected value
1645			return;
1646		}
1647		$res = $this->CheckConsistency();
1648		if ($res !== true)
1649		{
1650			// $res contains the error description
1651			$this->m_aCheckIssues[] = "Consistency rules not followed: $res";
1652		}
1653
1654		// Synchronization: are we attempting to modify an attribute for which an external source is master?
1655		//
1656		if ($this->m_bIsInDB && $this->InSyncScope() && (count($aChanges) > 0))
1657		{
1658			foreach($aChanges as $sAttCode => $value)
1659			{
1660				$iFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
1661				if ($iFlags & OPT_ATT_SLAVE)
1662				{
1663					// Note: $aReasonInfo['name'] could be reported (the task owning the attribute)
1664					$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
1665					$sAttLabel = $oAttDef->GetLabel();
1666					if (!empty($aReasons))
1667					{
1668						// Todo: associate the attribute code with the error
1669						$this->m_aCheckIssues[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $sAttLabel);
1670					}
1671				}
1672			}
1673		}
1674	}
1675
1676	/**
1677	 * @return array containing :
1678	 * <ul>
1679	 * <li>{@link $m_bCheckStatus}
1680	 * <li>{@link $m_aCheckIssues}
1681	 * <li>{@link $m_bSecurityIssue}
1682	 * </ul>
1683	 *
1684	 * @throws \ArchivedObjectException
1685	 * @throws \CoreException
1686	 * @throws \OQLException
1687	 *
1688	 * @internal do not overwrite ! Use {@link DoCheckToWrite} instead
1689	 */
1690	final public function CheckToWrite()
1691	{
1692		if (MetaModel::SkipCheckToWrite())
1693		{
1694			return array(true, array());
1695		}
1696		if (is_null($this->m_bCheckStatus))
1697		{
1698			$this->m_aCheckIssues = array();
1699
1700			$oKPI = new ExecutionKPI();
1701			$this->DoCheckToWrite();
1702			$oKPI->ComputeStats('CheckToWrite', get_class($this));
1703			if (count($this->m_aCheckIssues) == 0)
1704			{
1705				$this->m_bCheckStatus = true;
1706			}
1707			else
1708			{
1709				$this->m_bCheckStatus = false;
1710			}
1711		}
1712		return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue);
1713	}
1714
1715	// check if it is allowed to delete the existing object from the database
1716	// a displayable error is returned
1717	/**
1718	 * check if it is allowed to delete the existing object from the database
1719	 *
1720	 * a displayable error is added in {@link $m_aDeleteIssues}
1721	 *
1722	 * @param \DeletionPlan $oDeletionPlan
1723	 *
1724	 * @throws \CoreException
1725	 */
1726	protected function DoCheckToDelete(&$oDeletionPlan)
1727	{
1728		$this->m_aDeleteIssues = array(); // Ok
1729
1730		if ($this->InSyncScope())
1731		{
1732
1733			foreach ($this->GetSynchroData() as $iSourceId => $aSourceData)
1734			{
1735				foreach ($aSourceData['replica'] as $oReplica)
1736				{
1737					$oDeletionPlan->AddToDelete($oReplica, DEL_SILENT);
1738				}
1739				/** @var \SynchroDataSource $oDataSource */
1740				$oDataSource = $aSourceData['source'];
1741				if ($oDataSource->GetKey() == SynchroExecution::GetCurrentTaskId())
1742				{
1743					// The current task has the right to delete the object
1744					continue;
1745				}
1746				$oReplica = reset($aSourceData['replica']); // Take the first one
1747				if ($oReplica->Get('status_dest_creator') != 1)
1748				{
1749					// The object is not owned by the task
1750					continue;
1751				}
1752
1753				$sLink = $oDataSource->GetName();
1754				$sUserDeletePolicy = $oDataSource->Get('user_delete_policy');
1755				switch($sUserDeletePolicy)
1756				{
1757				case 'nobody':
1758					$this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink);
1759					break;
1760
1761				case 'administrators':
1762					if (!UserRights::IsAdministrator())
1763					{
1764						$this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink);
1765					}
1766					break;
1767
1768				case 'everybody':
1769				default:
1770					// Ok
1771					break;
1772				}
1773			}
1774		}
1775	}
1776
1777	/**
1778	 * @param \DeletionPlan $oDeletionPlan
1779	 *
1780	 * @return bool
1781	 */
1782	public function CheckToDelete(&$oDeletionPlan)
1783  	{
1784		$this->MakeDeletionPlan($oDeletionPlan);
1785		$oDeletionPlan->ComputeResults();
1786		return (!$oDeletionPlan->FoundStopper());
1787	}
1788
1789	protected function ListChangedValues(array $aProposal)
1790	{
1791		$aDelta = array();
1792		foreach ($aProposal as $sAtt => $proposedValue)
1793		{
1794			if (!array_key_exists($sAtt, $this->m_aOrigValues))
1795			{
1796				// The value was not set
1797				$aDelta[$sAtt] = $proposedValue;
1798			}
1799			elseif(!array_key_exists($sAtt, $this->m_aTouchedAtt) || (array_key_exists($sAtt, $this->m_aModifiedAtt) && $this->m_aModifiedAtt[$sAtt] == false))
1800			{
1801				// This attCode was never set, cannot be modified
1802				// or the same value - as the original value - was set, and has been verified as equivalent to the original value
1803				continue;
1804			}
1805			else if (array_key_exists($sAtt, $this->m_aModifiedAtt) && $this->m_aModifiedAtt[$sAtt] == true)
1806			{
1807				// We already know that the value is really modified
1808				$aDelta[$sAtt] = $proposedValue;
1809			}
1810			elseif(is_object($proposedValue))
1811			{
1812				$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAtt);
1813				// The value is an object, the comparison is not strict
1814				if (!$oAttDef->Equals($this->m_aOrigValues[$sAtt], $proposedValue))
1815				{
1816					$aDelta[$sAtt] = $proposedValue;
1817					$this->m_aModifiedAtt[$sAtt] = true; // Really modified
1818				}
1819				else
1820				{
1821					$this->m_aModifiedAtt[$sAtt] = false; // Not really modified
1822				}
1823			}
1824			else
1825			{
1826				// The value is a scalar, the comparison must be 100% strict
1827				if($this->m_aOrigValues[$sAtt] !== $proposedValue)
1828				{
1829					//echo "$sAtt:<pre>\n";
1830					//var_dump($this->m_aOrigValues[$sAtt]);
1831					//var_dump($proposedValue);
1832					//echo "</pre>\n";
1833					$aDelta[$sAtt] = $proposedValue;
1834					$this->m_aModifiedAtt[$sAtt] = true; // Really modified
1835				}
1836				else
1837				{
1838					$this->m_aModifiedAtt[$sAtt] = false; // Not really modified
1839				}
1840			}
1841		}
1842		return $aDelta;
1843	}
1844
1845	/**
1846	 * List the attributes that have been changed
1847	 *
1848	 * @return array attname => currentvalue
1849	 * @internal
1850	 */
1851	public function ListChanges()
1852	{
1853		if ($this->m_bIsInDB)
1854		{
1855			return $this->ListChangedValues($this->m_aCurrValues);
1856		}
1857		else
1858		{
1859			return $this->m_aCurrValues;
1860		}
1861	}
1862
1863	// Tells whether or not an object was modified since last read (ie: does it differ from the DB ?)
1864	public function IsModified()
1865	{
1866		$aChanges = $this->ListChanges();
1867		return (count($aChanges) != 0);
1868	}
1869
1870	/**
1871	 * @param \DBObject $oSibling
1872	 *
1873	 * @return bool
1874	 */
1875	public function Equals($oSibling)
1876	{
1877		if (get_class($oSibling) != get_class($this))
1878		{
1879			return false;
1880		}
1881		if ($this->GetKey() != $oSibling->GetKey())
1882		{
1883			return false;
1884		}
1885		if ($this->m_bIsInDB)
1886		{
1887			// If one has changed, then consider them as being different
1888			if ($this->IsModified() || $oSibling->IsModified())
1889			{
1890				return false;
1891			}
1892		}
1893		else
1894		{
1895			// Todo - implement this case (loop on every attribute)
1896			//foreach(MetaModel::ListAttributeDefs(get_class($this) as $sAttCode => $oAttDef)
1897			//{
1898					//if (!isset($this->m_CurrentValues[$sAttCode])) continue;
1899					//if (!isset($this->m_CurrentValues[$sAttCode])) continue;
1900					//if (!$oAttDef->Equals($this->m_CurrentValues[$sAttCode], $oSibling->m_CurrentValues[$sAttCode]))
1901					//{
1902						//return false;
1903					//}
1904			//}
1905			return false;
1906		}
1907		return true;
1908	}
1909
1910	/**
1911	 * Used only by insert, Meant to be overloaded
1912	 *
1913	 * @api
1914	 */
1915	protected function OnObjectKeyReady()
1916    {
1917    }
1918
1919	/**
1920	 * used both by insert/update
1921	 *
1922	 * @throws \CoreException
1923	 * @internal
1924	 */
1925	private function DBWriteLinks()
1926	{
1927		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
1928		{
1929			if (!$oAttDef->IsLinkSet()) continue;
1930			if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue;
1931			if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue;
1932
1933			/** @var \ormLinkSet $oLinkSet */
1934			$oLinkSet = $this->m_aCurrValues[$sAttCode];
1935			$oLinkSet->DBWrite($this);
1936		}
1937	}
1938
1939	/**
1940	 * Used both by insert/update
1941	 *
1942	 * @throws \CoreException
1943	 * @internal
1944	 */
1945	private function WriteExternalAttributes()
1946	{
1947		foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
1948		{
1949			if (!$oAttDef->LoadInObject()) continue;
1950			if ($oAttDef->LoadFromDB()) continue;
1951			if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue;
1952			if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue;
1953			/** @var \AttributeCustomFields $oAttDef */
1954			$oAttDef->WriteValue($this, $this->m_aCurrValues[$sAttCode]);
1955		}
1956	}
1957
1958	// Note: this is experimental - it was designed to speed up the setup of iTop
1959	// Known limitations:
1960	//   - does not work with multi-table classes (issue with the unique id to maintain in several tables)
1961	//   - the id of the object is not updated
1962	static public final function BulkInsertStart()
1963	{
1964		self::$m_bBulkInsert = true;
1965	}
1966
1967	static public final function BulkInsertFlush()
1968	{
1969		if (!self::$m_bBulkInsert) return;
1970
1971		foreach(self::$m_aBulkInsertCols as $sClass => $aTables)
1972		{
1973			foreach ($aTables as $sTable => $sColumns)
1974			{
1975				$sValues = implode(', ', self::$m_aBulkInsertItems[$sClass][$sTable]);
1976				$sInsertSQL = "INSERT INTO `$sTable` ($sColumns) VALUES $sValues";
1977				CMDBSource::InsertInto($sInsertSQL);
1978			}
1979		}
1980
1981		// Reset
1982		self::$m_aBulkInsertItems = array();
1983		self::$m_aBulkInsertCols = array();
1984		self::$m_bBulkInsert = false;
1985	}
1986
1987	/**
1988	 * Persists new object in the DB
1989	 *
1990	 * @param $sTableClass
1991	 *
1992	 * @return bool|int false if nothing to persist (no change), new key value otherwise
1993	 * @throws \CoreException
1994	 * @throws \MySQLException
1995	 * @internal
1996	 */
1997	private function DBInsertSingleTable($sTableClass)
1998	{
1999		$sTable = MetaModel::DBGetTable($sTableClass);
2000		// Abstract classes or classes having no specific attribute do not have an associated table
2001		if ($sTable == '') return false;
2002
2003		$sClass = get_class($this);
2004
2005		// fields in first array, values in the second
2006		$aFieldsToWrite = array();
2007		$aValuesToWrite = array();
2008
2009		if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
2010		{
2011			// Add it to the list of fields to write
2012			$aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`';
2013			$aValuesToWrite[] = CMDBSource::Quote($this->m_iKey);
2014		}
2015
2016		$aHierarchicalKeys = array();
2017
2018		foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
2019		{
2020			// Skip this attribute if not defined in this table
2021			if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue;
2022			$aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]);
2023			foreach($aAttColumns as $sColumn => $sValue)
2024			{
2025				$aFieldsToWrite[] = "`$sColumn`";
2026				$aValuesToWrite[] = CMDBSource::Quote($sValue);
2027			}
2028			if ($oAttDef->IsHierarchicalKey())
2029			{
2030				$aHierarchicalKeys[$sAttCode] = $oAttDef;
2031			}
2032		}
2033
2034		if (count($aValuesToWrite) == 0) return false;
2035
2036		if (MetaModel::DBIsReadOnly())
2037		{
2038			$iNewKey = -1;
2039		}
2040		else
2041		{
2042			if (self::$m_bBulkInsert)
2043			{
2044				if (!isset(self::$m_aBulkInsertCols[$sClass][$sTable]))
2045				{
2046					self::$m_aBulkInsertCols[$sClass][$sTable] = implode(', ', $aFieldsToWrite);
2047				}
2048				self::$m_aBulkInsertItems[$sClass][$sTable][] = '('.implode (', ', $aValuesToWrite).')';
2049
2050				$iNewKey = 999999; // TODO - compute next id....
2051			}
2052			else
2053			{
2054				if (count($aHierarchicalKeys) > 0)
2055				{
2056					foreach($aHierarchicalKeys as $sAttCode => $oAttDef)
2057					{
2058						$aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable);
2059						$aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`';
2060						$aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()];
2061						$aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`';
2062						$aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()];
2063					}
2064				}
2065				$sInsertSQL = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).")";
2066				$iNewKey = CMDBSource::InsertInto($sInsertSQL);
2067			}
2068		}
2069		// Note that it is possible to have a key defined here, and the autoincrement expected, this is acceptable in a non root class
2070		if (empty($this->m_iKey))
2071		{
2072			// Take the autonumber
2073			$this->m_iKey = $iNewKey;
2074		}
2075		return $this->m_iKey;
2076	}
2077
2078	/**
2079	 * Persists object to new records in the DB
2080	 *
2081	 * @return int key of the newly created object
2082	 * @throws \ArchivedObjectException
2083	 * @throws \CoreCannotSaveObjectException if {@link CheckToWrite()} returns issues
2084	 * @throws \CoreException
2085	 * @throws \CoreUnexpectedValue
2086	 * @throws \CoreWarning
2087	 * @throws \MySQLException
2088	 * @throws \OQLException
2089	 *
2090	 * @internal
2091	 */
2092	public function DBInsertNoReload()
2093	{
2094		if ($this->m_bIsInDB)
2095		{
2096			throw new CoreException("The object already exists into the Database, you may want to use the clone function");
2097		}
2098
2099		$sClass = get_class($this);
2100		$sRootClass = MetaModel::GetRootClass($sClass);
2101
2102		// Ensure the update of the values (we are accessing the data directly)
2103		$this->DoComputeValues();
2104		$this->OnInsert();
2105
2106		if ($this->m_iKey < 0)
2107		{
2108			// This was a temporary "memory" key: discard it so that DBInsertSingleTable will not try to use it!
2109			$this->m_iKey = null;
2110		}
2111
2112		// If not automatically computed, then check that the key is given by the caller
2113		if (!MetaModel::IsAutoIncrementKey($sRootClass))
2114		{
2115			if (empty($this->m_iKey))
2116			{
2117				throw new CoreWarning("Missing key for the object to write - This class is supposed to have a user defined key, not an autonumber", array('class' => $sRootClass));
2118			}
2119		}
2120
2121		// Ultimate check - ensure DB integrity
2122		list($bRes, $aIssues) = $this->CheckToWrite();
2123		if (!$bRes)
2124		{
2125			throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
2126		}
2127
2128		// Stop watches
2129		$sState = $this->GetState();
2130		if ($sState != '')
2131		{
2132			foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
2133			{
2134				if ($oAttDef instanceof AttributeStopWatch)
2135				{
2136					if (in_array($sState, $oAttDef->GetStates()))
2137					{
2138						// Start the stop watch and compute the deadlines
2139						/** @var \ormStopWatch $oSW */
2140						$oSW = $this->Get($sAttCode);
2141						$oSW->Start($this, $oAttDef);
2142						$oSW->ComputeDeadlines($this, $oAttDef);
2143						$this->Set($sAttCode, $oSW);
2144					}
2145				}
2146			}
2147		}
2148
2149		// First query built upon on the root class, because the ID must be created first
2150		$this->m_iKey = $this->DBInsertSingleTable($sRootClass);
2151
2152		// Then do the leaf class, if different from the root class
2153		if ($sClass != $sRootClass)
2154		{
2155			$this->DBInsertSingleTable($sClass);
2156		}
2157
2158		// Then do the other classes
2159		foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass)
2160		{
2161			if ($sParentClass == $sRootClass) continue;
2162			$this->DBInsertSingleTable($sParentClass);
2163		}
2164
2165		$this->OnObjectKeyReady();
2166
2167        $this->DBWriteLinks();
2168		$this->WriteExternalAttributes();
2169
2170		$this->m_bIsInDB = true;
2171		$this->m_bDirty = false;
2172		foreach ($this->m_aCurrValues as $sAttCode => $value)
2173		{
2174			if (is_object($value))
2175			{
2176				$value = clone $value;
2177			}
2178			$this->m_aOrigValues[$sAttCode] = $value;
2179		}
2180
2181		$this->AfterInsert();
2182
2183		// Activate any existing trigger
2184		$sClass = get_class($this);
2185		$sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
2186		$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectCreate AS t WHERE t.target_class IN ('$sClassList')"));
2187		while ($oTrigger = $oSet->Fetch())
2188		{
2189			/** @var \Trigger $oTrigger */
2190			$oTrigger->DoActivate($this->ToArgs('this'));
2191		}
2192
2193		// Callbacks registered with RegisterCallback
2194		if (isset($this->m_aCallbacks[self::CALLBACK_AFTERINSERT]))
2195		{
2196			foreach ($this->m_aCallbacks[self::CALLBACK_AFTERINSERT] as $aCallBackData)
2197			{
2198				call_user_func_array($aCallBackData['callback'], $aCallBackData['params']);
2199			}
2200		}
2201
2202		$this->RecordObjCreation();
2203
2204		return $this->m_iKey;
2205	}
2206
2207	protected function MakeInsertStatementSingleTable($aAuthorizedExtKeys, &$aStatements, $sTableClass)
2208	{
2209		$sTable = MetaModel::DBGetTable($sTableClass);
2210		// Abstract classes or classes having no specific attribute do not have an associated table
2211		if ($sTable == '') return;
2212
2213		// fields in first array, values in the second
2214		$aFieldsToWrite = array();
2215		$aValuesToWrite = array();
2216
2217		if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
2218		{
2219			// Add it to the list of fields to write
2220			$aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`';
2221			$aValuesToWrite[] = CMDBSource::Quote($this->m_iKey);
2222		}
2223
2224		$aHierarchicalKeys = array();
2225		foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
2226		{
2227			// Skip this attribute if not defined in this table
2228			if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue;
2229			// Skip link set that can still be undefined though the object is 100% loaded
2230			if ($oAttDef->IsLinkSet()) continue;
2231
2232			$value = $this->m_aCurrValues[$sAttCode];
2233			if ($oAttDef->IsExternalKey())
2234			{
2235				/** @var \AttributeExternalKey $oAttDef */
2236				$sTargetClass = $oAttDef->GetTargetClass();
2237				if (is_array($aAuthorizedExtKeys))
2238				{
2239					if (!array_key_exists($sTargetClass, $aAuthorizedExtKeys) || !array_key_exists($value, $aAuthorizedExtKeys[$sTargetClass]))
2240					{
2241						$value = 0;
2242					}
2243				}
2244			}
2245			$aAttColumns = $oAttDef->GetSQLValues($value);
2246			foreach($aAttColumns as $sColumn => $sValue)
2247			{
2248				$aFieldsToWrite[] = "`$sColumn`";
2249				$aValuesToWrite[] = CMDBSource::Quote($sValue);
2250			}
2251			if ($oAttDef->IsHierarchicalKey())
2252			{
2253				$aHierarchicalKeys[$sAttCode] = $oAttDef;
2254			}
2255		}
2256
2257		if (count($aValuesToWrite) == 0) return;
2258
2259		if (count($aHierarchicalKeys) > 0)
2260		{
2261			foreach($aHierarchicalKeys as $sAttCode => $oAttDef)
2262			{
2263				$aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable);
2264				$aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`';
2265				$aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()];
2266				$aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`';
2267				$aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()];
2268			}
2269		}
2270		$aStatements[] = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).");";
2271	}
2272
2273	public function MakeInsertStatements($aAuthorizedExtKeys, &$aStatements)
2274	{
2275		$sClass = get_class($this);
2276		$sRootClass = MetaModel::GetRootClass($sClass);
2277
2278		// First query built upon on the root class, because the ID must be created first
2279		$this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sRootClass);
2280
2281		// Then do the leaf class, if different from the root class
2282		if ($sClass != $sRootClass)
2283		{
2284			$this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sClass);
2285		}
2286
2287		// Then do the other classes
2288		foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass)
2289		{
2290			if ($sParentClass == $sRootClass) continue;
2291			$this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sParentClass);
2292		}
2293	}
2294
2295	/**
2296	 * @return int|null inserted object key
2297	 * @throws \ArchivedObjectException
2298	 * @throws \CoreCannotSaveObjectException
2299	 * @throws \CoreException
2300	 * @throws \CoreUnexpectedValue
2301	 * @throws \CoreWarning
2302	 * @throws \MySQLException
2303	 * @throws \OQLException
2304	 * @internal
2305	 */
2306	public function DBInsert()
2307	{
2308		$this->DBInsertNoReload();
2309		$this->Reload();
2310		return $this->m_iKey;
2311	}
2312
2313	public function DBInsertTracked(CMDBChange $oChange)
2314	{
2315		CMDBObject::SetCurrentChange($oChange);
2316		return $this->DBInsert();
2317	}
2318
2319	public function DBInsertTrackedNoReload(CMDBChange $oChange)
2320	{
2321		CMDBObject::SetCurrentChange($oChange);
2322		return $this->DBInsertNoReload();
2323	}
2324
2325	// Creates a copy of the current object into the database
2326	// Returns the id of the newly created object
2327	public function DBClone($iNewKey = null)
2328	{
2329		$this->m_bIsInDB = false;
2330		$this->m_iKey = $iNewKey;
2331		$ret = $this->DBInsert();
2332		$this->RecordObjCreation();
2333		return $ret;
2334	}
2335
2336	/**
2337	 * This function is automatically called after cloning an object with the "clone" PHP language construct
2338	 * The purpose of this method is to reset the appropriate attributes of the object in
2339	 * order to make sure that the newly cloned object is really distinct from its clone
2340	 */
2341	public function __clone()
2342	{
2343		$this->m_bIsInDB = false;
2344		$this->m_bDirty = true;
2345		$this->m_iKey = self::GetNextTempId(get_class($this));
2346	}
2347
2348	/**
2349	 * Update an object in DB
2350	 *
2351	 * @return int object key
2352	 * @throws \CoreException
2353	 * @throws \CoreCannotSaveObjectException if {@link CheckToWrite()} returns issues
2354	 */
2355	public function DBUpdate()
2356	{
2357		if (!$this->m_bIsInDB)
2358		{
2359			throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead");
2360		}
2361
2362		// Protect against reentrance (e.g. cascading the update of ticket logs)
2363		static $aUpdateReentrance = array();
2364		$sKey = get_class($this).'::'.$this->GetKey();
2365		if (array_key_exists($sKey, $aUpdateReentrance))
2366		{
2367			return false;
2368		}
2369		$aUpdateReentrance[$sKey] = true;
2370
2371		try
2372		{
2373			$this->DoComputeValues();
2374			// Stop watches
2375			$sState = $this->GetState();
2376			if ($sState != '')
2377			{
2378				foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
2379				{
2380					if ($oAttDef instanceof AttributeStopWatch)
2381					{
2382						if (in_array($sState, $oAttDef->GetStates()))
2383						{
2384							// Compute or recompute the deadlines
2385							$oSW = $this->Get($sAttCode);
2386							$oSW->ComputeDeadlines($this, $oAttDef);
2387							$this->Set($sAttCode, $oSW);
2388						}
2389					}
2390				}
2391			}
2392			$this->OnUpdate();
2393
2394			$aChanges = $this->ListChanges();
2395			if (count($aChanges) == 0)
2396			{
2397				// Attempting to update an unchanged object
2398				unset($aUpdateReentrance[$sKey]);
2399				return $this->m_iKey;
2400			}
2401
2402			// Ultimate check - ensure DB integrity
2403			list($bRes, $aIssues) = $this->CheckToWrite();
2404			if (!$bRes)
2405			{
2406				throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
2407			}
2408
2409			// Save the original values (will be reset to the new values when the object get written to the DB)
2410			$aOriginalValues = $this->m_aOrigValues;
2411
2412			// Activate any existing trigger
2413			$sClass = get_class($this);
2414			$sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
2415			$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN ('$sClassList')"));
2416			while ($oTrigger = $oSet->Fetch())
2417			{
2418				/** @var \Trigger $oTrigger */
2419				$oTrigger->DoActivate($this->ToArgs('this'));
2420			}
2421
2422			$bHasANewExternalKeyValue = false;
2423			$aHierarchicalKeys = array();
2424			$aDBChanges = array();
2425			foreach($aChanges as $sAttCode => $valuecurr)
2426			{
2427				$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
2428				if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true;
2429				if ($oAttDef->IsBasedOnDBColumns())
2430				{
2431					$aDBChanges[$sAttCode] = $aChanges[$sAttCode];
2432				}
2433				if ($oAttDef->IsHierarchicalKey())
2434				{
2435					$aHierarchicalKeys[$sAttCode] = $oAttDef;
2436				}
2437			}
2438
2439			if (!MetaModel::DBIsReadOnly())
2440			{
2441				// Update the left & right indexes for each hierarchical key
2442				foreach($aHierarchicalKeys as $sAttCode => $oAttDef)
2443				{
2444					$sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode);
2445					$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey();
2446					$aRes = CMDBSource::QueryToArray($sSQL);
2447					$iMyLeft = $aRes[0]['left'];
2448					$iMyRight = $aRes[0]['right'];
2449					$iDelta =$iMyRight - $iMyLeft + 1;
2450					MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable);
2451
2452					if ($aDBChanges[$sAttCode] == 0)
2453					{
2454						// No new parent, insert completely at the right of the tree
2455						$sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`";
2456						$aRes = CMDBSource::QueryToArray($sSQL);
2457						if (count($aRes) == 0)
2458						{
2459							$iNewLeft = 1;
2460						}
2461						else
2462						{
2463							$iNewLeft = $aRes[0]['max']+1;
2464						}
2465					}
2466					else
2467					{
2468						// Insert at the right of the specified parent
2469						$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aDBChanges[$sAttCode]);
2470						$iNewLeft = CMDBSource::QueryToScalar($sSQL);
2471					}
2472
2473					MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable);
2474
2475					$aHKChanges = array();
2476					$aHKChanges[$sAttCode] = $aDBChanges[$sAttCode];
2477					$aHKChanges[$oAttDef->GetSQLLeft()] = $iNewLeft;
2478					$aHKChanges[$oAttDef->GetSQLRight()] = $iNewLeft + $iDelta - 1;
2479					$aDBChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below
2480				}
2481
2482				// Update scalar attributes
2483				if (count($aDBChanges) != 0)
2484				{
2485					$oFilter = new DBObjectSearch(get_class($this));
2486					$oFilter->AddCondition('id', $this->m_iKey, '=');
2487					$oFilter->AllowAllData();
2488
2489					$sSQL = $oFilter->MakeUpdateQuery($aDBChanges);
2490					CMDBSource::Query($sSQL);
2491				}
2492			}
2493
2494			$this->DBWriteLinks();
2495			$this->WriteExternalAttributes();
2496
2497			$this->m_bDirty = false;
2498			$this->m_aTouchedAtt = array();
2499			$this->m_aModifiedAtt = array();
2500
2501			$this->AfterUpdate();
2502
2503			// Reload to get the external attributes
2504			if ($bHasANewExternalKeyValue)
2505			{
2506				$this->Reload(true /* AllowAllData */);
2507			}
2508			else
2509			{
2510				// Reset original values although the object has not been reloaded
2511				foreach ($this->m_aLoadedAtt as $sAttCode => $bLoaded)
2512				{
2513					if ($bLoaded)
2514					{
2515						$value = $this->m_aCurrValues[$sAttCode];
2516						$this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value;
2517					}
2518				}
2519			}
2520
2521			if (count($aChanges) != 0)
2522			{
2523				$this->RecordAttChanges($aChanges, $aOriginalValues);
2524			}
2525		}
2526		catch (CoreCannotSaveObjectException $e)
2527		{
2528			throw $e;
2529		}
2530		catch (Exception $e)
2531		{
2532			$aErrors = array($e->getMessage());
2533			throw new CoreCannotSaveObjectException(array('id' => $this->GetKey(), 'class' => get_class($this), 'issues' => $aErrors));
2534		}
2535		finally
2536		{
2537			unset($aUpdateReentrance[$sKey]);
2538		}
2539
2540		return $this->m_iKey;
2541	}
2542
2543	public function DBUpdateTracked(CMDBChange $oChange)
2544	{
2545		CMDBObject::SetCurrentChange($oChange);
2546		return $this->DBUpdate();
2547	}
2548
2549	/**
2550	 * Make the current changes persistent - clever wrapper for Insert or Update
2551	 *
2552	 * @return int
2553	 * @throws \CoreCannotSaveObjectException
2554	 * @throws \CoreException
2555	 */
2556	public function DBWrite()
2557	{
2558		if ($this->m_bIsInDB)
2559		{
2560			return $this->DBUpdate();
2561		}
2562		else
2563		{
2564			return $this->DBInsert();
2565		}
2566	}
2567
2568	private function DBDeleteSingleTable($sTableClass)
2569	{
2570		$sTable = MetaModel::DBGetTable($sTableClass);
2571		// Abstract classes or classes having no specific attribute do not have an associated table
2572		if ($sTable == '') return;
2573
2574		$sPKField = '`'.MetaModel::DBGetKey($sTableClass).'`';
2575		$sKey = CMDBSource::Quote($this->m_iKey);
2576
2577		$sDeleteSQL = "DELETE FROM `$sTable` WHERE $sPKField = $sKey";
2578		CMDBSource::DeleteFrom($sDeleteSQL);
2579	}
2580
2581	protected function DBDeleteSingleObject()
2582	{
2583		if (!MetaModel::DBIsReadOnly())
2584		{
2585			$this->OnDelete();
2586
2587			// Activate any existing trigger
2588			$sClass = get_class($this);
2589			$sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
2590			$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectDelete AS t WHERE t.target_class IN ('$sClassList')"));
2591			while ($oTrigger = $oSet->Fetch())
2592			{
2593				/** @var \Trigger $oTrigger */
2594				$oTrigger->DoActivate($this->ToArgs('this'));
2595			}
2596
2597			$this->RecordObjDeletion($this->m_iKey); // May cause a reload for storing history information
2598
2599			foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
2600			{
2601				if ($oAttDef->IsHierarchicalKey())
2602				{
2603					// Update the left & right indexes for each hierarchical key
2604					$sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode);
2605					/** @var \AttributeHierarchicalKey $oAttDef */
2606					$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".CMDBSource::Quote($this->m_iKey);
2607					$aRes = CMDBSource::QueryToArray($sSQL);
2608					$iMyLeft = $aRes[0]['left'];
2609					$iMyRight = $aRes[0]['right'];
2610					$iDelta =$iMyRight - $iMyLeft + 1;
2611					MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable);
2612
2613					// No new parent for now, insert completely at the right of the tree
2614					$sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`";
2615					$aRes = CMDBSource::QueryToArray($sSQL);
2616					if (count($aRes) == 0)
2617					{
2618						$iNewLeft = 1;
2619					}
2620					else
2621					{
2622						$iNewLeft = $aRes[0]['max']+1;
2623					}
2624					MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable);
2625				}
2626				elseif (!$oAttDef->LoadFromDB())
2627				{
2628					/** @var \AttributeCustomFields $oAttDef */
2629					$oAttDef->DeleteValue($this);
2630				}
2631			}
2632
2633			foreach(MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL) as $sParentClass)
2634			{
2635				$this->DBDeleteSingleTable($sParentClass);
2636			}
2637
2638			$this->AfterDelete();
2639
2640			$this->m_bIsInDB = false;
2641			// Fix for N°926: do NOT reset m_iKey as it can be used to have it for reporting purposes (see the REST service to delete
2642			// objects, reported as bug N°926)
2643			// Thought the key is not reset, using DBInsert or DBWrite will create an object having the same characteristics and a new ID. DBUpdate is protected
2644		}
2645	}
2646
2647	// Delete an object... and guarantee data integrity
2648	//
2649	public function DBDelete(&$oDeletionPlan = null)
2650	{
2651		static $iLoopTimeLimit = null;
2652		if ($iLoopTimeLimit == null)
2653		{
2654			$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
2655		}
2656		if (is_null($oDeletionPlan))
2657		{
2658			$oDeletionPlan = new DeletionPlan();
2659		}
2660		$this->MakeDeletionPlan($oDeletionPlan);
2661		$oDeletionPlan->ComputeResults();
2662
2663		if ($oDeletionPlan->FoundStopper())
2664		{
2665			$aIssues = $oDeletionPlan->GetIssues();
2666			throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues)));
2667		}
2668		else
2669		{
2670			// Getting and setting time limit are not symetric:
2671			// www.php.net/manual/fr/function.set-time-limit.php#72305
2672			$iPreviousTimeLimit = ini_get('max_execution_time');
2673
2674			foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete)
2675			{
2676				foreach ($aToDelete as $iId => $aData)
2677				{
2678					/** @var \DBObject $oToDelete */
2679					$oToDelete = $aData['to_delete'];
2680					// The deletion based on a deletion plan should not be done for each oject if the deletion plan is common (Trac #457)
2681					// because for each object we would try to update all the preceding ones... that are already deleted
2682					// A better approach would be to change the API to apply the DBDelete on the deletion plan itself... just once
2683					// As a temporary fix: delete only the objects that are still to be deleted...
2684					if ($oToDelete->m_bIsInDB)
2685					{
2686						set_time_limit($iLoopTimeLimit);
2687						$oToDelete->DBDeleteSingleObject();
2688					}
2689				}
2690			}
2691
2692			foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate)
2693			{
2694				foreach ($aToUpdate as $iId => $aData)
2695				{
2696					$oToUpdate = $aData['to_reset'];
2697					/** @var \DBObject $oToUpdate */
2698					foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef)
2699					{
2700						$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
2701						set_time_limit($iLoopTimeLimit);
2702						$oToUpdate->DBUpdate();
2703					}
2704				}
2705			}
2706
2707			set_time_limit($iPreviousTimeLimit);
2708		}
2709
2710		return $oDeletionPlan;
2711	}
2712
2713	public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null)
2714	{
2715		CMDBObject::SetCurrentChange($oChange);
2716		$this->DBDelete($oDeletionPlan);
2717	}
2718
2719	public function EnumTransitions()
2720	{
2721		$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
2722		if (empty($sStateAttCode)) return array();
2723
2724		$sState = $this->Get(MetaModel::GetStateAttributeCode(get_class($this)));
2725		return MetaModel::EnumTransitions(get_class($this), $sState);
2726	}
2727
2728	/**
2729	* Designed as an action to be called when a stop watch threshold times out
2730	*/
2731	public function ResetStopWatch($sAttCode)
2732	{
2733		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
2734		if (!$oAttDef instanceof AttributeStopWatch)
2735		{
2736			throw new CoreException("Invalid stop watch id: '$sAttCode'");
2737		}
2738		$oSW = $this->Get($sAttCode);
2739		$oSW->Reset($this, $oAttDef);
2740		$this->Set($sAttCode, $oSW);
2741		return true;
2742	}
2743
2744	/**
2745	 * Designed as an action to be called when a stop watch threshold times out
2746	 * or from within the framework
2747	 * @param $sStimulusCode
2748	 * @param bool|false $bDoNotWrite
2749	 * @return bool
2750	 * @throws CoreException
2751	 * @throws CoreUnexpectedValue
2752	 */
2753	public function ApplyStimulus($sStimulusCode, $bDoNotWrite = false)
2754	{
2755		$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
2756		if (empty($sStateAttCode))
2757		{
2758			throw new CoreException('No lifecycle for the class '.get_class($this));
2759		}
2760
2761		MyHelpers::CheckKeyInArray('object lifecycle stimulus', $sStimulusCode, MetaModel::EnumStimuli(get_class($this)));
2762
2763		$aStateTransitions = $this->EnumTransitions();
2764		if (!array_key_exists($sStimulusCode, $aStateTransitions))
2765		{
2766			// This simulus has no effect in the current state... do nothing
2767			return true;
2768		}
2769		$aTransitionDef = $aStateTransitions[$sStimulusCode];
2770
2771		// Change the state before proceeding to the actions, this is necessary because an action might
2772		// trigger another stimuli (alternative: push the stimuli into a queue)
2773		$sPreviousState = $this->Get($sStateAttCode);
2774		$sNewState = $aTransitionDef['target_state'];
2775		$this->Set($sStateAttCode, $sNewState);
2776
2777		// $aTransitionDef is an
2778		//    array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD
2779
2780		$bSuccess = true;
2781		foreach ($aTransitionDef['actions'] as $actionHandler)
2782		{
2783			if (is_string($actionHandler))
2784			{
2785				// Old (pre-2.1.0 modules) action definition without any parameter
2786				$aActionCallSpec = array($this, $actionHandler);
2787				$sActionDesc = get_class($this).'::'.$actionHandler;
2788
2789				if (!is_callable($aActionCallSpec))
2790				{
2791					throw new CoreException("Unable to call action: ".get_class($this)."::$actionHandler");
2792				}
2793				$bRet = call_user_func($aActionCallSpec, $sStimulusCode);
2794			}
2795			else // if (is_array($actionHandler))
2796			{
2797				// New syntax: 'verb' and typed parameters
2798				$sAction = $actionHandler['verb'];
2799				$sActionDesc = get_class($this).'::'.$sAction;
2800				$aParams = array();
2801				foreach($actionHandler['params'] as $aDefinition)
2802				{
2803					$sParamType = array_key_exists('type', $aDefinition) ? $aDefinition['type'] : 'string';
2804					switch($sParamType)
2805					{
2806						case 'int':
2807							$value = (int)$aDefinition['value'];
2808							break;
2809
2810						case 'float':
2811							$value = (float)$aDefinition['value'];
2812							break;
2813
2814						case 'bool':
2815							$value = (bool)$aDefinition['value'];
2816							break;
2817
2818						case 'reference':
2819							$value = ${$aDefinition['value']};
2820							break;
2821
2822						case 'string':
2823						default:
2824							$value = (string)$aDefinition['value'];
2825					}
2826					$aParams[] = $value;
2827				}
2828				$aCallSpec = array($this, $sAction);
2829				$bRet = call_user_func_array($aCallSpec, $aParams);
2830			}
2831			// if one call fails, the whole is considered as failed
2832			// (in case there is no returned value, null is obtained and means "ok")
2833			if ($bRet === false)
2834			{
2835				IssueLog::Info("Lifecycle action $sActionDesc returned false on object #".$this->GetKey());
2836				$bSuccess = false;
2837			}
2838		}
2839		if ($bSuccess)
2840		{
2841			$sClass = get_class($this);
2842
2843			// Stop watches
2844			foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
2845			{
2846				if ($oAttDef instanceof AttributeStopWatch)
2847				{
2848					$oSW = $this->Get($sAttCode);
2849					if (in_array($sNewState, $oAttDef->GetStates()))
2850					{
2851						$oSW->Start($this, $oAttDef);
2852					}
2853					else
2854					{
2855						$oSW->Stop($this, $oAttDef);
2856					}
2857					$this->Set($sAttCode, $oSW);
2858				}
2859			}
2860
2861			if (!$bDoNotWrite)
2862			{
2863				$this->DBWrite();
2864			}
2865
2866			// Change state triggers...
2867			$sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
2868			$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateLeave AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sPreviousState'"));
2869			while ($oTrigger = $oSet->Fetch())
2870			{
2871				/** @var \Trigger $oTrigger */
2872				$oTrigger->DoActivate($this->ToArgs('this'));
2873			}
2874
2875			$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateEnter AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sNewState'"));
2876			while ($oTrigger = $oSet->Fetch())
2877			{
2878				/** @var \Trigger $oTrigger */
2879				$oTrigger->DoActivate($this->ToArgs('this'));
2880			}
2881		}
2882
2883		return $bSuccess;
2884	}
2885
2886	/**
2887	 * Lifecycle action: Recover the default value (aka when an object is being created)
2888	 */
2889	public function Reset($sAttCode)
2890	{
2891		$this->Set($sAttCode, $this->GetDefaultValue($sAttCode));
2892		return true;
2893	}
2894
2895	/**
2896	 * Lifecycle action: Copy an attribute to another
2897	 */
2898	public function Copy($sDestAttCode, $sSourceAttCode)
2899	{
2900		$this->Set($sDestAttCode, $this->Get($sSourceAttCode));
2901		return true;
2902	}
2903
2904	/**
2905	 * Lifecycle action: Set the current date/time for the given attribute
2906	 */
2907	public function SetCurrentDate($sAttCode)
2908	{
2909		$this->Set($sAttCode, time());
2910		return true;
2911	}
2912
2913	/**
2914	 * Lifecycle action: Set the current logged in user for the given attribute
2915	 */
2916	public function SetCurrentUser($sAttCode)
2917	{
2918		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
2919		if ($oAttDef instanceof AttributeString)
2920		{
2921			// Note: the user friendly name is the contact friendly name if a contact is attached to the logged in user
2922			$this->Set($sAttCode, UserRights::GetUserFriendlyName());
2923		}
2924		else
2925		{
2926			if ($oAttDef->IsExternalKey())
2927			{
2928				/** @var \AttributeExternalKey $oAttDef */
2929				if ($oAttDef->GetTargetClass() != 'User')
2930				{
2931					throw new Exception("SetCurrentUser: the attribute $sAttCode must be an external key to 'User', found '".$oAttDef->GetTargetClass()."'");
2932				}
2933			}
2934			$this->Set($sAttCode, UserRights::GetUserId());
2935		}
2936		return true;
2937	}
2938
2939	/**
2940	 * Lifecycle action: Set the current logged in CONTACT for the given attribute
2941	 */
2942	public function SetCurrentPerson($sAttCode)
2943	{
2944		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
2945		if ($oAttDef instanceof AttributeString)
2946		{
2947			$iPerson = UserRights::GetContactId();
2948			if ($iPerson == 0)
2949			{
2950				$this->Set($sAttCode, '');
2951			}
2952			else
2953			{
2954				$oPerson = MetaModel::GetObject('Person', $iPerson);
2955				$this->Set($sAttCode, $oPerson->Get('friendlyname'));
2956			}
2957		}
2958		else
2959		{
2960			if ($oAttDef->IsExternalKey())
2961			{
2962				/** @var \AttributeExternalKey $oAttDef */
2963				if (!MetaModel::IsParentClass($oAttDef->GetTargetClass(), 'Person'))
2964				{
2965					throw new Exception("SetCurrentContact: the attribute $sAttCode must be an external key to 'Person' or any other class above 'Person', found '".$oAttDef->GetTargetClass()."'");
2966				}
2967			}
2968			$this->Set($sAttCode, UserRights::GetContactId());
2969		}
2970		return true;
2971	}
2972
2973	/**
2974	 * Lifecycle action: Set the time elapsed since a reference point
2975	 */
2976	public function SetElapsedTime($sAttCode, $sRefAttCode, $sWorkingTimeComputer = null)
2977	{
2978		if (is_null($sWorkingTimeComputer))
2979		{
2980			$sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer';
2981		}
2982		$oComputer = new $sWorkingTimeComputer();
2983		$aCallSpec = array($oComputer, 'GetOpenDuration');
2984		if (!is_callable($aCallSpec))
2985		{
2986			throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetOpenDuration'");
2987		}
2988
2989		$iStartTime = AttributeDateTime::GetAsUnixSeconds($this->Get($sRefAttCode));
2990		$oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2
2991		$oEndDate = new DateTime(); // now
2992
2993		if (class_exists('WorkingTimeRecorder'))
2994		{
2995			$sClass = get_class($this);
2996			WorkingTimeRecorder::Start($this, time(), "DBObject-SetElapsedTime-$sAttCode-$sRefAttCode", 'Core:ExplainWTC:ElapsedTime', array("Class:$sClass/Attribute:$sAttCode"));
2997		}
2998		$iElapsed = call_user_func($aCallSpec, $this, $oStartDate, $oEndDate);
2999		if (class_exists('WorkingTimeRecorder'))
3000		{
3001			WorkingTimeRecorder::End();
3002		}
3003
3004		$this->Set($sAttCode, $iElapsed);
3005		return true;
3006	}
3007
3008
3009
3010   	/**
3011	 * Create query parameters (SELECT ... WHERE service = :this->service_id)
3012	 * to be used with the APIs DBObjectSearch/DBObjectSet
3013	 *
3014	 * Starting 2.0.2 the parameters are computed on demand, at the lowest level,
3015	 * in VariableExpression::Render()
3016	 */
3017	public function ToArgsForQuery($sArgName = 'this')
3018	{
3019		return array($sArgName.'->object()' => $this);
3020	}
3021
3022	/**
3023 	 * Create template placeholders: now equivalent to ToArgsForQuery since the actual
3024	 * template placeholders are computed on demand.
3025	 */
3026	public function ToArgs($sArgName = 'this')
3027	{
3028		return $this->ToArgsForQuery($sArgName);
3029	}
3030
3031	public function GetForTemplate($sPlaceholderAttCode)
3032	{
3033		$ret = null;
3034		if (preg_match('/^([^-]+)-(>|&gt;)(.+)$/', $sPlaceholderAttCode, $aMatches)) // Support both syntaxes: this->xxx or this-&gt;xxx for HTML compatibility
3035		{
3036			$sExtKeyAttCode = $aMatches[1];
3037			$sRemoteAttCode = $aMatches[3];
3038			if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode))
3039			{
3040				throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this));
3041			}
3042
3043			$oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
3044			if (!$oKeyAttDef instanceof AttributeExternalKey)
3045			{
3046				throw new CoreException("'$sExtKeyAttCode' is not an external key of the class ".get_class($this));
3047			}
3048			$sRemoteClass = $oKeyAttDef->GetTargetClass();
3049			$oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false);
3050			if (is_null($oRemoteObj))
3051			{
3052				$ret = Dict::S('UI:UndefinedObject');
3053			}
3054			else
3055			{
3056				// Recurse
3057				$ret  = $oRemoteObj->GetForTemplate($sRemoteAttCode);
3058			}
3059		}
3060		else
3061		{
3062			switch($sPlaceholderAttCode)
3063			{
3064				case 'id':
3065				$ret = $this->GetKey();
3066				break;
3067
3068				case 'name()':
3069				$ret = $this->GetName();
3070				break;
3071
3072				default:
3073				if (preg_match('/^([^(]+)\\((.*)\\)$/', $sPlaceholderAttCode, $aMatches))
3074				{
3075					$sVerb = $aMatches[1];
3076					$sAttCode = $aMatches[2];
3077				}
3078				else
3079				{
3080					$sVerb = '';
3081					$sAttCode = $sPlaceholderAttCode;
3082				}
3083
3084				if ($sVerb == 'hyperlink')
3085				{
3086					$sPortalId = ($sAttCode === '') ? 'console' : $sAttCode;
3087					if (!array_key_exists($sPortalId, self::$aPortalToURLMaker))
3088					{
3089						throw new Exception("Unknown portal id '$sPortalId' in placeholder '$sPlaceholderAttCode''");
3090					}
3091					$ret = $this->GetHyperlink(self::$aPortalToURLMaker[$sPortalId], false);
3092				}
3093				else
3094				{
3095					$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
3096					$ret = $oAttDef->GetForTemplate($this->Get($sAttCode), $sVerb, $this);
3097				}
3098			}
3099			if ($ret === null)
3100			{
3101				$ret = '';
3102			}
3103		}
3104		return $ret;
3105	}
3106
3107	static protected $aPortalToURLMaker = array('console' => 'iTopStandardURLMaker', 'portal' => 'PortalURLMaker');
3108
3109	/**
3110	 * Associate a portal to a class that implements iDBObjectURLMaker,
3111	 * and which will be invoked with placeholders like $this->org_id->hyperlink(portal)$
3112	 *
3113	 * @param string $sPortalId Identifies the portal. Conventions: the main portal is 'console', The user requests portal is 'portal'.
3114	 * @param string $sUrlMakerClass
3115	 */
3116	static public function RegisterURLMakerClass($sPortalId, $sUrlMakerClass)
3117	{
3118		self::$aPortalToURLMaker[$sPortalId] = $sUrlMakerClass;
3119	}
3120
3121	/**
3122	 * Can be overloaded
3123	 *
3124	 * @api
3125	 */
3126	protected function OnInsert()
3127	{
3128	}
3129
3130	/**
3131	 * Can be overloaded
3132	 *
3133	 * @api
3134	 */
3135	protected function AfterInsert()
3136	{
3137	}
3138
3139	/**
3140	 * Can be overloaded
3141	 *
3142	 * @api
3143	 */
3144	protected function OnUpdate()
3145	{
3146	}
3147
3148	/**
3149	 * Can be overloaded
3150	 *
3151	 * @api
3152	 */
3153	protected function AfterUpdate()
3154	{
3155	}
3156
3157	/**
3158	 * Can be overloaded
3159	 *
3160	 * @api
3161	 */
3162	protected function OnDelete()
3163	{
3164	}
3165
3166	/**
3167	 * Can be overloaded
3168	 *
3169	 * @api
3170	 */
3171	protected function AfterDelete()
3172	{
3173	}
3174
3175
3176	/**
3177	 * Common to the recording of link set changes (add/remove/modify)
3178	 *
3179	 * @param $iLinkSetOwnerId
3180	 * @param \AttributeLinkedSet $oLinkSet
3181	 * @param $sChangeOpClass
3182	 * @param array $aOriginalValues
3183	 *
3184	 * @return \DBObject|null
3185	 * @throws \ArchivedObjectException
3186	 * @throws \CoreException
3187	 * @throws \CoreUnexpectedValue
3188	 */
3189	private function PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, $sChangeOpClass, $aOriginalValues = null)
3190	{
3191		if ($iLinkSetOwnerId <= 0)
3192		{
3193			return null;
3194		}
3195
3196		if (!is_subclass_of($oLinkSet->GetHostClass(), 'CMDBObject'))
3197		{
3198			// The link set owner class does not keep track of its history
3199			return null;
3200		}
3201
3202		// Determine the linked item class and id
3203		//
3204		if ($oLinkSet->IsIndirect())
3205		{
3206			// The "item" is on the other end (N-N links)
3207			/** @var \AttributeLinkedSetIndirect $oLinkSet */
3208			$sExtKeyToRemote = $oLinkSet->GetExtKeyToRemote();
3209			$oExtKeyToRemote = MetaModel::GetAttributeDef(get_class($this), $sExtKeyToRemote);
3210			/** @var \AttributeExternalKey $oExtKeyToRemote */
3211			$sItemClass = $oExtKeyToRemote->GetTargetClass();
3212			if ($aOriginalValues)
3213			{
3214				// Get the value from the original values
3215				$iItemId = $aOriginalValues[$sExtKeyToRemote];
3216			}
3217			else
3218			{
3219				$iItemId = $this->Get($sExtKeyToRemote);
3220			}
3221		}
3222		else
3223		{
3224			// I am the "item" (1-N links)
3225			$sItemClass = get_class($this);
3226			$iItemId = $this->GetKey();
3227		}
3228
3229		// Get the remote object, to determine its exact class
3230		// Possible optimization: implement a tool in MetaModel, to get the final class of an object (not always querying + query reduced to a select on the root table!
3231		$oOwner = MetaModel::GetObject($oLinkSet->GetHostClass(), $iLinkSetOwnerId, false);
3232		if ($oOwner)
3233		{
3234			$sLinkSetOwnerClass = get_class($oOwner);
3235
3236			$oMyChangeOp = MetaModel::NewObject($sChangeOpClass);
3237			$oMyChangeOp->Set("objclass", $sLinkSetOwnerClass);
3238			$oMyChangeOp->Set("objkey", $iLinkSetOwnerId);
3239			$oMyChangeOp->Set("attcode", $oLinkSet->GetCode());
3240			$oMyChangeOp->Set("item_class", $sItemClass);
3241			$oMyChangeOp->Set("item_id", $iItemId);
3242			return $oMyChangeOp;
3243		}
3244		else
3245		{
3246			// Depending on the deletion order, it may happen that the id is already invalid... ignore
3247			return null;
3248		}
3249	}
3250
3251	/**
3252	 * This object has been created/deleted, record that as a change in link sets pointing to this (if any)
3253	 *
3254	 * @internal
3255	 */
3256	private function RecordLinkSetListChange($bAdd = true)
3257	{
3258		foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet)
3259		{
3260			/** @var \AttributeLinkedSet $oLinkSet */
3261			if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue;
3262
3263			$iLinkSetOwnerId  = $this->Get($sExtKeyAttCode);
3264			$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove');
3265			if ($oMyChangeOp)
3266			{
3267				if ($bAdd)
3268				{
3269					$oMyChangeOp->Set("type", "added");
3270				}
3271				else
3272				{
3273					$oMyChangeOp->Set("type", "removed");
3274				}
3275				$oMyChangeOp->DBInsertNoReload();
3276			}
3277		}
3278	}
3279
3280	/**
3281	 * @internal
3282	 */
3283	protected function RecordObjCreation()
3284	{
3285		$this->RecordLinkSetListChange(true);
3286	}
3287
3288	protected function RecordObjDeletion($objkey)
3289	{
3290		$this->RecordLinkSetListChange(false);
3291	}
3292
3293	protected function RecordAttChanges(array $aValues, array $aOrigValues)
3294	{
3295		foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet)
3296		{
3297
3298			if (array_key_exists($sExtKeyAttCode, $aValues))
3299			{
3300				/** @var \AttributeLinkedSet $oLinkSet */
3301				if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue;
3302
3303				// Keep track of link added/removed
3304				//
3305				$iLinkSetOwnerNext = $aValues[$sExtKeyAttCode];
3306				$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerNext, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove');
3307				if ($oMyChangeOp)
3308				{
3309					$oMyChangeOp->Set("type", "added");
3310					$oMyChangeOp->DBInsertNoReload();
3311				}
3312
3313				$iLinkSetOwnerPrevious = $aOrigValues[$sExtKeyAttCode];
3314				$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerPrevious, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove', $aOrigValues);
3315				if ($oMyChangeOp)
3316				{
3317					$oMyChangeOp->Set("type", "removed");
3318					$oMyChangeOp->DBInsertNoReload();
3319				}
3320			}
3321			else
3322			{
3323				// Keep track of link changes
3324				//
3325				if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_DETAILS) == 0) continue;
3326
3327				$iLinkSetOwnerId  = $this->Get($sExtKeyAttCode);
3328				$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksTune');
3329				if ($oMyChangeOp)
3330				{
3331					$oMyChangeOp->Set("link_id", $this->GetKey());
3332					$oMyChangeOp->DBInsertNoReload();
3333				}
3334			}
3335		}
3336	}
3337
3338	// Return an empty set for the parent of all
3339	// May be overloaded.
3340	// Anyhow, this way of implementing the relations suffers limitations (not handling the redundancy)
3341	// and you should consider defining those things in XML.
3342	public static function GetRelationQueries($sRelCode)
3343	{
3344		return array();
3345	}
3346
3347	// Reserved: do not overload
3348	public static function GetRelationQueriesEx($sRelCode)
3349	{
3350		return array();
3351	}
3352
3353	/**
3354	 * Will be deprecated soon - use GetRelatedObjectsDown/Up instead to take redundancy into account
3355	 */
3356	public function GetRelatedObjects($sRelCode, $iMaxDepth = 99, &$aResults = array())
3357	{
3358		// Temporary patch: until the impact analysis GUI gets rewritten,
3359		// let's consider that "depends on" is equivalent to "impacts/up"
3360		// The current patch has been implemented in DBObject and MetaModel
3361		$sHackedRelCode = $sRelCode;
3362		$bDown = true;
3363		if ($sRelCode == 'depends on')
3364		{
3365			$sHackedRelCode = 'impacts';
3366			$bDown = false;
3367		}
3368		foreach (MetaModel::EnumRelationQueries(get_class($this), $sHackedRelCode, $bDown) as $sDummy => $aQueryInfo)
3369		{
3370			$sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp'];
3371			//$bPropagate = $aQueryInfo["bPropagate"];
3372			//$iDepth = $bPropagate ? $iMaxDepth - 1 : 0;
3373			$iDepth = $iMaxDepth - 1;
3374
3375			// Note: the loop over the result set has been written in an unusual way for error reporting purposes
3376			// In the case of a wrong query parameter name, the error occurs on the first call to Fetch,
3377			// thus we need to have this first call into the try/catch, but
3378			// we do NOT want to nest the try/catch for the error message to be clear
3379			try
3380			{
3381				$oFlt = DBObjectSearch::FromOQL($sQuery);
3382				$oObjSet = new DBObjectSet($oFlt, array(), $this->ToArgsForQuery());
3383				$oObj = $oObjSet->Fetch();
3384			}
3385			catch (Exception $e)
3386			{
3387				$sClassOfDefinition = $aQueryInfo['_legacy_'] ? get_class($this).'(or a parent)::GetRelationQueries()' : $aQueryInfo['sDefinedInClass'];
3388				throw new Exception("Wrong query for the relation $sRelCode/$sClassOfDefinition/{$aQueryInfo['sNeighbour']}: ".$e->getMessage());
3389			}
3390			if ($oObj)
3391			{
3392				do
3393				{
3394					$sRootClass = MetaModel::GetRootClass(get_class($oObj));
3395					$sObjKey = $oObj->GetKey();
3396					if (array_key_exists($sRootClass, $aResults))
3397					{
3398						if (array_key_exists($sObjKey, $aResults[$sRootClass]))
3399						{
3400							continue; // already visited, skip
3401						}
3402					}
3403
3404					$aResults[$sRootClass][$sObjKey] = $oObj;
3405					if ($iDepth > 0)
3406					{
3407						$oObj->GetRelatedObjects($sRelCode, $iDepth, $aResults);
3408					}
3409				}
3410				while ($oObj = $oObjSet->Fetch());
3411			}
3412		}
3413		return $aResults;
3414	}
3415
3416	/**
3417	 * Compute the "RelatedObjects" (forward or "down" direction) for the object
3418	 * for the specified relation
3419	 *
3420	 * @param string $sRelCode The code of the relation to use for the computation
3421	 * @param int $iMaxDepth Maximum recursion depth
3422	 * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy
3423	 *
3424	 * @return RelationGraph The graph of all the related objects
3425	 */
3426	public function GetRelatedObjectsDown($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true)
3427	{
3428		$oGraph = new RelationGraph();
3429		$oGraph->AddSourceObject($this);
3430		$oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy);
3431		return $oGraph;
3432	}
3433
3434	/**
3435	 * Compute the "RelatedObjects" (reverse or "up" direction) for the object
3436	 * for the specified relation
3437	 *
3438	 * @param string $sRelCode The code of the relation to use for the computation
3439	 * @param int $iMaxDepth Maximum recursion depth
3440	 * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy
3441	 *
3442	 * @return RelationGraph The graph of all the related objects
3443	 */
3444	public function GetRelatedObjectsUp($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true)
3445	{
3446		$oGraph = new RelationGraph();
3447		$oGraph->AddSourceObject($this);
3448		$oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy);
3449		return $oGraph;
3450	}
3451
3452	public function GetReferencingObjects($bAllowAllData = false)
3453	{
3454		$aDependentObjects = array();
3455		$aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this));
3456		foreach($aRererencingMe as $sRemoteClass => $aExtKeys)
3457		{
3458			foreach($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef)
3459			{
3460				// skip if this external key is behind an external field
3461				/** @var \AttributeDefinition $oExtKeyAttDef */
3462				if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) continue;
3463
3464				$oSearch = new DBObjectSearch($sRemoteClass);
3465				$oSearch->AddCondition($sExtKeyAttCode, $this->GetKey(), '=');
3466				if ($bAllowAllData)
3467				{
3468					$oSearch->AllowAllData();
3469				}
3470				$oSet = new CMDBObjectSet($oSearch);
3471				if ($oSet->CountExceeds(0))
3472				{
3473					$aDependentObjects[$sRemoteClass][$sExtKeyAttCode] = array(
3474						'attribute' => $oExtKeyAttDef,
3475						'objects' => $oSet,
3476					);
3477				}
3478			}
3479		}
3480		return $aDependentObjects;
3481	}
3482
3483	/**
3484	 * @param \DeletionPlan $oDeletionPlan
3485	 * @param array $aVisited
3486	 * @param int $iDeleteOption
3487	 *
3488	 * @throws \CoreException
3489	 */
3490	private function MakeDeletionPlan(&$oDeletionPlan, $aVisited = array(), $iDeleteOption = null)
3491	{
3492		static $iLoopTimeLimit = null;
3493		if ($iLoopTimeLimit == null)
3494		{
3495			$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
3496		}
3497		$sClass = get_class($this);
3498		$iThisId = $this->GetKey();
3499
3500		$oDeletionPlan->AddToDelete($this, $iDeleteOption);
3501
3502		if (array_key_exists($sClass, $aVisited))
3503		{
3504			if (in_array($iThisId, $aVisited[$sClass]))
3505			{
3506				return;
3507			}
3508		}
3509		$aVisited[$sClass] = $iThisId;
3510
3511		if ($iDeleteOption == DEL_MANUAL)
3512		{
3513			// Stop the recursion here
3514			return;
3515		}
3516		// Check the node itself
3517		$this->DoCheckToDelete($oDeletionPlan);
3518		$oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue);
3519
3520		$aDependentObjects = $this->GetReferencingObjects(true /* allow all data */);
3521
3522		// Getting and setting time limit are not symetric:
3523		// www.php.net/manual/fr/function.set-time-limit.php#72305
3524		$iPreviousTimeLimit = ini_get('max_execution_time');
3525
3526		foreach ($aDependentObjects as $sRemoteClass => $aPotentialDeletes)
3527		{
3528			foreach ($aPotentialDeletes as $sRemoteExtKey => $aData)
3529			{
3530				set_time_limit($iLoopTimeLimit);
3531
3532				/** @var \AttributeExternalKey $oAttDef */
3533				$oAttDef = $aData['attribute'];
3534				$iDeletePropagationOption = $oAttDef->GetDeletionPropagationOption();
3535				/** @var \DBObjectSet $oDepSet */
3536				$oDepSet = $aData['objects'];
3537				$oDepSet->Rewind();
3538				while ($oDependentObj = $oDepSet->fetch())
3539				{
3540					if ($oAttDef->IsNullAllowed())
3541					{
3542						// Optional external key, list to reset
3543						if (($iDeletePropagationOption == DEL_MOVEUP) && ($oAttDef->IsHierarchicalKey()))
3544						{
3545							// Move the child up one level i.e. set the same parent as the current object
3546							$iParentId = $this->Get($oAttDef->GetCode());
3547							$oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef, $iParentId);
3548						}
3549						else
3550						{
3551							$oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef);
3552						}
3553					}
3554					else
3555					{
3556						// Mandatory external key, list to delete
3557						$oDependentObj->MakeDeletionPlan($oDeletionPlan, $aVisited, $iDeletePropagationOption);
3558					}
3559				}
3560			}
3561		}
3562		set_time_limit($iPreviousTimeLimit);
3563	}
3564
3565	/**
3566	 * WILL DEPRECATED SOON
3567	 * Caching relying on an object set is not efficient since 2.0.3
3568	 * Use GetSynchroData instead
3569	 *
3570	 * Get all the synchro replica related to this object
3571	 *
3572	 * @return DBObjectSet Set with two columns: R=SynchroReplica S=SynchroDataSource
3573	 * @throws \OQLException
3574	 */
3575	public function GetMasterReplica()
3576	{
3577		$sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id";
3578		$oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey()));
3579		return $oReplicaSet;
3580	}
3581
3582	/**
3583	 * Get all the synchro data related to this object
3584	 *
3585	 * @return array of data_source_id => array
3586	 *    'source' => $oSource,
3587	 *    'attributes' => array of $oSynchroAttribute
3588	 *    'replica' => array of $oReplica (though only one should exist, misuse of the data sync can have this consequence)
3589	 * @throws \CoreException
3590	 * @throws \CoreUnexpectedValue
3591	 * @throws \MySQLException
3592	 * @throws \OQLException
3593	 */
3594	public function GetSynchroData()
3595	{
3596		if (is_null($this->m_aSynchroData))
3597		{
3598			$sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id";
3599			$oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey()));
3600			$this->m_aSynchroData = array();
3601			while($aData = $oReplicaSet->FetchAssoc())
3602			{
3603				/** @var \DBObject[] $aData */
3604				$iSourceId = $aData['datasource']->GetKey();
3605				if (!array_key_exists($iSourceId, $this->m_aSynchroData))
3606				{
3607					$aAttributes = array();
3608					$oAttrSet = $aData['datasource']->Get('attribute_list');
3609					while($oSyncAttr = $oAttrSet->Fetch())
3610					{
3611						/** @var \DBObject $oSyncAttr */
3612						$aAttributes[$oSyncAttr->Get('attcode')] = $oSyncAttr;
3613					}
3614					$this->m_aSynchroData[$iSourceId] = array(
3615						'source' => $aData['datasource'],
3616						'attributes' => $aAttributes,
3617						'replica' => array()
3618					);
3619				}
3620				// Assumption: $aData['datasource'] will not be null because the data source id is always set...
3621				$this->m_aSynchroData[$iSourceId]['replica'][] = $aData['replica'];
3622			}
3623		}
3624		return $this->m_aSynchroData;
3625	}
3626
3627	public function GetSynchroReplicaFlags($sAttCode, &$aReason)
3628	{
3629		$iFlags = OPT_ATT_NORMAL;
3630		foreach ($this->GetSynchroData() as $iSourceId => $aSourceData)
3631		{
3632			if ($iSourceId == SynchroExecution::GetCurrentTaskId())
3633			{
3634				// Ignore the current task (check to write => ok)
3635				continue;
3636			}
3637			// Assumption: one replica - take the first one!
3638			$oReplica = reset($aSourceData['replica']);
3639			$oSource = $aSourceData['source'];
3640			if (array_key_exists($sAttCode, $aSourceData['attributes']))
3641			{
3642				/** @var \DBObject $oSyncAttr */
3643				$oSyncAttr = $aSourceData['attributes'][$sAttCode];
3644				if (($oSyncAttr->Get('update') == 1) && ($oSyncAttr->Get('update_policy') == 'master_locked'))
3645				{
3646					$iFlags |= OPT_ATT_SLAVE;
3647					/** @var \SynchroDataSource $oSource */
3648					$sUrl = $oSource->GetApplicationUrl($this, $oReplica);
3649					$aReason[] = array('name' => $oSource->GetName(), 'description' => $oSource->Get('description'), 'url_application' => $sUrl);
3650				}
3651			}
3652		}
3653		return $iFlags;
3654	}
3655
3656	/**
3657	 * @return bool true if this object is used in a data synchro
3658	 * @throws \CoreException
3659	 * @throws \CoreUnexpectedValue
3660	 * @throws \MySQLException
3661	 * @throws \OQLException
3662	 * @internal
3663	 * @see \SynchroDataSource
3664	 */
3665	public function InSyncScope()
3666	{
3667		//
3668		// Optimization: cache the list of Data Sources and classes candidates for synchro
3669		//
3670		static $aSynchroClasses = null;
3671		if (is_null($aSynchroClasses))
3672		{
3673			$aSynchroClasses = array();
3674			$sOQL = "SELECT SynchroDataSource AS datasource";
3675			$oSourceSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array());
3676			while($oSource = $oSourceSet->Fetch())
3677			{
3678				$sTarget = $oSource->Get('scope_class');
3679				$aSynchroClasses[] = $sTarget;
3680			}
3681		}
3682
3683		foreach($aSynchroClasses as $sClass)
3684		{
3685			if ($this instanceof $sClass)
3686			{
3687				return true;
3688			}
3689		}
3690		return false;
3691	}
3692	/////////////////////////////////////////////////////////////////////////
3693	//
3694	// Experimental iDisplay implementation
3695	//
3696	/////////////////////////////////////////////////////////////////////////
3697
3698	public static function MapContextParam($sContextParam)
3699	{
3700		return null;
3701	}
3702
3703	public function GetHilightClass()
3704	{
3705		$sCode = $this->ComputeHighlightCode();
3706		if($sCode != '')
3707		{
3708			$aHighlightScale = MetaModel::GetHighlightScale(get_class($this));
3709			if (array_key_exists($sCode, $aHighlightScale))
3710			{
3711				return $aHighlightScale[$sCode]['color'];
3712			}
3713		}
3714		return HILIGHT_CLASS_NONE;
3715	}
3716
3717	public function DisplayDetails(WebPage $oPage, $bEditMode = false)
3718	{
3719		$oPage->add('<h1>'.MetaModel::GetName(get_class($this)).': '.$this->GetName().'</h1>');
3720		$aValues = array();
3721		$aList = MetaModel::FlattenZList(MetaModel::GetZListItems(get_class($this), 'details'));
3722		if (empty($aList))
3723		{
3724			$aList = array_keys(MetaModel::ListAttributeDefs(get_class($this)));
3725		}
3726		foreach($aList as $sAttCode)
3727		{
3728			$aValues[$sAttCode] = array('label' => MetaModel::GetLabel(get_class($this), $sAttCode), 'value' => $this->GetAsHTML($sAttCode));
3729		}
3730		$oPage->details($aValues);
3731	}
3732
3733
3734	const CALLBACK_AFTERINSERT = 0;
3735
3736	/**
3737	 * Register a call back that will be called when some internal event happens
3738	 *
3739	 * @param $iType string Any of the CALLBACK_x constants
3740	 * @param $callback callable Call specification like a function name, or array('<class>', '<method>') or array($object, '<method>')
3741	 * @param $aParameters array Values that will be passed to the callback, after $this
3742	 *
3743	 * @throws \Exception
3744	 */
3745	public function RegisterCallback($iType, $callback, $aParameters = array())
3746	{
3747		$sCallBackName = '';
3748		if (!is_callable($callback, false, $sCallBackName))
3749		{
3750			throw new Exception('Registering an unknown/protected function or wrong syntax for the call spec: '.$sCallBackName);
3751		}
3752		$this->m_aCallbacks[$iType][] = array(
3753			'callback' => $callback,
3754			'params' => $aParameters
3755		);
3756	}
3757
3758	/**
3759	 * Computes a text-like fingerprint identifying the content of the object
3760	 * but excluding the specified columns
3761	 * @param $aExcludedColumns array The list of columns to exclude
3762	 * @return string
3763	 */
3764	public function Fingerprint($aExcludedColumns = array())
3765	{
3766		$sFingerprint = '';
3767		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
3768		{
3769			if (!in_array($sAttCode, $aExcludedColumns))
3770			{
3771				if ($oAttDef->IsPartOfFingerprint())
3772				{
3773					$sFingerprint .= chr(0).$oAttDef->Fingerprint($this->Get($sAttCode));
3774				}
3775			}
3776		}
3777		return $sFingerprint;
3778	}
3779
3780	/**
3781	 * Execute a set of scripted actions onto the current object
3782	 * See ExecAction for the syntax and features of the scripted actions
3783	 *
3784	 * @param $aActions array of statements (e.g. "set(name, Made after $source->name$)")
3785	 * @param $aSourceObjects array of Alias => Context objects (Convention: some statements require the 'source' element
3786	 * @throws Exception
3787	 */
3788	public function ExecActions($aActions, $aSourceObjects)
3789	{
3790		foreach($aActions as $sAction)
3791		{
3792			try
3793			{
3794				if (preg_match('/^(\S*)\s*\((.*)\)$/ms', $sAction, $aMatches)) // multiline and newline matched by a dot
3795				{
3796					$sVerb = trim($aMatches[1]);
3797					$sParams = $aMatches[2];
3798
3799					// the coma is the separator for the parameters
3800					// comas can be escaped: \,
3801					$sParams = str_replace(array("\\\\", "\\,"), array("__backslash__", "__coma__"), $sParams);
3802					$sParams = trim($sParams);
3803
3804					if (strlen($sParams) == 0)
3805					{
3806						$aParams = array();
3807					}
3808					else
3809					{
3810						$aParams = explode(',', $sParams);
3811						foreach ($aParams as &$sParam)
3812						{
3813							$sParam = str_replace(array("__backslash__", "__coma__"), array("\\", ","), $sParam);
3814							$sParam = trim($sParam);
3815						}
3816					}
3817					$this->ExecAction($sVerb, $aParams, $aSourceObjects);
3818				}
3819				else
3820				{
3821					throw new Exception("Invalid syntax");
3822				}
3823			}
3824			catch(Exception $e)
3825			{
3826				throw new Exception('Action: '.$sAction.' - '.$e->getMessage());
3827			}
3828		}
3829	}
3830
3831	/**
3832	 * Helper to copy an attribute between two objects (in memory)
3833	 * Originally designed for ExecAction()
3834	 *
3835	 * @param \DBObject $oSourceObject
3836	 * @param $sSourceAttCode
3837	 * @param $sDestAttCode
3838	 *
3839	 * @throws \CoreException
3840	 * @throws \CoreUnexpectedValue
3841	 * @throws \MySQLException
3842	 */
3843	public function CopyAttribute($oSourceObject, $sSourceAttCode, $sDestAttCode)
3844	{
3845		if ($sSourceAttCode == 'id')
3846		{
3847			$oSourceAttDef = null;
3848		}
3849		else
3850		{
3851			if (!MetaModel::IsValidAttCode(get_class($this), $sDestAttCode))
3852			{
3853				throw new Exception("Unknown attribute ".get_class($this)."::".$sDestAttCode);
3854			}
3855			if (!MetaModel::IsValidAttCode(get_class($oSourceObject), $sSourceAttCode))
3856			{
3857				throw new Exception("Unknown attribute ".get_class($oSourceObject)."::".$sSourceAttCode);
3858			}
3859
3860			$oSourceAttDef = MetaModel::GetAttributeDef(get_class($oSourceObject), $sSourceAttCode);
3861		}
3862		if (is_object($oSourceAttDef) && $oSourceAttDef->IsLinkSet())
3863		{
3864			// The copy requires that we create a new object set (the semantic of DBObject::Set is unclear about link sets)
3865			/** @var \AttributeLinkedSet $oSourceAttDef */
3866			$oDestSet = DBObjectSet::FromScratch($oSourceAttDef->GetLinkedClass());
3867			$oSourceSet = $oSourceObject->Get($sSourceAttCode);
3868			$oSourceSet->Rewind();
3869			/** @var \DBObject $oSourceLink */
3870			while ($oSourceLink = $oSourceSet->Fetch())
3871			{
3872				// Clone the link
3873				$sLinkClass = get_class($oSourceLink);
3874				$oLinkClone = MetaModel::NewObject($sLinkClass);
3875				foreach(MetaModel::ListAttributeDefs($sLinkClass) as $sAttCode => $oAttDef)
3876				{
3877					// As of now, ignore other attribute (do not attempt to recurse!)
3878					if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
3879					{
3880						$oLinkClone->Set($sAttCode, $oSourceLink->Get($sAttCode));
3881					}
3882				}
3883
3884				// Not necessary - this will be handled by DBObject
3885				// $oLinkClone->Set($oSourceAttDef->GetExtKeyToMe(), 0);
3886				$oDestSet->AddObject($oLinkClone);
3887			}
3888			$this->Set($sDestAttCode, $oDestSet);
3889		}
3890		else
3891		{
3892			$this->Set($sDestAttCode, $oSourceObject->Get($sSourceAttCode));
3893		}
3894	}
3895
3896	/**
3897	 * Execute a scripted action onto the current object
3898	 *    - clone (att1, att2, att3, ...)
3899	 *    - clone_scalars ()
3900	 *    - copy (source_att, dest_att)
3901	 *    - reset (att)
3902	 *    - nullify (att)
3903	 *    - set (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...))
3904	 *    - append (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...))
3905	 *    - add_to_list (source_key_att, dest_att)
3906	 *    - add_to_list (source_key_att, dest_att, lnk_att, lnk_att_value)
3907	 *    - apply_stimulus (stimulus)
3908	 *    - call_method (method_name)
3909	 *
3910	 * @param $sVerb string Any of the verb listed above (e.g. "set")
3911	 * @param $aParams array of strings (e.g. array('name', 'copied from $source->name$')
3912	 * @param $aSourceObjects array of Alias => Context objects (Convention: some statements require the 'source' element
3913	 * @throws CoreException
3914	 * @throws CoreUnexpectedValue
3915	 * @throws Exception
3916	 */
3917	public function ExecAction($sVerb, $aParams, $aSourceObjects)
3918	{
3919		switch($sVerb)
3920		{
3921			case 'clone':
3922				if (!array_key_exists('source', $aSourceObjects))
3923				{
3924					throw new Exception('Missing conventional "source" object');
3925				}
3926				$oObjectToRead = $aSourceObjects['source'];
3927				foreach($aParams as $sAttCode)
3928				{
3929					$this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode);
3930				}
3931				break;
3932
3933			case 'clone_scalars':
3934				if (!array_key_exists('source', $aSourceObjects))
3935				{
3936					throw new Exception('Missing conventional "source" object');
3937				}
3938				$oObjectToRead = $aSourceObjects['source'];
3939				foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
3940				{
3941					if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
3942					{
3943						$this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode);
3944					}
3945				}
3946				break;
3947
3948			case 'copy':
3949				if (!array_key_exists('source', $aSourceObjects))
3950				{
3951					throw new Exception('Missing conventional "source" object');
3952				}
3953				$oObjectToRead = $aSourceObjects['source'];
3954				if (!array_key_exists(0, $aParams))
3955				{
3956					throw new Exception('Missing argument #1: source attribute');
3957				}
3958				$sSourceAttCode = $aParams[0];
3959				if (!array_key_exists(1, $aParams))
3960				{
3961					throw new Exception('Missing argument #2: target attribute');
3962				}
3963				$sDestAttCode = $aParams[1];
3964				$this->CopyAttribute($oObjectToRead, $sSourceAttCode, $sDestAttCode);
3965				break;
3966
3967			case 'reset':
3968				if (!array_key_exists(0, $aParams))
3969				{
3970					throw new Exception('Missing argument #1: target attribute');
3971				}
3972				$sAttCode = $aParams[0];
3973				if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
3974				{
3975					throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
3976				}
3977				$this->Set($sAttCode, $this->GetDefaultValue($sAttCode));
3978				break;
3979
3980			case 'nullify':
3981				if (!array_key_exists(0, $aParams))
3982				{
3983					throw new Exception('Missing argument #1: target attribute');
3984				}
3985				$sAttCode = $aParams[0];
3986				if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
3987				{
3988					throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
3989				}
3990				$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
3991				$this->Set($sAttCode, $oAttDef->GetNullValue());
3992				break;
3993
3994			case 'set':
3995				if (!array_key_exists(0, $aParams))
3996				{
3997					throw new Exception('Missing argument #1: target attribute');
3998				}
3999				$sAttCode = $aParams[0];
4000				if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
4001				{
4002					throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
4003				}
4004				if (!array_key_exists(1, $aParams))
4005				{
4006					throw new Exception('Missing argument #2: value to set');
4007				}
4008				$sRawValue = $aParams[1];
4009				$aContext = array();
4010				foreach ($aSourceObjects as $sAlias => $oObject)
4011				{
4012					$aContext = array_merge($aContext, $oObject->ToArgs($sAlias));
4013				}
4014				$aContext['current_contact_id'] = UserRights::GetContactId();
4015				$aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName();
4016				$aContext['current_date'] = date(AttributeDate::GetSQLFormat());
4017				$aContext['current_time'] = date(AttributeDateTime::GetSQLTimeFormat());
4018				$sValue = MetaModel::ApplyParams($sRawValue, $aContext);
4019				$this->Set($sAttCode, $sValue);
4020				break;
4021
4022			case 'append':
4023				if (!array_key_exists(0, $aParams))
4024				{
4025					throw new Exception('Missing argument #1: target attribute');
4026				}
4027				$sAttCode = $aParams[0];
4028				if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
4029				{
4030					throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
4031				}
4032				if (!array_key_exists(1, $aParams))
4033				{
4034					throw new Exception('Missing argument #2: value to append');
4035				}
4036				$sRawAddendum = $aParams[1];
4037				$aContext = array();
4038				foreach ($aSourceObjects as $sAlias => $oObject)
4039				{
4040					$aContext = array_merge($aContext, $oObject->ToArgs($sAlias));
4041				}
4042				$aContext['current_contact_id'] = UserRights::GetContactId();
4043				$aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName();
4044				$aContext['current_date'] = date(AttributeDate::GetSQLFormat());
4045				$aContext['current_time'] = date(AttributeDateTime::GetSQLTimeFormat());
4046				$sAddendum = MetaModel::ApplyParams($sRawAddendum, $aContext);
4047				$this->Set($sAttCode, $this->Get($sAttCode).$sAddendum);
4048				break;
4049
4050			case 'add_to_list':
4051				if (!array_key_exists('source', $aSourceObjects))
4052				{
4053					throw new Exception('Missing conventional "source" object');
4054				}
4055				$oObjectToRead = $aSourceObjects['source'];
4056				if (!array_key_exists(0, $aParams))
4057				{
4058					throw new Exception('Missing argument #1: source attribute');
4059				}
4060				$sSourceKeyAttCode = $aParams[0];
4061				if (($sSourceKeyAttCode != 'id') && !MetaModel::IsValidAttCode(get_class($oObjectToRead), $sSourceKeyAttCode))
4062				{
4063					throw new Exception("Unknown attribute ".get_class($oObjectToRead)."::".$sSourceKeyAttCode);
4064				}
4065				if (!array_key_exists(1, $aParams))
4066				{
4067					throw new Exception('Missing argument #2: target attribute (link set)');
4068				}
4069				$sTargetListAttCode = $aParams[1]; // indirect !!!
4070				if (!MetaModel::IsValidAttCode(get_class($this), $sTargetListAttCode))
4071				{
4072					throw new Exception("Unknown attribute ".get_class($this)."::".$sTargetListAttCode);
4073				}
4074				if (isset($aParams[2]) && isset($aParams[3]))
4075				{
4076					$sRoleAttCode = $aParams[2];
4077					$sRoleValue = $aParams[3];
4078				}
4079
4080				$iObjKey = $oObjectToRead->Get($sSourceKeyAttCode);
4081				if ($iObjKey > 0)
4082				{
4083					$oLinkSet = $this->Get($sTargetListAttCode);
4084
4085					/** @var \AttributeLinkedSetIndirect $oListAttDef */
4086					$oListAttDef = MetaModel::GetAttributeDef(get_class($this), $sTargetListAttCode);
4087					/** @var \AttributeLinkedSet $oListAttDef */
4088					$oLnk = MetaModel::NewObject($oListAttDef->GetLinkedClass());
4089					$oLnk->Set($oListAttDef->GetExtKeyToRemote(), $iObjKey);
4090					if (isset($sRoleAttCode))
4091					{
4092						if (!MetaModel::IsValidAttCode(get_class($oLnk), $sRoleAttCode))
4093						{
4094							throw new Exception("Unknown attribute ".get_class($oLnk)."::".$sRoleAttCode);
4095						}
4096						$oLnk->Set($sRoleAttCode, $sRoleValue);
4097					}
4098					$oLinkSet->AddObject($oLnk);
4099					$this->Set($sTargetListAttCode, $oLinkSet);
4100				}
4101				break;
4102
4103			case 'apply_stimulus':
4104				if (!array_key_exists(0, $aParams))
4105				{
4106					throw new Exception('Missing argument #1: stimulus');
4107				}
4108				$sStimulus = $aParams[0];
4109				if (!in_array($sStimulus, MetaModel::EnumStimuli(get_class($this))))
4110				{
4111					throw new Exception("Unknown stimulus ".get_class($this)."::".$sStimulus);
4112				}
4113				$this->ApplyStimulus($sStimulus);
4114				break;
4115
4116			case 'call_method':
4117				if (!array_key_exists('source', $aSourceObjects))
4118				{
4119					throw new Exception('Missing conventional "source" object');
4120				}
4121				$oObjectToRead = $aSourceObjects['source'];
4122				if (!array_key_exists(0, $aParams))
4123				{
4124					throw new Exception('Missing argument #1: method name');
4125				}
4126				$sMethod = $aParams[0];
4127				$aCallSpec = array($this, $sMethod);
4128				if (!is_callable($aCallSpec))
4129				{
4130					throw new Exception("Unknown method ".get_class($this)."::".$sMethod.'()');
4131				}
4132				// Note: $oObjectToRead has been preserved when adding $aSourceObjects, so as to remain backward compatible with methods having only 1 parameter ($oObjectToRead�
4133				call_user_func($aCallSpec, $oObjectToRead, $aSourceObjects);
4134				break;
4135
4136			default:
4137				throw new Exception("Invalid verb");
4138		}
4139	}
4140
4141	public function IsArchived($sKeyAttCode = null)
4142	{
4143		$bRet = false;
4144		$sFlagAttCode = is_null($sKeyAttCode) ? 'archive_flag' : $sKeyAttCode.'_archive_flag';
4145		if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode))
4146		{
4147			$bRet = true;
4148		}
4149		return $bRet;
4150	}
4151
4152	public function IsObsolete($sKeyAttCode = null)
4153	{
4154		$bRet = false;
4155		$sFlagAttCode = is_null($sKeyAttCode) ? 'obsolescence_flag' : $sKeyAttCode.'_obsolescence_flag';
4156		if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode))
4157		{
4158			$bRet = true;
4159		}
4160		return $bRet;
4161	}
4162
4163	/**
4164	 * @param $bArchive
4165	 * @throws Exception
4166	 */
4167	protected function DBWriteArchiveFlag($bArchive)
4168	{
4169		if (!MetaModel::IsArchivable(get_class($this)))
4170		{
4171			throw new Exception(get_class($this).' is not an archivable class');
4172		}
4173
4174		$iFlag = $bArchive ? 1 : 0;
4175		$sDate = $bArchive ? '"'.date(AttributeDate::GetSQLFormat()).'"' : 'null';
4176
4177		$sClass = get_class($this);
4178		$sArchiveRoot = MetaModel::GetAttributeOrigin($sClass, 'archive_flag');
4179		$sRootTable = MetaModel::DBGetTable($sArchiveRoot);
4180		$sRootKey = MetaModel::DBGetKey($sArchiveRoot);
4181		$aJoins = array("`$sRootTable`");
4182		$aUpdates = array();
4183		foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL) as $sParentClass)
4184		{
4185			if (!MetaModel::IsValidAttCode($sParentClass, 'archive_flag')) continue;
4186
4187			$sTable = MetaModel::DBGetTable($sParentClass);
4188			$aUpdates[] = "`$sTable`.`archive_flag` = $iFlag";
4189			if ($sParentClass == $sArchiveRoot)
4190			{
4191				if (!$bArchive || $this->Get('archive_date') == '')
4192				{
4193					// Erase or set the date (do not change it)
4194					$aUpdates[] = "`$sTable`.`archive_date` = $sDate";
4195				}
4196			}
4197			else
4198			{
4199				$sKey = MetaModel::DBGetKey($sParentClass);
4200				$aJoins[] = "`$sTable` ON `$sTable`.`$sKey` = `$sRootTable`.`$sRootKey`";
4201			}
4202		}
4203		$sJoins = implode(' INNER JOIN ', $aJoins);
4204		$sValues = implode(', ', $aUpdates);
4205		$sUpdateQuery = "UPDATE $sJoins SET $sValues WHERE `$sRootTable`.`$sRootKey` = ".$this->GetKey();
4206		CMDBSource::Query($sUpdateQuery);
4207	}
4208
4209	/**
4210	 * Can be called to repair the database (tables consistency)
4211	 * The archive_date will be preserved
4212	 * @throws Exception
4213	 */
4214	public function DBArchive()
4215	{
4216		$this->DBWriteArchiveFlag(true);
4217		$this->m_aCurrValues['archive_flag'] = true;
4218		$this->m_aOrigValues['archive_flag'] = true;
4219	}
4220
4221	public function DBUnarchive()
4222	{
4223		$this->DBWriteArchiveFlag(false);
4224		$this->m_aCurrValues['archive_flag'] = false;
4225		$this->m_aOrigValues['archive_flag'] = false;
4226		$this->m_aCurrValues['archive_date'] = null;
4227		$this->m_aOrigValues['archive_date'] = null;
4228	}
4229
4230
4231
4232	/**
4233	 * @param string $sClass Needs to be an instanciable class
4234	 * @returns $oObj
4235	 **/
4236	public static function MakeDefaultInstance($sClass)
4237	{
4238		$sStateAttCode = MetaModel::GetStateAttributeCode($sClass);
4239		$oObj = MetaModel::NewObject($sClass);
4240		if (!empty($sStateAttCode))
4241		{
4242			$sTargetState = MetaModel::GetDefaultState($sClass);
4243			$oObj->Set($sStateAttCode, $sTargetState);
4244		}
4245		return $oObj;
4246	}
4247
4248	/**
4249	 * Complete a new object with data from context
4250	 * @param array $aContextParam Context used for creation form prefilling
4251	 *
4252	 */
4253	public function PrefillCreationForm(&$aContextParam)
4254	{
4255	}
4256
4257	/**
4258	 * Complete an object after a state transition with data from context
4259	 * @param array $aContextParam Context used for creation form prefilling
4260	 *
4261	 */
4262	public function PrefillTransitionForm(&$aContextParam)
4263	{
4264	}
4265
4266	/**
4267	 * Complete a filter ($aContextParam['filter']) data from context
4268	 * (Called on source object)
4269	 * @param array $aContextParam Context used for creation form prefilling
4270	 *
4271	 */
4272	public function PrefillSearchForm(&$aContextParam)
4273	{
4274	}
4275
4276	/**
4277	 * Prefill a creation / stimulus change / search form according to context, current state of an object, stimulus.. $sOperation
4278	 * @param string $sOperation Operation identifier
4279	 * @param array $aContextParam Context used for creation form prefilling
4280	 *
4281	 */
4282	public function PrefillForm($sOperation, &$aContextParam)
4283	{
4284		switch($sOperation){
4285			case 'creation_from_0':
4286			case 'creation_from_extkey':
4287			case 'creation_from_editinplace':
4288				$this->PrefillCreationForm($aContextParam);
4289				break;
4290			case 'state_change':
4291				$this->PrefillTransitionForm($aContextParam);
4292				break;
4293			case 'search':
4294				$this->PrefillSearchForm($aContextParam);
4295				break;
4296			default:
4297				break;
4298		}
4299	}
4300}
4301
4302