1<?php
2// Copyright (C) 2010-2018 Combodo SARL
3//
4//   This file is part of iTop.
5//
6//   iTop is free software; you can redistribute it and/or modify
7//   it under the terms of the GNU Affero General Public License as published by
8//   the Free Software Foundation, either version 3 of the License, or
9//   (at your option) any later version.
10//
11//   iTop is distributed in the hope that it will be useful,
12//   but WITHOUT ANY WARRANTY; without even the implied warranty of
13//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//   GNU Affero General Public License for more details.
15//
16//   You should have received a copy of the GNU Affero General Public License
17//   along with iTop. If not, see <http://www.gnu.org/licenses/>
18
19
20/**
21 * Abstract class that implements some common and useful methods for displaying
22 * the objects
23 *
24 * @copyright   Copyright (C) 2010-2018 Combodo SARL
25 * @license     http://opensource.org/licenses/AGPL-3.0
26 */
27
28define('OBJECT_PROPERTIES_TAB', 'ObjectProperties');
29
30define('HILIGHT_CLASS_CRITICAL', 'red');
31define('HILIGHT_CLASS_WARNING', 'orange');
32define('HILIGHT_CLASS_OK', 'green');
33define('HILIGHT_CLASS_NONE', '');
34
35define('MIN_WATCHDOG_INTERVAL', 15); // Minimum interval for the watchdog: 15s
36
37require_once(APPROOT.'core/cmdbobject.class.inc.php');
38require_once(APPROOT.'application/applicationextension.inc.php');
39require_once(APPROOT.'application/utils.inc.php');
40require_once(APPROOT.'application/applicationcontext.class.inc.php');
41require_once(APPROOT.'application/ui.linkswidget.class.inc.php');
42require_once(APPROOT.'application/ui.linksdirectwidget.class.inc.php');
43require_once(APPROOT.'application/ui.passwordwidget.class.inc.php');
44require_once(APPROOT.'application/ui.extkeywidget.class.inc.php');
45require_once(APPROOT.'application/ui.htmleditorwidget.class.inc.php');
46require_once(APPROOT.'application/datatable.class.inc.php');
47require_once(APPROOT.'sources/renderer/console/consoleformrenderer.class.inc.php');
48require_once(APPROOT.'sources/application/search/searchform.class.inc.php');
49require_once(APPROOT.'sources/application/search/criterionparser.class.inc.php');
50require_once(APPROOT.'sources/application/search/criterionconversionabstract.class.inc.php');
51require_once(APPROOT.'sources/application/search/criterionconversion/criteriontooql.class.inc.php');
52require_once(APPROOT.'sources/application/search/criterionconversion/criteriontosearchform.class.inc.php');
53
54abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
55{
56	protected $m_iFormId; // The ID of the form used to edit the object (when in edition mode !)
57	static $iGlobalFormId = 1;
58	protected $aFieldsMap;
59
60	/**
61	 * If true, bypass IsActionAllowedOnAttribute when writing this object
62	 *
63	 * @var bool
64	 */
65	protected $bAllowWrite;
66
67	/**
68	 * Constructor from a row of data (as a hash 'attcode' => value)
69	 *
70	 * @param array $aRow
71	 * @param string $sClassAlias
72	 * @param array $aAttToLoad
73	 * @param array $aExtendedDataSpec
74	 */
75	public function __construct($aRow = null, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null)
76	{
77		parent::__construct($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec);
78		$this->bAllowWrite = false;
79	}
80
81	/**
82	 * returns what will be the next ID for the forms
83	 */
84	public static function GetNextFormId()
85	{
86		return 1 + self::$iGlobalFormId;
87	}
88
89	public static function GetUIPage()
90	{
91		return 'UI.php';
92	}
93
94	public static function ReloadAndDisplay($oPage, $oObj, $aParams)
95	{
96		$oAppContext = new ApplicationContext();
97		// Reload the page to let the "calling" page execute its 'onunload' method.
98		// Note 1: The redirection MUST NOT be made via an HTTP "header" since onunload is only called when the actual content of the DOM
99		// is replaced by some other content. So the "bouncing" page must provide some content (in our case a script making the redirection).
100		// Note 2: make sure that the URL below is different from the one of the "Modify" button, otherwise the button will have no effect. This is why we add "&a=1" at the end !!!
101		// Note 3: we use the toggle of a flag in the sessionStorage object to prevent an infinite loop of reloads in case the object is actually locked by another window
102		$sSessionStorageKey = get_class($oObj).'_'.$oObj->GetKey();
103		$sParams = '';
104		foreach($aParams as $sName => $value)
105		{
106			$sParams .= $sName.'='.urlencode($value).'&'; // Always add a trailing &
107		}
108		$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/'.$oObj->GetUIPage().'?'.$sParams.'class='.get_class($oObj).'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink().'&a=1';
109		$oPage->add_script(
110			<<<EOF
111	if (!sessionStorage.getItem('$sSessionStorageKey'))
112	{
113		sessionStorage.setItem('$sSessionStorageKey', 1);
114		window.location.href= "$sUrl";
115	}
116	else
117	{
118		sessionStorage.removeItem('$sSessionStorageKey');
119	}
120EOF
121		);
122
123		$oObj->Reload();
124		$oObj->DisplayDetails($oPage, false);
125	}
126
127	/**
128	 * @param $sMessageId
129	 * @param $sMessage
130	 * @param $sSeverity
131	 * @param $fRank
132	 * @param bool $bMustNotExist
133	 *
134	 * @see SetSessionMessage()
135	 * @since 2.6
136	 */
137	protected function SetSessionMessageFromInstance($sMessageId, $sMessage, $sSeverity, $fRank, $bMustNotExist = false)
138	{
139		$sObjectClass = get_class($this);
140		$iObjectId = $this->GetKey();
141
142		self::SetSessionMessage($sObjectClass, $iObjectId, $sMessageId, $sMessage, $sSeverity, $fRank);
143	}
144
145	/**
146	 * Set a message diplayed to the end-user next time this object will be displayed
147	 * Messages are uniquely identified so that plugins can override standard messages (the final work is given to the
148	 * last plugin to set the message for a given message id) In practice, standard messages are recorded at the end
149	 * but they will not overwrite existing messages
150	 *
151	 * @param string $sClass The class of the object (must be the final class)
152	 * @param int $iKey The identifier of the object
153	 * @param string $sMessageId Your id or one of the well-known ids: 'create', 'update' and 'apply_stimulus'
154	 * @param string $sMessage The HTML message (must be correctly escaped)
155	 * @param string $sSeverity Any of the following: ok, info, error.
156	 * @param float $fRank Ordering of the message: smallest displayed first (can be negative)
157	 * @param bool $bMustNotExist Do not alter any existing message (considering the id)
158	 *
159	 * @see SetSessionMessageFromInstance() to call from within an instance
160	 */
161	public static function SetSessionMessage(
162		$sClass, $iKey, $sMessageId, $sMessage, $sSeverity, $fRank, $bMustNotExist = false
163	) {
164		$sMessageKey = $sClass.'::'.$iKey;
165		if (!isset($_SESSION['obj_messages'][$sMessageKey]))
166		{
167			$_SESSION['obj_messages'][$sMessageKey] = array();
168		}
169		if (!$bMustNotExist || !array_key_exists($sMessageId, $_SESSION['obj_messages'][$sMessageKey]))
170		{
171			$_SESSION['obj_messages'][$sMessageKey][$sMessageId] = array(
172				'rank' => $fRank,
173				'severity' => $sSeverity,
174				'message' => $sMessage,
175			);
176		}
177	}
178
179	function DisplayBareHeader(WebPage $oPage, $bEditMode = false)
180	{
181		// Standard Header with name, actions menu and history block
182		//
183
184		if (!$oPage->IsPrintableVersion())
185		{
186			// Is there a message for this object ??
187			$aMessages = array();
188			$aRanks = array();
189			if (MetaModel::GetConfig()->Get('concurrent_lock_enabled'))
190			{
191				$aLockInfo = iTopOwnershipLock::IsLocked(get_class($this), $this->GetKey());
192				if ($aLockInfo['locked'])
193				{
194					$aRanks[] = 0;
195					$sName = $aLockInfo['owner']->GetName();
196					if ($aLockInfo['owner']->Get('contactid') != 0)
197					{
198						$sName .= ' ('.$aLockInfo['owner']->Get('contactid_friendlyname').')';
199					}
200					$aResult['message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User', $sName);
201					$aMessages[] = "<div class=\"header_message message_error\">".Dict::Format('UI:CurrentObjectIsLockedBy_User',
202							$sName)."</div>";
203				}
204			}
205			$sMessageKey = get_class($this).'::'.$this->GetKey();
206			if (array_key_exists('obj_messages', $_SESSION) && array_key_exists($sMessageKey,
207					$_SESSION['obj_messages']))
208			{
209				foreach($_SESSION['obj_messages'][$sMessageKey] as $sMessageId => $aMessageData)
210				{
211					$sMsgClass = 'message_'.$aMessageData['severity'];
212					$aMessages[] = "<div class=\"header_message $sMsgClass\">".$aMessageData['message']."</div>";
213					$aRanks[] = $aMessageData['rank'];
214				}
215				unset($_SESSION['obj_messages'][$sMessageKey]);
216			}
217			array_multisort($aRanks, $aMessages);
218			foreach($aMessages as $sMessage)
219			{
220				$oPage->add($sMessage);
221			}
222		}
223
224		if (!$oPage->IsPrintableVersion())
225		{
226			// action menu
227			$oSingletonFilter = new DBObjectSearch(get_class($this));
228			$oSingletonFilter->AddCondition('id', $this->GetKey(), '=');
229			$oBlock = new MenuBlock($oSingletonFilter, 'details', false);
230			$oBlock->Display($oPage, -1);
231		}
232
233		// Master data sources
234		$aIcons = array();
235		if (!$oPage->IsPrintableVersion())
236		{
237			$oCreatorTask = null;
238			$bCanBeDeletedByTask = false;
239			$bCanBeDeletedByUser = true;
240			$aMasterSources = array();
241			$aSyncData = $this->GetSynchroData();
242			if (count($aSyncData) > 0)
243			{
244				foreach($aSyncData as $iSourceId => $aSourceData)
245				{
246					$oDataSource = $aSourceData['source'];
247					$oReplica = reset($aSourceData['replica']); // Take the first one!
248
249					$sApplicationURL = $oDataSource->GetApplicationUrl($this, $oReplica);
250					$sLink = $oDataSource->GetName();
251					if (!empty($sApplicationURL))
252					{
253						$sLink = "<a href=\"$sApplicationURL\" target=\"_blank\">".$oDataSource->GetName()."</a>";
254					}
255					if ($oReplica->Get('status_dest_creator') == 1)
256					{
257						$oCreatorTask = $oDataSource;
258						$bCreatedByTask = true;
259					}
260					else
261					{
262						$bCreatedByTask = false;
263					}
264					if ($bCreatedByTask)
265					{
266						$sDeletePolicy = $oDataSource->Get('delete_policy');
267						if (($sDeletePolicy == 'delete') || ($sDeletePolicy == 'update_then_delete'))
268						{
269							$bCanBeDeletedByTask = true;
270						}
271						$sUserDeletePolicy = $oDataSource->Get('user_delete_policy');
272						if ($sUserDeletePolicy == 'nobody')
273						{
274							$bCanBeDeletedByUser = false;
275						}
276						elseif (($sUserDeletePolicy == 'administrators') && !UserRights::IsAdministrator())
277						{
278							$bCanBeDeletedByUser = false;
279						}
280					}
281					$aMasterSources[$iSourceId]['datasource'] = $oDataSource;
282					$aMasterSources[$iSourceId]['url'] = $sLink;
283					$aMasterSources[$iSourceId]['last_synchro'] = $oReplica->Get('status_last_seen');
284				}
285
286				if (is_object($oCreatorTask))
287				{
288					$sTaskUrl = $aMasterSources[$oCreatorTask->GetKey()]['url'];
289					if (!$bCanBeDeletedByUser)
290					{
291						$sTip = "<p>".Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source',
292								$sTaskUrl)."</p>";
293					}
294					else
295					{
296						$sTip = "<p>".Dict::Format('Core:Synchro:TheObjectWasCreatedBy_Source', $sTaskUrl)."</p>";
297					}
298					if ($bCanBeDeletedByTask)
299					{
300						$sTip .= "<p>".Dict::Format('Core:Synchro:TheObjectCanBeDeletedBy_Source', $sTaskUrl)."</p>";
301					}
302				}
303				else
304				{
305					$sTip = "<p>".Dict::S('Core:Synchro:ThisObjectIsSynchronized')."</p>";
306				}
307
308				$sTip .= "<p><b>".Dict::S('Core:Synchro:ListOfDataSources')."</b></p>";
309				foreach($aMasterSources as $aStruct)
310				{
311					// Formatting last synchro date
312					$oDateTime = DateTime::createFromFormat('Y-m-d H:i:s', $aStruct['last_synchro']);
313					$oDateTimeFormat = AttributeDateTime::GetFormat();
314					$sLastSynchro = $oDateTimeFormat->Format($oDateTime);
315
316					$oDataSource = $aStruct['datasource'];
317					$sLink = $aStruct['url'];
318					$sTip .= "<p style=\"white-space:nowrap\">".$oDataSource->GetIcon(true,
319							'style="vertical-align:middle"')."&nbsp;$sLink<br/>";
320					$sTip .= Dict::S('Core:Synchro:LastSynchro').'<br/>'.$sLastSynchro."</p>";
321				}
322				$sLabel = htmlentities(Dict::S('Tag:Synchronized'), ENT_QUOTES, 'UTF-8');
323				$sSynchroTagId = 'synchro_icon-'.$this->GetKey();
324				$aIcons[] = "<div class=\"tag\" id=\"$sSynchroTagId\"><span class=\"object-synchronized fa fa-lock fa-1x\">&nbsp;</span>&nbsp;$sLabel</div>";
325				$sTip = addslashes($sTip);
326				$oPage->add_ready_script("$('#$sSynchroTagId').qtip( { content: '$sTip', show: 'mouseover', hide: { fixed: true }, style: { name: 'dark', tip: 'topLeft' }, position: { corner: { target: 'bottomMiddle', tooltip: 'topLeft' }} } );");
327			}
328		}
329
330		if ($this->IsArchived())
331		{
332			$sLabel = htmlentities(Dict::S('Tag:Archived'), ENT_QUOTES, 'UTF-8');
333			$sTitle = htmlentities(Dict::S('Tag:Archived+'), ENT_QUOTES, 'UTF-8');
334			$aIcons[] = "<div class=\"tag\" title=\"$sTitle\"><span class=\"object-archived fa fa-archive fa-1x\">&nbsp;</span>&nbsp;$sLabel</div>";
335		}
336		elseif ($this->IsObsolete())
337		{
338			$sLabel = htmlentities(Dict::S('Tag:Obsolete'), ENT_QUOTES, 'UTF-8');
339			$sTitle = htmlentities(Dict::S('Tag:Obsolete+'), ENT_QUOTES, 'UTF-8');
340			$aIcons[] = "<div class=\"tag\" title=\"$sTitle\"><span class=\"object-obsolete fa fa-eye-slash fa-1x\">&nbsp;</span>&nbsp;$sLabel</div>";
341		}
342
343		$sObjectIcon = $this->GetIcon();
344		$sClassName = MetaModel::GetName(get_class($this));
345		$sObjectName = $this->GetName();
346		if (count($aIcons) > 0)
347		{
348			$sTags = '<div class="tags">'.implode('&nbsp;', $aIcons).'</div>';
349		}
350		else
351		{
352			$sTags = '';
353		}
354
355		$oPage->add(
356			<<<EOF
357<div class="page_header">
358   <div class="object-details-header">
359      <div class ="object-icon">$sObjectIcon</div>
360      <div class ="object-infos">
361		  <h1 class="object-name">$sClassName: <span class="hilite">$sObjectName</span></h1>
362		  $sTags
363      </div>
364   </div>
365</div>
366EOF
367		);
368	}
369
370	function DisplayBareHistory(WebPage $oPage, $bEditMode = false, $iLimitCount = 0, $iLimitStart = 0)
371	{
372		// history block (with as a tab)
373		$oHistoryFilter = new DBObjectSearch('CMDBChangeOp');
374		$oHistoryFilter->AddCondition('objkey', $this->GetKey(), '=');
375		$oHistoryFilter->AddCondition('objclass', get_class($this), '=');
376		$oBlock = new HistoryBlock($oHistoryFilter, 'table', false);
377		$oBlock->SetLimit($iLimitCount, $iLimitStart);
378		$oBlock->Display($oPage, 'history');
379	}
380
381	function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array())
382	{
383		$aFieldsMap = $this->GetBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams);
384
385
386		if (!isset($aExtraParams['disable_plugins']) || !$aExtraParams['disable_plugins'])
387		{
388			foreach(MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
389			{
390				$oExtensionInstance->OnDisplayProperties($this, $oPage, $bEditMode);
391			}
392		}
393
394		// Special case to display the case log, if any...
395		// WARNING: if you modify the loop below, also check the corresponding code in UpdateObject and DisplayModifyForm
396		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
397		{
398			if ($oAttDef instanceof AttributeCaseLog)
399			{
400				$sComment = (isset($aExtraParams['fieldsComments'][$sAttCode])) ? $aExtraParams['fieldsComments'][$sAttCode] : '';
401				$this->DisplayCaseLog($oPage, $sAttCode, $sComment, $sPrefix, $bEditMode);
402				$aFieldsMap[$sAttCode] = $this->m_iFormId.'_'.$sAttCode;
403			}
404		}
405
406		return $aFieldsMap;
407	}
408
409	/**
410	 * Add a field to the map: attcode => id used when building a form
411	 *
412	 * @param string $sAttCode The attribute code of the field being edited
413	 * @param string $sInputId The unique ID of the control/widget in the page
414	 */
415	protected function AddToFieldsMap($sAttCode, $sInputId)
416	{
417		$this->aFieldsMap[$sAttCode] = $sInputId;
418	}
419
420	/**
421	 * @param \iTopWebPage $oPage
422	 * @param $sAttCode
423	 *
424	 * @throws \Exception
425	 */
426	public function DisplayDashboard($oPage, $sAttCode)
427	{
428		$sClass = get_class($this);
429		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
430
431		if (!$oAttDef instanceof AttributeDashboard)
432		{
433			throw new CoreException(Dict::S('UI:Error:InvalidDashboard'));
434		}
435
436		// Load the dashboard
437		$oDashboard = $oAttDef->GetDashboard();
438		if (is_null($oDashboard))
439		{
440			throw new CoreException(Dict::S('UI:Error:InvalidDashboard'));
441		}
442
443		$bCanEdit = UserRights::IsAdministrator() || $oAttDef->IsUserEditable();
444		$sDivId = $oDashboard->GetId();
445		$oPage->add('<div class="dashboard_contents" id="'.$sDivId.'">');
446		$aExtraParams = array('query_params' => $this->ToArgsForQuery());
447		$oDashboard->Render($oPage, false, $aExtraParams, $bCanEdit);
448		$oPage->add('</div>');
449	}
450
451	/**
452	 * @param \WebPage $oPage
453	 * @param bool $bEditMode
454	 *
455	 * @throws \CoreException
456	 * @throws \CoreUnexpectedValue
457	 * @throws \DictExceptionMissingString
458	 * @throws \MissingQueryArgument
459	 * @throws \MySQLException
460	 * @throws \MySQLHasGoneAwayException
461	 * @throws \OQLException
462	 */
463	function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
464	{
465		$aRedundancySettings = $this->FindVisibleRedundancySettings();
466
467		// Related objects: display all the linkset attributes, each as a separate tab
468		// In the order described by the 'display' ZList
469		$aList = $this->FlattenZList(MetaModel::GetZListItems(get_class($this), 'details'));
470		if (count($aList) == 0)
471		{
472			// Empty ZList defined, display all the linkedset attributes defined
473			$aList = array_keys(MetaModel::ListAttributeDefs(get_class($this)));
474		}
475		$sClass = get_class($this);
476		foreach($aList as $sAttCode)
477		{
478			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
479			if ($oAttDef instanceof AttributeDashboard)
480			{
481				if ($bEditMode)
482				{
483					continue;
484				}
485				$oPage->AddAjaxTab($oAttDef->GetLabel(), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=dashboard&class='.get_class($this).'&id='.$this->GetKey().'&attcode='.$oAttDef->GetCode());
486				continue;
487			}
488
489			// Display mode
490			if (!$oAttDef->IsLinkset())
491			{
492				continue;
493			} // Process only linkset attributes...
494
495			$sLinkedClass = $oAttDef->GetLinkedClass();
496
497			// Filter out links pointing to obsolete objects (if relevant)
498			$oOrmLinkSet = $this->Get($sAttCode);
499			$oLinkSet = $oOrmLinkSet->ToDBObjectSet(utils::ShowObsoleteData());
500
501			$iCount = $oLinkSet->Count();
502			$sCount = '';
503			if ($iCount != 0)
504			{
505				$sCount = " ($iCount)";
506			}
507			$oPage->SetCurrentTab($oAttDef->GetLabel().$sCount);
508			if ($this->IsNew())
509			{
510				$iFlags = $this->GetInitialStateAttributeFlags($sAttCode);
511			}
512			else
513			{
514				$iFlags = $this->GetAttributeFlags($sAttCode);
515			}
516			// Adjust the flags according to user rights
517			if ($oAttDef->IsIndirect())
518			{
519				$oLinkingAttDef = MetaModel::GetAttributeDef($sLinkedClass, $oAttDef->GetExtKeyToRemote());
520				$sTargetClass = $oLinkingAttDef->GetTargetClass();
521				// n:n links => must be allowed to modify the linking class AND  read the target class in order to edit the linkedset
522				if (!UserRights::IsActionAllowed($sLinkedClass,
523						UR_ACTION_MODIFY) || !UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ))
524				{
525					$iFlags |= OPT_ATT_READONLY;
526				}
527				// n:n links => must be allowed to read the linking class AND  the target class in order to display the linkedset
528				if (!UserRights::IsActionAllowed($sLinkedClass,
529						UR_ACTION_READ) || !UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ))
530				{
531					$iFlags |= OPT_ATT_HIDDEN;
532				}
533			}
534			else
535			{
536				// 1:n links => must be allowed to modify the linked class in order to edit the linkedset
537				if (!UserRights::IsActionAllowed($sLinkedClass, UR_ACTION_MODIFY))
538				{
539					$iFlags |= OPT_ATT_READONLY;
540				}
541				// 1:n links => must be allowed to read the linked class in order to display the linkedset
542				if (!UserRights::IsActionAllowed($sLinkedClass, UR_ACTION_READ))
543				{
544					$iFlags |= OPT_ATT_HIDDEN;
545				}
546			}
547			// Non-readable/hidden linkedset... don't display anything
548			if ($iFlags & OPT_ATT_HIDDEN)
549			{
550				continue;
551			}
552
553			$aArgs = array('this' => $this);
554			$bReadOnly = ($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE));
555			if ($bEditMode && (!$bReadOnly))
556			{
557				$sInputId = $this->m_iFormId.'_'.$sAttCode;
558
559				if ($oAttDef->IsIndirect())
560				{
561					$oLinkingAttDef = MetaModel::GetAttributeDef($sLinkedClass, $oAttDef->GetExtKeyToRemote());
562					$sTargetClass = $oLinkingAttDef->GetTargetClass();
563				}
564				else
565				{
566					$sTargetClass = $sLinkedClass;
567				}
568				$oPage->p(MetaModel::GetClassIcon($sTargetClass)."&nbsp;".$oAttDef->GetDescription().'<span id="busy_'.$sInputId.'"></span>');
569
570				$sDisplayValue = ''; // not used
571				$sHTMLValue = "<span id=\"field_{$sInputId}\">".self::GetFormElementForField($oPage, $sClass, $sAttCode,
572						$oAttDef, $oLinkSet, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).'</span>';
573				$this->AddToFieldsMap($sAttCode, $sInputId);
574				$oPage->add($sHTMLValue);
575			}
576			else
577			{
578				// Display mode
579				if (!$oAttDef->IsIndirect())
580				{
581					// 1:n links
582					$sTargetClass = $sLinkedClass;
583
584					$aDefaults = array($oAttDef->GetExtKeyToMe() => $this->GetKey());
585					$oAppContext = new ApplicationContext();
586					foreach($oAppContext->GetNames() as $sKey)
587					{
588						// The linked object inherits the parent's value for the context
589						if (MetaModel::IsValidAttCode($sClass, $sKey))
590						{
591							$aDefaults[$sKey] = $this->Get($sKey);
592						}
593					}
594					$aParams = array(
595						'target_attr' => $oAttDef->GetExtKeyToMe(),
596						'object_id' => $this->GetKey(),
597						'menu' => MetaModel::GetConfig()->Get('allow_menu_on_linkset'),
598						//'menu_actions_target' => '_blank',
599						'default' => $aDefaults,
600						'table_id' => $sClass.'_'.$sAttCode,
601					);
602				}
603				else
604				{
605					// n:n links
606					$oLinkingAttDef = MetaModel::GetAttributeDef($sLinkedClass, $oAttDef->GetExtKeyToRemote());
607					$sTargetClass = $oLinkingAttDef->GetTargetClass();
608					$aParams = array(
609						'link_attr' => $oAttDef->GetExtKeyToMe(),
610						'object_id' => $this->GetKey(),
611						'target_attr' => $oAttDef->GetExtKeyToRemote(),
612						'view_link' => false,
613						'menu' => false,
614						//'menu_actions_target' => '_blank',
615						'display_limit' => true, // By default limit the list to speed up the initial load & display
616						'table_id' => $sClass.'_'.$sAttCode,
617					);
618				}
619				$oPage->p(MetaModel::GetClassIcon($sTargetClass)."&nbsp;".$oAttDef->GetDescription());
620				$oBlock = new DisplayBlock($oLinkSet->GetFilter(), 'list', false);
621				$oBlock->Display($oPage, 'rel_'.$sAttCode, $aParams);
622			}
623			if (array_key_exists($sAttCode, $aRedundancySettings))
624			{
625				foreach($aRedundancySettings[$sAttCode] as $oRedundancyAttDef)
626				{
627					$sRedundancyAttCode = $oRedundancyAttDef->GetCode();
628					$sValue = $this->Get($sRedundancyAttCode);
629					$iRedundancyFlags = $this->GetFormAttributeFlags($sRedundancyAttCode);
630					$bRedundancyReadOnly = ($iRedundancyFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE));
631
632					$oPage->add('<fieldset>');
633					$oPage->add('<legend>'.$oRedundancyAttDef->GetLabel().'</legend>');
634					if ($bEditMode && (!$bRedundancyReadOnly))
635					{
636						$sInputId = $this->m_iFormId.'_'.$sRedundancyAttCode;
637						$oPage->add("<span id=\"field_{$sInputId}\">".self::GetFormElementForField($oPage, $sClass,
638								$sRedundancyAttCode, $oRedundancyAttDef, $sValue, '', $sInputId, '', $iFlags,
639								$aArgs).'</span>');
640					}
641					else
642					{
643						$oPage->add($oRedundancyAttDef->GetDisplayForm($sValue, $oPage, false, $this->m_iFormId));
644					}
645					$oPage->add('</fieldset>');
646				}
647			}
648		}
649		$oPage->SetCurrentTab('');
650
651		foreach(MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
652		{
653			$oExtensionInstance->OnDisplayRelations($this, $oPage, $bEditMode);
654		}
655
656		// Display Notifications after the other tabs since this tab disappears in edition
657		if (!$bEditMode)
658		{
659			// Look for any trigger that considers this object as "In Scope"
660			// If any trigger has been found then display a tab with notifications
661			//
662			$oTriggerSet = new CMDBObjectSet(new DBObjectSearch('Trigger'));
663			$aTriggers = array();
664			while ($oTrigger = $oTriggerSet->Fetch())
665			{
666				if ($oTrigger->IsInScope($this))
667				{
668					$aTriggers[] = $oTrigger->GetKey();
669				}
670			}
671			if (count($aTriggers) > 0)
672			{
673				$iId = $this->GetKey();
674				$sTriggersList = implode(',', $aTriggers);
675				$aNotifSearches = array();
676				$iNotifsCount = 0;
677				$aNotificationClasses = MetaModel::EnumChildClasses('EventNotification', ENUM_CHILD_CLASSES_EXCLUDETOP);
678				foreach($aNotificationClasses as $sNotifClass)
679				{
680					$aNotifSearches[$sNotifClass] = DBObjectSearch::FromOQL("SELECT $sNotifClass AS Ev JOIN Trigger AS T ON Ev.trigger_id = T.id WHERE T.id IN ($sTriggersList) AND Ev.object_id = $iId");
681					$oNotifSet = new DBObjectSet($aNotifSearches[$sNotifClass]);
682					$iNotifsCount += $oNotifSet->Count();
683				}
684				// Display notifications regarding the object: on block per subclass to have the intersting columns
685				$sCount = ($iNotifsCount > 0) ? ' ('.$iNotifsCount.')' : '';
686				$oPage->SetCurrentTab(Dict::S('UI:NotificationsTab').$sCount);
687
688				foreach($aNotificationClasses as $sNotifClass)
689				{
690					$oPage->p(MetaModel::GetClassIcon($sNotifClass, true).'&nbsp;'.MetaModel::GetName($sNotifClass));
691					$oBlock = new DisplayBlock($aNotifSearches[$sNotifClass], 'list', false);
692					$oBlock->Display($oPage, 'notifications_'.$sNotifClass, array('menu' => false));
693				}
694			}
695		}
696	}
697
698	function GetBareProperties(WebPage $oPage, $bEditMode, $sPrefix, $aExtraParams = array())
699	{
700		$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
701		$sClass = get_class($this);
702		$aDetailsList = MetaModel::GetZListItems($sClass, 'details');
703		$aDetailsStruct = self::ProcessZlist($aDetailsList, array('UI:PropertiesTab' => array()), 'UI:PropertiesTab', 'col1', '');
704		// Compute the list of properties to display, first the attributes in the 'details' list, then
705		// all the remaining attributes that are not external fields
706		$sEditMode = ($bEditMode) ? 'edit' : 'view';
707		$aDetails = array();
708		$iInputId = 0;
709		$aFieldsMap = array();
710		$aFieldsComments = (isset($aExtraParams['fieldsComments'])) ? $aExtraParams['fieldsComments'] : array();
711		$aExtraFlags = (isset($aExtraParams['fieldsFlags'])) ? $aExtraParams['fieldsFlags'] : array();
712
713		foreach($aDetailsStruct as $sTab => $aCols)
714		{
715			$aDetails[$sTab] = array();
716			$aTableStyles[] = 'vertical-align:top';
717			$aTableClasses = array();
718			$aColStyles[] = 'vertical-align:top';
719			$aColClasses = array();
720
721			ksort($aCols);
722			$iColCount = count($aCols);
723			if ($iColCount > 1)
724			{
725				$aTableClasses[] = 'n-cols-details';
726				$aTableClasses[] = $iColCount.'-cols-details';
727
728				$aColStyles[] = 'width:'.floor(100 / $iColCount).'%';
729			}
730			else
731			{
732				$aTableClasses[] = 'one-col-details';
733			}
734
735			$oPage->SetCurrentTab(Dict::S($sTab));
736			$oPage->add('<table style="'.implode('; ', $aTableStyles).'" class="'.implode(' ',
737					$aTableClasses).'" data-mode="'.$sEditMode.'"><tr>');
738			foreach($aCols as $sColIndex => $aFieldsets)
739			{
740				$oPage->add('<td style="'.implode('; ', $aColStyles).'" class="'.implode(' ', $aColClasses).'">');
741				$sPreviousLabel = '';
742				$aDetails[$sTab][$sColIndex] = array();
743				foreach($aFieldsets as $sFieldsetName => $aFields)
744				{
745					if (!empty($sFieldsetName) && ($sFieldsetName[0] != '_'))
746					{
747						$sLabel = $sFieldsetName;
748					}
749					else
750					{
751						$sLabel = '';
752					}
753					if ($sLabel != $sPreviousLabel)
754					{
755						if (!empty($sPreviousLabel))
756						{
757							$oPage->add('<fieldset>');
758							$oPage->add('<legend>'.Dict::S($sPreviousLabel).'</legend>');
759						}
760						$oPage->Details($aDetails[$sTab][$sColIndex]);
761						if (!empty($sPreviousLabel))
762						{
763							$oPage->add('</fieldset>');
764						}
765						$aDetails[$sTab][$sColIndex] = array();
766						$sPreviousLabel = $sLabel;
767					}
768					foreach($aFields as $sAttCode)
769					{
770						if ($bEditMode)
771						{
772							$sComments = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : '';
773							$sInfos = '';
774							$iFlags = $this->GetFormAttributeFlags($sAttCode);
775							if (array_key_exists($sAttCode, $aExtraFlags))
776							{
777								// the caller may override some flags if needed
778								$iFlags = $iFlags | $aExtraFlags[$sAttCode];
779							}
780							$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
781							if ((!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0) && !($oAttDef instanceof AttributeDashboard))
782							{
783								$sInputId = $this->m_iFormId.'_'.$sAttCode;
784								if ($oAttDef->IsWritable())
785								{
786									if ($sStateAttCode == $sAttCode)
787									{
788										// State attribute is always read-only from the UI
789										$sHTMLValue = $this->GetStateLabel();
790										$val = array(
791											'label' => '<label>'.$oAttDef->GetLabel().'</label>',
792											'value' => $sHTMLValue,
793											'comments' => $sComments,
794											'infos' => $sInfos,
795											'attcode' => $sAttCode,
796										);
797									}
798									else
799									{
800										if ($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE))
801										{
802											// Check if the attribute is not read-only because of a synchro...
803											if ($iFlags & OPT_ATT_SLAVE)
804											{
805												$aReasons = array();
806												$this->GetSynchroReplicaFlags($sAttCode, $aReasons);
807												$sSynchroIcon = "&nbsp;<img id=\"synchro_$sInputId\" src=\"../images/transp-lock.png\" style=\"vertical-align:middle\"/>";
808												$sTip = '';
809												foreach($aReasons as $aRow)
810												{
811													$sDescription = htmlentities($aRow['description'], ENT_QUOTES,
812														'UTF-8');
813													$sDescription = str_replace(array("\r\n", "\n"), "<br/>",
814														$sDescription);
815													$sTip .= "<div class='synchro-source'>";
816													$sTip .= "<div class='synchro-source-title'>Synchronized with {$aRow['name']}</div>";
817													$sTip .= "<div class='synchro-source-description'>$sDescription</div>";
818												}
819												$sTip = addslashes($sTip);
820												$oPage->add_ready_script("$('#synchro_$sInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );");
821												$sComments = $sSynchroIcon;
822											}
823
824											// Attribute is read-only
825											$sHTMLValue = "<span id=\"field_{$sInputId}\">".$this->GetAsHTML($sAttCode).'</span>';
826										}
827										else
828										{
829											$sValue = $this->Get($sAttCode);
830											$sDisplayValue = $this->GetEditValue($sAttCode);
831											$aArgs = array('this' => $this, 'formPrefix' => $sPrefix);
832											$sHTMLValue = "".self::GetFormElementForField($oPage, $sClass, $sAttCode,
833													$oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags,
834													$aArgs).'';
835										}
836										$aFieldsMap[$sAttCode] = $sInputId;
837										$val = array(
838											'label' => '<span title="'.$oAttDef->GetDescription().'">'.$oAttDef->GetLabel().'</span>',
839											'value' => $sHTMLValue,
840											'comments' => $sComments,
841											'infos' => $sInfos,
842											'attcode' => $sAttCode,
843										);
844									}
845								}
846								else
847								{
848									$val = array(
849										'label' => '<span title="'.$oAttDef->GetDescription().'">'.$oAttDef->GetLabel().'</span>',
850										'value' => "<span id=\"field_{$sInputId}\">".$this->GetAsHTML($sAttCode)."</span>",
851										'comments' => $sComments,
852										'infos' => $sInfos,
853										'attcode' => $sAttCode,
854									);
855									$aFieldsMap[$sAttCode] = $sInputId;
856								}
857
858								// Checking how the field should be rendered
859								// Note: For view mode, this is done in cmdbAbstractObject::GetFieldAsHtml()
860								// Note 2: Shouldn't this be a property of the AttDef instead an array that we have to maintain?
861								if (in_array($oAttDef->GetEditClass(),
862									array('Text', 'HTML', 'CaseLog', 'CustomFields', 'OQLExpression')))
863								{
864									$val['layout'] = 'large';
865								}
866								else
867								{
868									$val['layout'] = 'small';
869								}
870							}
871							else
872							{
873								$val = null; // Skip this field
874							}
875
876						}
877						else
878						{
879							// !bEditMode
880							$val = $this->GetFieldAsHtml($sClass, $sAttCode, $sStateAttCode);
881						}
882
883						if ($val != null)
884						{
885							// The field is visible, add it to the current column
886							$aDetails[$sTab][$sColIndex][] = $val;
887							$iInputId++;
888						}
889					}
890				}
891				if (!empty($sPreviousLabel))
892				{
893					$oPage->add('<fieldset>');
894					$oPage->add('<legend>'.Dict::S($sFieldsetName).'</legend>');
895				}
896				$oPage->Details($aDetails[$sTab][$sColIndex]);
897				if (!empty($sPreviousLabel))
898				{
899					$oPage->add('</fieldset>');
900				}
901				$oPage->add('</td>');
902			}
903			$oPage->add('</tr></table>');
904		}
905
906		return $aFieldsMap;
907	}
908
909
910	/**
911	 * @param \iTopWebPage $oPage
912	 * @param bool $bEditMode
913	 *
914	 * @throws \CoreException
915	 * @throws \CoreUnexpectedValue
916	 * @throws \DictExceptionMissingString
917	 * @throws \MissingQueryArgument
918	 * @throws \MySQLException
919	 * @throws \MySQLHasGoneAwayException
920	 * @throws \OQLException
921	 */
922	function DisplayDetails(WebPage $oPage, $bEditMode = false)
923	{
924		$sTemplate = Utils::ReadFromFile(MetaModel::GetDisplayTemplate(get_class($this)));
925		if (!empty($sTemplate))
926		{
927			$oTemplate = new DisplayTemplate($sTemplate);
928			// Note: to preserve backward compatibility with home-made templates, the placeholder '$pkey$' has been preserved
929			//       but the preferred method is to use '$id$'
930			$oTemplate->Render($oPage, array(
931				'class_name' => MetaModel::GetName(get_class($this)),
932				'class' => get_class($this),
933				'pkey' => $this->GetKey(),
934				'id' => $this->GetKey(),
935				'name' => $this->GetName(),
936			));
937		}
938		else
939		{
940			// Object's details
941			// template not found display the object using the *old style*
942			$oPage->add('<div id="search-widget-results-outer">');
943			$this->DisplayBareHeader($oPage, $bEditMode);
944			$oPage->AddTabContainer(OBJECT_PROPERTIES_TAB);
945			$oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB);
946			$oPage->SetCurrentTab(Dict::S('UI:PropertiesTab'));
947			$this->DisplayBareProperties($oPage, $bEditMode);
948			$this->DisplayBareRelations($oPage, $bEditMode);
949			//$oPage->SetCurrentTab(Dict::S('UI:HistoryTab'));
950			//$this->DisplayBareHistory($oPage, $bEditMode);
951			$oPage->AddAjaxTab(Dict::S('UI:HistoryTab'),
952				utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=history&class='.get_class($this).'&id='.$this->GetKey());
953			$oPage->add('</div>');
954		}
955	}
956
957	function DisplayPreview(WebPage $oPage)
958	{
959		$aDetails = array();
960		$sClass = get_class($this);
961		$aList = MetaModel::GetZListItems($sClass, 'preview');
962		foreach($aList as $sAttCode)
963		{
964			$aDetails[] = array(
965				'label' => MetaModel::GetLabel($sClass, $sAttCode),
966				'value' => $this->GetAsHTML($sAttCode),
967			);
968		}
969		$oPage->details($aDetails);
970	}
971
972	public static function DisplaySet(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
973	{
974		$oPage->add(self::GetDisplaySet($oPage, $oSet, $aExtraParams));
975	}
976
977	/**
978	 * Simplified version of GetDisplaySet() with less "decoration" around the table (and no paging)
979	 * that fits better into a printed document (like a PDF or a printable view)
980	 *
981	 * @param WebPage $oPage
982	 * @param DBObjectSet $oSet
983	 * @param array $aExtraParams
984	 *
985	 * @return string The HTML representation of the table
986	 * @throws \CoreException
987	 */
988	public static function GetDisplaySetForPrinting(WebPage $oPage, DBObjectSet $oSet, $aExtraParams = array())
989	{
990		$sTableId = isset($aExtraParams['table_id']) ? $aExtraParams['table_id'] : null;
991
992		$bViewLink = true;
993		$sSelectMode = 'none';
994		$iListId = $sTableId;
995		$sClassAlias = $oSet->GetClassAlias();
996		$sClassName = $oSet->GetClass();
997		$sZListName = 'list';
998		$aClassAliases = array($sClassAlias => $sClassName);
999		$aList = cmdbAbstractObject::FlattenZList(MetaModel::GetZListItems($sClassName, $sZListName));
1000
1001		$oDataTable = new PrintableDataTable($iListId, $oSet, $aClassAliases, $sTableId);
1002		$oSettings = DataTableSettings::GetDataModelSettings($aClassAliases, $bViewLink, array($sClassAlias => $aList));
1003		$oSettings->iDefaultPageSize = 0;
1004		$oSettings->aSortOrder = MetaModel::GetOrderByDefault($sClassName);
1005
1006		return $oDataTable->Display($oPage, $oSettings, false /* $bDisplayMenu */, $sSelectMode, $bViewLink,
1007			$aExtraParams);
1008
1009	}
1010
1011	/**
1012	 * Get the HTML fragment corresponding to the display of a table representing a set of objects
1013	 *
1014	 * @param WebPage $oPage The page object is used for out-of-band information (mostly scripts) output
1015	 * @param CMDBObjectSet The set of objects to display
1016	 * @param array $aExtraParams Some extra configuration parameters to tweak the behavior of the display
1017	 *
1018	 * @return String The HTML fragment representing the table of objects. <b>Warning</b> : no JS added to handled
1019	 *     pagination or table sorting !
1020	 *
1021	 * @throws \ApplicationException
1022	 * @throws \CoreException
1023	 * @see DisplayBlock to get a similar table but with the JS for pagination & sorting
1024	 */
1025	public static function GetDisplaySet(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
1026	{
1027		if ($oPage->IsPrintableVersion() || $oPage->is_pdf())
1028		{
1029			return self::GetDisplaySetForPrinting($oPage, $oSet, $aExtraParams);
1030		}
1031
1032		if (empty($aExtraParams['currentId']))
1033		{
1034			$iListId = $oPage->GetUniqueId(); // Works only if not in an Ajax page !!
1035		}
1036		else
1037		{
1038			$iListId = $aExtraParams['currentId'];
1039		}
1040
1041		// Initialize and check the parameters
1042		$bViewLink = isset($aExtraParams['view_link']) ? $aExtraParams['view_link'] : true;
1043		$sLinkageAttribute = isset($aExtraParams['link_attr']) ? $aExtraParams['link_attr'] : '';
1044		$iLinkedObjectId = isset($aExtraParams['object_id']) ? $aExtraParams['object_id'] : 0;
1045		$sTargetAttr = isset($aExtraParams['target_attr']) ? $aExtraParams['target_attr'] : '';
1046		if (!empty($sLinkageAttribute))
1047		{
1048			if ($iLinkedObjectId == 0)
1049			{
1050				// if 'links' mode is requested the id of the object to link to must be specified
1051				throw new ApplicationException(Dict::S('UI:Error:MandatoryTemplateParameter_object_id'));
1052			}
1053			if ($sTargetAttr == '')
1054			{
1055				// if 'links' mode is requested the d of the object to link to must be specified
1056				throw new ApplicationException(Dict::S('UI:Error:MandatoryTemplateParameter_target_attr'));
1057			}
1058		}
1059		$bDisplayMenu = isset($aExtraParams['menu']) ? $aExtraParams['menu'] == true : true;
1060		$bSelectMode = isset($aExtraParams['selection_mode']) ? $aExtraParams['selection_mode'] == true : false;
1061		$bSingleSelectMode = isset($aExtraParams['selection_type']) ? ($aExtraParams['selection_type'] == 'single') : false;
1062
1063		$aExtraFieldsRaw = isset($aExtraParams['extra_fields']) ? explode(',',
1064			trim($aExtraParams['extra_fields'])) : array();
1065		$aExtraFields = array();
1066		foreach($aExtraFieldsRaw as $sFieldName)
1067		{
1068			// Ignore attributes not of the main queried class
1069			if (preg_match('/^(.*)\.(.*)$/', $sFieldName, $aMatches))
1070			{
1071				$sClassAlias = $aMatches[1];
1072				$sAttCode = $aMatches[2];
1073				if ($sClassAlias == $oSet->GetFilter()->GetClassAlias())
1074				{
1075					$aExtraFields[] = $sAttCode;
1076				}
1077			}
1078			else
1079			{
1080				$aExtraFields[] = $sFieldName;
1081			}
1082		}
1083		$sClassName = $oSet->GetFilter()->GetClass();
1084		$sZListName = isset($aExtraParams['zlist']) ? ($aExtraParams['zlist']) : 'list';
1085		if ($sZListName !== false)
1086		{
1087			$aList = self::FlattenZList(MetaModel::GetZListItems($sClassName, $sZListName));
1088			$aList = array_merge($aList, $aExtraFields);
1089		}
1090		else
1091		{
1092			$aList = $aExtraFields;
1093		}
1094
1095		// Filter the list to removed linked set since we are not able to display them here
1096		foreach($aList as $index => $sAttCode)
1097		{
1098			$oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
1099			if ($oAttDef instanceof AttributeLinkedSet)
1100			{
1101				// Removed from the display list
1102				unset($aList[$index]);
1103			}
1104		}
1105
1106
1107		if (!empty($sLinkageAttribute))
1108		{
1109			// The set to display is in fact a set of links between the object specified in the $sLinkageAttribute
1110			// and other objects...
1111			// The display will then group all the attributes related to the link itself:
1112			// | Link_attr1 | link_attr2 | ... || Object_attr1 | Object_attr2 | Object_attr3 | .. | Object_attr_n |
1113			$aDisplayList = array();
1114			$aAttDefs = MetaModel::ListAttributeDefs($sClassName);
1115			assert(isset($aAttDefs[$sLinkageAttribute]));
1116			$oAttDef = $aAttDefs[$sLinkageAttribute];
1117			assert($oAttDef->IsExternalKey());
1118			// First display all the attributes specific to the link record
1119			foreach($aList as $sLinkAttCode)
1120			{
1121				$oLinkAttDef = $aAttDefs[$sLinkAttCode];
1122				if ((!$oLinkAttDef->IsExternalKey()) && (!$oLinkAttDef->IsExternalField()))
1123				{
1124					$aDisplayList[] = $sLinkAttCode;
1125				}
1126			}
1127			// Then display all the attributes neither specific to the link record nor to the 'linkage' object (because the latter are constant)
1128			foreach($aList as $sLinkAttCode)
1129			{
1130				$oLinkAttDef = $aAttDefs[$sLinkAttCode];
1131				if (($oLinkAttDef->IsExternalKey() && ($sLinkAttCode != $sLinkageAttribute))
1132					|| ($oLinkAttDef->IsExternalField() && ($oLinkAttDef->GetKeyAttCode() != $sLinkageAttribute)))
1133				{
1134					$aDisplayList[] = $sLinkAttCode;
1135				}
1136			}
1137			// First display all the attributes specific to the link
1138			// Then display all the attributes linked to the other end of the relationship
1139			$aList = $aDisplayList;
1140		}
1141
1142		$sSelectMode = 'none';
1143		if ($bSelectMode)
1144		{
1145			$sSelectMode = $bSingleSelectMode ? 'single' : 'multiple';
1146		}
1147
1148		$sClassAlias = $oSet->GetClassAlias();
1149		$bDisplayLimit = isset($aExtraParams['display_limit']) ? $aExtraParams['display_limit'] : true;
1150
1151		$sTableId = isset($aExtraParams['table_id']) ? $aExtraParams['table_id'] : null;
1152		$aClassAliases = array($sClassAlias => $sClassName);
1153		$oDataTable = new DataTable($iListId, $oSet, $aClassAliases, $sTableId);
1154		$oSettings = DataTableSettings::GetDataModelSettings($aClassAliases, $bViewLink, array($sClassAlias => $aList));
1155
1156		if ($bDisplayLimit)
1157		{
1158			$iDefaultPageSize = appUserPreferences::GetPref('default_page_size',
1159				MetaModel::GetConfig()->GetMinDisplayLimit());
1160			$oSettings->iDefaultPageSize = $iDefaultPageSize;
1161		}
1162		else
1163		{
1164			$oSettings->iDefaultPageSize = 0;
1165		}
1166		$oSettings->aSortOrder = MetaModel::GetOrderByDefault($sClassName);
1167
1168		return $oDataTable->Display($oPage, $oSettings, $bDisplayMenu, $sSelectMode, $bViewLink, $aExtraParams);
1169	}
1170
1171	public static function GetDisplayExtendedSet(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
1172	{
1173		if (empty($aExtraParams['currentId']))
1174		{
1175			$iListId = $oPage->GetUniqueId(); // Works only if not in an Ajax page !!
1176		}
1177		else
1178		{
1179			$iListId = $aExtraParams['currentId'];
1180		}
1181		$aList = array();
1182
1183		// Initialize and check the parameters
1184		$bViewLink = isset($aExtraParams['view_link']) ? $aExtraParams['view_link'] : true;
1185		$bDisplayMenu = isset($aExtraParams['menu']) ? $aExtraParams['menu'] == true : true;
1186		// Check if there is a list of aliases to limit the display to...
1187		$aDisplayAliases = isset($aExtraParams['display_aliases']) ? explode(',',
1188			$aExtraParams['display_aliases']) : array();
1189		$sZListName = isset($aExtraParams['zlist']) ? ($aExtraParams['zlist']) : 'list';
1190
1191		$aExtraFieldsRaw = isset($aExtraParams['extra_fields']) ? explode(',',
1192			trim($aExtraParams['extra_fields'])) : array();
1193		$aExtraFields = array();
1194		$sAttCode = '';
1195		foreach($aExtraFieldsRaw as $sFieldName)
1196		{
1197			// Ignore attributes not of the main queried class
1198			if (preg_match('/^(.*)\.(.*)$/', $sFieldName, $aMatches))
1199			{
1200				$sClassAlias = $aMatches[1];
1201				$sAttCode = $aMatches[2];
1202				if (array_key_exists($sClassAlias, $oSet->GetSelectedClasses()))
1203				{
1204					$aExtraFields[$sClassAlias][] = $sAttCode;
1205				}
1206			}
1207			else
1208			{
1209				$aExtraFields['*'] = $sAttCode;
1210			}
1211		}
1212
1213		$aClasses = $oSet->GetFilter()->GetSelectedClasses();
1214		$aAuthorizedClasses = array();
1215		foreach($aClasses as $sAlias => $sClassName)
1216		{
1217			if ((UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO) &&
1218				((count($aDisplayAliases) == 0) || (in_array($sAlias, $aDisplayAliases))))
1219			{
1220				$aAuthorizedClasses[$sAlias] = $sClassName;
1221			}
1222		}
1223		foreach($aAuthorizedClasses as $sAlias => $sClassName)
1224		{
1225			if (array_key_exists($sAlias, $aExtraFields))
1226			{
1227				$aList[$sAlias] = $aExtraFields[$sAlias];
1228			}
1229			else
1230			{
1231				$aList[$sAlias] = array();
1232			}
1233			if ($sZListName !== false)
1234			{
1235				$aDefaultList = self::FlattenZList(MetaModel::GetZListItems($sClassName, $sZListName));
1236
1237				$aList[$sAlias] = array_merge($aDefaultList, $aList[$sAlias]);
1238			}
1239
1240			// Filter the list to removed linked set since we are not able to display them here
1241			foreach($aList[$sAlias] as $index => $sAttCode)
1242			{
1243				$oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
1244				if ($oAttDef instanceof AttributeLinkedSet)
1245				{
1246					// Removed from the display list
1247					unset($aList[$sAlias][$index]);
1248				}
1249			}
1250		}
1251
1252		$sSelectMode = 'none';
1253
1254		$oDataTable = new DataTable($iListId, $oSet, $aAuthorizedClasses);
1255
1256		$oSettings = DataTableSettings::GetDataModelSettings($aAuthorizedClasses, $bViewLink, $aList);
1257
1258		$bDisplayLimit = isset($aExtraParams['display_limit']) ? $aExtraParams['display_limit'] : true;
1259		if ($bDisplayLimit)
1260		{
1261			$iDefaultPageSize = appUserPreferences::GetPref('default_page_size',
1262				MetaModel::GetConfig()->GetMinDisplayLimit());
1263			$oSettings->iDefaultPageSize = $iDefaultPageSize;
1264		}
1265
1266		$oSettings->aSortOrder = MetaModel::GetOrderByDefault($sClassName);
1267
1268		return $oDataTable->Display($oPage, $oSettings, $bDisplayMenu, $sSelectMode, $bViewLink, $aExtraParams);
1269	}
1270
1271	static function DisplaySetAsCSV(WebPage $oPage, CMDBObjectSet $oSet, $aParams = array(), $sCharset = 'UTF-8')
1272	{
1273		$oPage->add(self::GetSetAsCSV($oSet, $aParams, $sCharset));
1274	}
1275
1276	static function GetSetAsCSV(DBObjectSet $oSet, $aParams = array(), $sCharset = 'UTF-8')
1277	{
1278		$sSeparator = isset($aParams['separator']) ? $aParams['separator'] : ','; // default separator is comma
1279		$sTextQualifier = isset($aParams['text_qualifier']) ? $aParams['text_qualifier'] : '"'; // default text qualifier is double quote
1280		$aFields = null;
1281		if (isset($aParams['fields']) && (strlen($aParams['fields']) > 0))
1282		{
1283			$aFields = explode(',', $aParams['fields']);
1284		}
1285
1286		$bFieldsAdvanced = false;
1287		if (isset($aParams['fields_advanced']))
1288		{
1289			$bFieldsAdvanced = (bool)$aParams['fields_advanced'];
1290		}
1291
1292		$bLocalize = true;
1293		if (isset($aParams['localize_values']))
1294		{
1295			$bLocalize = (bool)$aParams['localize_values'];
1296		}
1297
1298		$aList = array();
1299
1300		$aClasses = $oSet->GetFilter()->GetSelectedClasses();
1301		$aAuthorizedClasses = array();
1302		foreach($aClasses as $sAlias => $sClassName)
1303		{
1304			if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO)
1305			{
1306				$aAuthorizedClasses[$sAlias] = $sClassName;
1307			}
1308		}
1309		$aHeader = array();
1310		foreach($aAuthorizedClasses as $sAlias => $sClassName)
1311		{
1312			$aList[$sAlias] = array();
1313
1314			foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
1315			{
1316				if (is_null($aFields) || (count($aFields) == 0))
1317				{
1318					// Standard list of attributes (no link sets)
1319					if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField()))
1320					{
1321						$sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
1322
1323						if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE))
1324						{
1325							if ($bFieldsAdvanced)
1326							{
1327								$aList[$sAlias][$sAttCodeEx] = $oAttDef;
1328
1329								if ($oAttDef->IsExternalKey(EXTKEY_RELATIVE))
1330								{
1331									$sRemoteClass = $oAttDef->GetTargetClass();
1332									foreach(MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode)
1333									{
1334										$aList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef($sRemoteClass,
1335											$sRemoteAttCode);
1336									}
1337								}
1338							}
1339						}
1340						else
1341						{
1342							// Any other attribute
1343							$aList[$sAlias][$sAttCodeEx] = $oAttDef;
1344						}
1345					}
1346				}
1347				else
1348				{
1349					// User defined list of attributes
1350					if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields))
1351					{
1352						$aList[$sAlias][$sAttCode] = $oAttDef;
1353					}
1354				}
1355			}
1356			if ($bFieldsAdvanced)
1357			{
1358				$aHeader[] = 'id';
1359			}
1360			foreach($aList[$sAlias] as $sAttCodeEx => $oAttDef)
1361			{
1362				$aHeader[] = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx,
1363					isset($aParams['showMandatoryFields'])) : $sAttCodeEx;
1364			}
1365		}
1366		$sHtml = implode($sSeparator, $aHeader)."\n";
1367		$oSet->Seek(0);
1368		while ($aObjects = $oSet->FetchAssoc())
1369		{
1370			$aRow = array();
1371			foreach($aAuthorizedClasses as $sAlias => $sClassName)
1372			{
1373				$oObj = $aObjects[$sAlias];
1374				if ($bFieldsAdvanced)
1375				{
1376					if (is_null($oObj))
1377					{
1378						$aRow[] = '';
1379					}
1380					else
1381					{
1382						$aRow[] = $oObj->GetKey();
1383					}
1384				}
1385				foreach($aList[$sAlias] as $sAttCodeEx => $oAttDef)
1386				{
1387					if (is_null($oObj))
1388					{
1389						$aRow[] = '';
1390					}
1391					else
1392					{
1393						$value = $oObj->Get($sAttCodeEx);
1394						$sCSVValue = $oAttDef->GetAsCSV($value, $sSeparator, $sTextQualifier, $oObj, $bLocalize);
1395						$aRow[] = iconv('UTF-8', $sCharset.'//IGNORE//TRANSLIT', $sCSVValue);
1396					}
1397				}
1398			}
1399			$sHtml .= implode($sSeparator, $aRow)."\n";
1400		}
1401
1402		return $sHtml;
1403	}
1404
1405	static function DisplaySetAsHTMLSpreadsheet(WebPage $oPage, CMDBObjectSet $oSet, $aParams = array())
1406	{
1407		$oPage->add(self::GetSetAsHTMLSpreadsheet($oSet, $aParams));
1408	}
1409
1410	/**
1411	 * Spreadsheet output: designed for end users doing some reporting
1412	 * Then the ids are excluded and replaced by the corresponding friendlyname
1413	 */
1414	static function GetSetAsHTMLSpreadsheet(DBObjectSet $oSet, $aParams = array())
1415	{
1416		$aFields = null;
1417		if (isset($aParams['fields']) && (strlen($aParams['fields']) > 0))
1418		{
1419			$aFields = explode(',', $aParams['fields']);
1420		}
1421
1422		$bFieldsAdvanced = false;
1423		if (isset($aParams['fields_advanced']))
1424		{
1425			$bFieldsAdvanced = (bool)$aParams['fields_advanced'];
1426		}
1427
1428		$bLocalize = true;
1429		if (isset($aParams['localize_values']))
1430		{
1431			$bLocalize = (bool)$aParams['localize_values'];
1432		}
1433
1434		$aList = array();
1435
1436		$aClasses = $oSet->GetFilter()->GetSelectedClasses();
1437		$aAuthorizedClasses = array();
1438		foreach($aClasses as $sAlias => $sClassName)
1439		{
1440			if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO)
1441			{
1442				$aAuthorizedClasses[$sAlias] = $sClassName;
1443			}
1444		}
1445		$aHeader = array();
1446		foreach($aAuthorizedClasses as $sAlias => $sClassName)
1447		{
1448			$aList[$sAlias] = array();
1449
1450			foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
1451			{
1452				if (is_null($aFields) || (count($aFields) == 0))
1453				{
1454					// Standard list of attributes (no link sets)
1455					if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField()))
1456					{
1457						$sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
1458
1459						$aList[$sAlias][$sAttCodeEx] = $oAttDef;
1460
1461						if ($bFieldsAdvanced && $oAttDef->IsExternalKey(EXTKEY_RELATIVE))
1462						{
1463							$sRemoteClass = $oAttDef->GetTargetClass();
1464							foreach(MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode)
1465							{
1466								$aList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef($sRemoteClass,
1467									$sRemoteAttCode);
1468							}
1469						}
1470					}
1471				}
1472				else
1473				{
1474					// User defined list of attributes
1475					if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields))
1476					{
1477						$aList[$sAlias][$sAttCode] = $oAttDef;
1478					}
1479				}
1480			}
1481			// Replace external key by the corresponding friendly name (if not already in the list)
1482			foreach($aList[$sAlias] as $sAttCode => $oAttDef)
1483			{
1484				if ($oAttDef->IsExternalKey())
1485				{
1486					unset($aList[$sAlias][$sAttCode]);
1487					$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
1488					if (!array_key_exists($sFriendlyNameAttCode,
1489							$aList[$sAlias]) && MetaModel::IsValidAttCode($sClassName, $sFriendlyNameAttCode))
1490					{
1491						$oFriendlyNameAtt = MetaModel::GetAttributeDef($sClassName, $sFriendlyNameAttCode);
1492						$aList[$sAlias][$sFriendlyNameAttCode] = $oFriendlyNameAtt;
1493					}
1494				}
1495			}
1496
1497			foreach($aList[$sAlias] as $sAttCodeEx => $oAttDef)
1498			{
1499				$sColLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx) : $sAttCodeEx;
1500
1501				$oFinalAttDef = $oAttDef->GetFinalAttDef();
1502				if (get_class($oFinalAttDef) == 'AttributeDateTime')
1503				{
1504					$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Date').')';
1505					$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Time').')';
1506				}
1507				else
1508				{
1509					$aHeader[] = $sColLabel;
1510				}
1511			}
1512		}
1513
1514
1515		$sHtml = "<table border=\"1\">\n";
1516		$sHtml .= "<tr>\n";
1517		$sHtml .= "<td>".implode("</td><td>", $aHeader)."</td>\n";
1518		$sHtml .= "</tr>\n";
1519		$oSet->Seek(0);
1520		while ($aObjects = $oSet->FetchAssoc())
1521		{
1522			$aRow = array();
1523			foreach($aAuthorizedClasses as $sAlias => $sClassName)
1524			{
1525				$oObj = $aObjects[$sAlias];
1526				foreach($aList[$sAlias] as $sAttCodeEx => $oAttDef)
1527				{
1528					if (is_null($oObj))
1529					{
1530						$aRow[] = '<td></td>';
1531					}
1532					else
1533					{
1534						$oFinalAttDef = $oAttDef->GetFinalAttDef();
1535						if (get_class($oFinalAttDef) == 'AttributeDateTime')
1536						{
1537							$sDate = $oObj->Get($sAttCodeEx);
1538							if ($sDate === null)
1539							{
1540								$aRow[] = '<td></td>';
1541								$aRow[] = '<td></td>';
1542							}
1543							else
1544							{
1545								$iDate = AttributeDateTime::GetAsUnixSeconds($sDate);
1546								$aRow[] = '<td>'.date('Y-m-d',
1547										$iDate).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
1548								$aRow[] = '<td>'.date('H:i:s',
1549										$iDate).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
1550							}
1551						}
1552						else
1553						{
1554							if ($oAttDef instanceof AttributeCaseLog)
1555							{
1556								$rawValue = $oObj->Get($sAttCodeEx);
1557								$outputValue = str_replace("\n", "<br/>",
1558									htmlentities($rawValue->__toString(), ENT_QUOTES, 'UTF-8'));
1559								// Trick for Excel: treat the content as text even if it begins with an equal sign
1560								$aRow[] = '<td x:str>'.$outputValue.'</td>';
1561							}
1562							else
1563							{
1564								$rawValue = $oObj->Get($sAttCodeEx);
1565								// Due to custom formatting rules, empty friendlynames may be rendered as non-empty strings
1566								// let's fix this and make sure we render an empty string if the key == 0
1567								if ($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName())
1568								{
1569									$sKeyAttCode = $oAttDef->GetKeyAttCode();
1570									if ($oObj->Get($sKeyAttCode) == 0)
1571									{
1572										$rawValue = '';
1573									}
1574								}
1575								if ($bLocalize)
1576								{
1577									$outputValue = htmlentities($oFinalAttDef->GetEditValue($rawValue), ENT_QUOTES,
1578										'UTF-8');
1579								}
1580								else
1581								{
1582									$outputValue = htmlentities($rawValue, ENT_QUOTES, 'UTF-8');
1583								}
1584								$aRow[] = '<td>'.$outputValue.'</td>';
1585							}
1586						}
1587					}
1588				}
1589			}
1590			$sHtml .= implode("\n", $aRow);
1591			$sHtml .= "</tr>\n";
1592		}
1593		$sHtml .= "</table>\n";
1594
1595		return $sHtml;
1596	}
1597
1598	static function DisplaySetAsXML(WebPage $oPage, CMDBObjectSet $oSet, $aParams = array())
1599	{
1600		$bLocalize = true;
1601		if (isset($aParams['localize_values']))
1602		{
1603			$bLocalize = (bool)$aParams['localize_values'];
1604		}
1605
1606		$aClasses = $oSet->GetFilter()->GetSelectedClasses();
1607		$aAuthorizedClasses = array();
1608		foreach($aClasses as $sAlias => $sClassName)
1609		{
1610			if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO)
1611			{
1612				$aAuthorizedClasses[$sAlias] = $sClassName;
1613			}
1614		}
1615		$aList = array();
1616		$aList[$sAlias] = MetaModel::GetZListItems($sClassName, 'details');
1617		$oPage->add("<Set>\n");
1618		$oSet->Seek(0);
1619		while ($aObjects = $oSet->FetchAssoc())
1620		{
1621			if (count($aAuthorizedClasses) > 1)
1622			{
1623				$oPage->add("<Row>\n");
1624			}
1625			foreach($aAuthorizedClasses as $sAlias => $sClassName)
1626			{
1627				$oObj = $aObjects[$sAlias];
1628				if (is_null($oObj))
1629				{
1630					$oPage->add("<$sClassName alias=\"$sAlias\" id=\"null\">\n");
1631				}
1632				else
1633				{
1634					$sClassName = get_class($oObj);
1635					$oPage->add("<$sClassName alias=\"$sAlias\" id=\"".$oObj->GetKey()."\">\n");
1636				}
1637				foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
1638				{
1639					if (is_null($oObj))
1640					{
1641						$oPage->add("<$sAttCode>null</$sAttCode>\n");
1642					}
1643					else
1644					{
1645						if ($oAttDef->IsWritable())
1646						{
1647							if (!$oAttDef->IsLinkSet())
1648							{
1649								$sValue = $oObj->GetAsXML($sAttCode, $bLocalize);
1650								$oPage->add("<$sAttCode>$sValue</$sAttCode>\n");
1651							}
1652						}
1653					}
1654				}
1655				$oPage->add("</$sClassName>\n");
1656			}
1657			if (count($aAuthorizedClasses) > 1)
1658			{
1659				$oPage->add("</Row>\n");
1660			}
1661		}
1662		$oPage->add("</Set>\n");
1663	}
1664
1665	public static function DisplaySearchForm(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
1666	{
1667
1668		$oPage->add(self::GetSearchForm($oPage, $oSet, $aExtraParams));
1669	}
1670
1671	/**
1672	 * @param WebPage $oPage
1673	 * @param CMDBObjectSet $oSet
1674	 * @param array $aExtraParams
1675	 *
1676	 * @return string
1677	 * @throws CoreException
1678	 * @throws DictExceptionMissingString
1679	 */
1680	public static function GetSearchForm(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
1681	{
1682		$oSearchForm = new \Combodo\iTop\Application\Search\SearchForm();
1683
1684		return $oSearchForm->GetSearchForm($oPage, $oSet, $aExtraParams);
1685	}
1686
1687	/**
1688	 * @param \iTopWebPage $oPage
1689	 * @param string $sClass
1690	 * @param string $sAttCode
1691	 * @param \AttributeDefinition $oAttDef
1692	 * @param string $value
1693	 * @param string $sDisplayValue
1694	 * @param string $iId
1695	 * @param string $sNameSuffix
1696	 * @param int $iFlags
1697	 * @param array $aArgs
1698	 * @param bool $bPreserveCurrentValue Preserve the current value even if not allowed
1699	 *
1700	 * @return string
1701	 * @throws \ArchivedObjectException
1702	 * @throws \CoreException
1703	 * @throws \DictExceptionMissingString
1704	 */
1705	public static function GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $value = '', $sDisplayValue = '', $iId = '', $sNameSuffix = '',	$iFlags = 0, $aArgs = array(), $bPreserveCurrentValue = true)
1706	{
1707		$sFormPrefix = isset($aArgs['formPrefix']) ? $aArgs['formPrefix'] : '';
1708		$sFieldPrefix = isset($aArgs['prefix']) ? $sFormPrefix.$aArgs['prefix'] : $sFormPrefix;
1709		if ($sDisplayValue == '')
1710		{
1711			$sDisplayValue = $value;
1712		}
1713
1714		if (isset($aArgs[$sAttCode]) && empty($value))
1715		{
1716			// default value passed by the context (either the app context of the operation)
1717			$value = $aArgs[$sAttCode];
1718		}
1719
1720		if (!empty($iId))
1721		{
1722			$iInputId = $iId;
1723		}
1724		else
1725		{
1726			$iInputId = $oPage->GetUniqueId();
1727		}
1728
1729		$sHTMLValue = '';
1730		if (!$oAttDef->IsExternalField())
1731		{
1732			$bMandatory = 'false';
1733			if ((!$oAttDef->IsNullAllowed()) || ($iFlags & OPT_ATT_MANDATORY))
1734			{
1735				$bMandatory = 'true';
1736			}
1737			$sValidationSpan = "<span class=\"form_validation\" id=\"v_{$iId}\"></span>";
1738			$sReloadSpan = "<span class=\"field_status\" id=\"fstatus_{$iId}\"></span>";
1739			$sHelpText = htmlentities($oAttDef->GetHelpOnEdition(), ENT_QUOTES, 'UTF-8');
1740
1741			// mandatory field control vars
1742			$aEventsList = array(); // contains any native event (like change), plus 'validate' for the form submission
1743			$sNullValue = $oAttDef->GetNullValue(); // used for the ValidateField() call in js/forms-json-utils.js
1744			$sFieldToValidateId = $iId; // can be different than the displayed field (for example in TagSet)
1745
1746			switch ($oAttDef->GetEditClass())
1747			{
1748				case 'Date':
1749					$aEventsList[] = 'validate';
1750					$aEventsList[] = 'keyup';
1751					$aEventsList[] = 'change';
1752					$sPlaceholderValue = 'placeholder="'.htmlentities(AttributeDate::GetFormat()->ToPlaceholder(),
1753							ENT_QUOTES, 'UTF-8').'"';
1754
1755					$sHTMLValue = "<div class=\"field_input_zone field_input_date\"><input title=\"$sHelpText\" class=\"date-pick\" type=\"text\" $sPlaceholderValue name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue,
1756							ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/></div>{$sValidationSpan}{$sReloadSpan}";
1757					break;
1758
1759				case 'DateTime':
1760					$aEventsList[] = 'validate';
1761					$aEventsList[] = 'keyup';
1762					$aEventsList[] = 'change';
1763
1764					$sPlaceholderValue = 'placeholder="'.htmlentities(AttributeDateTime::GetFormat()->ToPlaceholder(),
1765							ENT_QUOTES, 'UTF-8').'"';
1766					$sHTMLValue = "<div class=\"field_input_zone field_input_datetime\"><input title=\"$sHelpText\" class=\"datetime-pick\" type=\"text\" size=\"19\" $sPlaceholderValue name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue,
1767							ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/></div>{$sValidationSpan}{$sReloadSpan}";
1768					break;
1769
1770				case 'Duration':
1771					$aEventsList[] = 'validate';
1772					$aEventsList[] = 'change';
1773					$oPage->add_ready_script("$('#{$iId}_d').bind('keyup change', function(evt, sFormId) { return UpdateDuration('$iId'); });");
1774					$oPage->add_ready_script("$('#{$iId}_h').bind('keyup change', function(evt, sFormId) { return UpdateDuration('$iId'); });");
1775					$oPage->add_ready_script("$('#{$iId}_m').bind('keyup change', function(evt, sFormId) { return UpdateDuration('$iId'); });");
1776					$oPage->add_ready_script("$('#{$iId}_s').bind('keyup change', function(evt, sFormId) { return UpdateDuration('$iId'); });");
1777					$aVal = AttributeDuration::SplitDuration($value);
1778					$sDays = "<input title=\"$sHelpText\" type=\"text\" style=\"text-align:right\" size=\"3\" name=\"attr_{$sFieldPrefix}{$sAttCode}[d]{$sNameSuffix}\" value=\"{$aVal['days']}\" id=\"{$iId}_d\"/>";
1779					$sHours = "<input title=\"$sHelpText\" type=\"text\" style=\"text-align:right\" size=\"2\" name=\"attr_{$sFieldPrefix}{$sAttCode}[h]{$sNameSuffix}\" value=\"{$aVal['hours']}\" id=\"{$iId}_h\"/>";
1780					$sMinutes = "<input title=\"$sHelpText\" type=\"text\" style=\"text-align:right\" size=\"2\" name=\"attr_{$sFieldPrefix}{$sAttCode}[m]{$sNameSuffix}\" value=\"{$aVal['minutes']}\" id=\"{$iId}_m\"/>";
1781					$sSeconds = "<input title=\"$sHelpText\" type=\"text\" style=\"text-align:right\" size=\"2\" name=\"attr_{$sFieldPrefix}{$sAttCode}[s]{$sNameSuffix}\" value=\"{$aVal['seconds']}\" id=\"{$iId}_s\"/>";
1782					$sHidden = "<input type=\"hidden\" id=\"{$iId}\" value=\"".htmlentities($value, ENT_QUOTES,
1783							'UTF-8')."\"/>";
1784					$sHTMLValue = Dict::Format('UI:DurationForm_Days_Hours_Minutes_Seconds', $sDays, $sHours, $sMinutes,
1785							$sSeconds).$sHidden."&nbsp;".$sValidationSpan.$sReloadSpan;
1786					$oPage->add_ready_script("$('#{$iId}').bind('update', function(evt, sFormId) { return ToggleDurationField('$iId'); });");
1787					break;
1788
1789				case 'Password':
1790					$aEventsList[] = 'validate';
1791					$aEventsList[] = 'keyup';
1792					$aEventsList[] = 'change';
1793					$sHTMLValue = "<div class=\"field_input_zone field_input_password\"><input title=\"$sHelpText\" type=\"password\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($value,
1794							ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/></div>{$sValidationSpan}{$sReloadSpan}";
1795					break;
1796
1797				case 'OQLExpression':
1798				case 'Text':
1799					$aEventsList[] = 'validate';
1800					$aEventsList[] = 'keyup';
1801					$aEventsList[] = 'change';
1802					$sEditValue = $oAttDef->GetEditValue($value);
1803
1804					$aStyles = array();
1805					$sStyle = '';
1806					$sWidth = $oAttDef->GetWidth('width', '');
1807					if (!empty($sWidth))
1808					{
1809						$aStyles[] = 'width:'.$sWidth;
1810					}
1811					$sHeight = $oAttDef->GetHeight('height', '');
1812					if (!empty($sHeight))
1813					{
1814						$aStyles[] = 'height:'.$sHeight;
1815					}
1816					if (count($aStyles) > 0)
1817					{
1818						$sStyle = 'style="'.implode('; ', $aStyles).'"';
1819					}
1820
1821					if ($oAttDef->GetEditClass() == 'OQLExpression')
1822					{
1823						$sTestResId = 'query_res_'.$sFieldPrefix.$sAttCode.$sNameSuffix; //$oPage->GetUniqueId();
1824						$sBaseUrl = utils::GetAbsoluteUrlAppRoot().'pages/run_query.php?expression=';
1825						$sInitialUrl = $sBaseUrl.urlencode($sEditValue);
1826						$sAdditionalStuff = "<a id=\"$sTestResId\" target=\"_blank\" href=\"$sInitialUrl\">".Dict::S('UI:Edit:TestQuery')."</a>";
1827						$oPage->add_ready_script("$('#$iId').bind('change keyup', function(evt, sFormId) { $('#$sTestResId').attr('href', '$sBaseUrl'+encodeURIComponent($(this).val())); } );");
1828					}
1829					else
1830					{
1831						$sAdditionalStuff = "";
1832					}
1833					// Ok, the text area is drawn here
1834					$sHTMLValue = "<div class=\"field_input_zone field_input_text\"><div class=\"f_i_text_header\"><span class=\"fullscreen_button\" title=\"".Dict::S('UI:ToggleFullScreen')."\"></span></div><textarea class=\"\" title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" rows=\"8\" cols=\"40\" id=\"$iId\" $sStyle>".htmlentities($sEditValue,
1835							ENT_QUOTES, 'UTF-8')."</textarea>$sAdditionalStuff</div>{$sValidationSpan}{$sReloadSpan}";
1836
1837					$oPage->add_ready_script(
1838						<<<EOF
1839                        $('#$iId').closest('.field_input_text').find('.fullscreen_button').on('click', function(oEvent){
1840                            var oOriginField = $('#$iId').closest('.field_input_text');
1841                            var oClonedField = oOriginField.clone();
1842                            oClonedField.addClass('fullscreen').appendTo('body');
1843                            oClonedField.find('.fullscreen_button').on('click', function(oEvent){
1844                                // Copying value to origin field
1845                                oOriginField.find('textarea').val(oClonedField.find('textarea').val());
1846                                oClonedField.remove();
1847                                // Triggering change event
1848                                oOriginField.find('textarea').triggerHandler('change');
1849                            });
1850                        });
1851EOF
1852					);
1853					break;
1854
1855				case 'CaseLog':
1856					$aStyles = array();
1857					$sStyle = '';
1858					$sWidth = $oAttDef->GetWidth('width', '');
1859					if (!empty($sWidth))
1860					{
1861						$aStyles[] = 'width:'.$sWidth;
1862					}
1863					$sHeight = $oAttDef->GetHeight('height', '');
1864					if (!empty($sHeight))
1865					{
1866						$aStyles[] = 'height:'.$sHeight;
1867					}
1868					if (count($aStyles) > 0)
1869					{
1870						$sStyle = 'style="'.implode('; ', $aStyles).'"';
1871					}
1872
1873					$sHeader = '<div class="caselog_input_header"></div>'; // will be hidden in CSS (via :empty) if it remains empty
1874					$sEditValue = is_object($value) ? $value->GetModifiedEntry('html') : '';
1875					$sPreviousLog = is_object($value) ? $value->GetAsHTML($oPage, true /* bEditMode */,
1876						array('AttributeText', 'RenderWikiHtml')) : '';
1877					$iEntriesCount = is_object($value) ? count($value->GetIndex()) : 0;
1878					$sHidden = "<input type=\"hidden\" id=\"{$iId}_count\" value=\"$iEntriesCount\"/>"; // To know how many entries the case log already contains
1879
1880					$sHTMLValue = "<div class=\"field_input_zone field_input_caselog caselog\" $sStyle>$sHeader<textarea class=\"htmlEditor\" style=\"border:0;width:100%\" title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" rows=\"8\" cols=\"40\" id=\"$iId\">".htmlentities($sEditValue,
1881							ENT_QUOTES,
1882							'UTF-8')."</textarea>$sPreviousLog</div>{$sValidationSpan}{$sReloadSpan}$sHidden";
1883
1884					// Note: This should be refactored for all types of attribute (see at the end of this function) but as we are doing this for a maintenance release, we are scheduling it for the next main release in to order to avoid regressions as much as possible.
1885					$sNullValue = $oAttDef->GetNullValue();
1886					if (!is_numeric($sNullValue))
1887					{
1888						$sNullValue = "'$sNullValue'"; // Add quotes to turn this into a JS string if it's not a number
1889					}
1890					$sOriginalValue = ($iFlags & OPT_ATT_MUSTCHANGE) ? json_encode($value->GetModifiedEntry('html')) : 'undefined';
1891
1892					$oPage->add_ready_script("$('#$iId').bind('keyup change validate', function(evt, sFormId) { return ValidateCaseLogField('$iId', $bMandatory, sFormId, $sNullValue, $sOriginalValue) } );"); // Custom validation function
1893
1894					// Replace the text area with CKEditor
1895					// To change the default settings of the editor,
1896					// a) edit the file /js/ckeditor/config.js
1897					// b) or override some of the configuration settings, using the second parameter of ckeditor()
1898					$aConfig = array();
1899					$sLanguage = strtolower(trim(UserRights::GetUserLanguage()));
1900					$aConfig['language'] = $sLanguage;
1901					$aConfig['contentsLanguage'] = $sLanguage;
1902					$aConfig['extraPlugins'] = 'disabler';
1903					$aConfig['placeholder'] = Dict::S('UI:CaseLogTypeYourTextHere');
1904					$sConfigJS = json_encode($aConfig);
1905
1906					$oPage->add_ready_script("$('#$iId').ckeditor(function() { /* callback code */ }, $sConfigJS);"); // Transform $iId into a CKEdit
1907
1908					$oPage->add_ready_script(
1909<<<EOF
1910$('#$iId').bind('update', function(evt){
1911	BlockField('cke_$iId', $('#$iId').attr('disabled'));
1912	//Delayed execution - ckeditor must be properly initialized before setting readonly
1913	var retryCount = 0;
1914	var oMe = $('#$iId');
1915	var delayedSetReadOnly = function () {
1916		if (oMe.data('ckeditorInstance').editable() == undefined && retryCount++ < 10) {
1917			setTimeout(delayedSetReadOnly, retryCount * 100); //Wait a while longer each iteration
1918		}
1919		else
1920		{
1921			oMe.data('ckeditorInstance').setReadOnly(oMe.prop('disabled'));
1922		}
1923	};
1924	setTimeout(delayedSetReadOnly, 50);
1925});
1926EOF
1927					);
1928				break;
1929
1930				case 'HTML':
1931					$sEditValue = $oAttDef->GetEditValue($value);
1932					$oWidget = new UIHTMLEditorWidget($iId, $oAttDef, $sNameSuffix, $sFieldPrefix, $sHelpText,
1933						$sValidationSpan.$sReloadSpan, $sEditValue, $bMandatory);
1934					$sHTMLValue = $oWidget->Display($oPage, $aArgs);
1935					break;
1936
1937				case 'LinkedSet':
1938					if ($oAttDef->IsIndirect())
1939					{
1940						$oWidget = new UILinksWidget($sClass, $sAttCode, $iId, $sNameSuffix,
1941							$oAttDef->DuplicatesAllowed());
1942					}
1943					else
1944					{
1945						$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iId, $sNameSuffix);
1946					}
1947					$aEventsList[] = 'validate';
1948					$aEventsList[] = 'change';
1949					$oObj = isset($aArgs['this']) ? $aArgs['this'] : null;
1950					$sHTMLValue = $oWidget->Display($oPage, $value, array(), $sFormPrefix, $oObj);
1951					break;
1952
1953				case 'Document':
1954					$aEventsList[] = 'validate';
1955					$aEventsList[] = 'change';
1956					$oDocument = $value; // Value is an ormDocument object
1957					$sFileName = '';
1958					if (is_object($oDocument))
1959					{
1960						$sFileName = $oDocument->GetFileName();
1961					}
1962					$iMaxFileSize = utils::ConvertToBytes(ini_get('upload_max_filesize'));
1963					$sHTMLValue = "<div class=\"field_input_zone field_input_document\">\n";
1964					$sHTMLValue .= "<input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"$iMaxFileSize\" />\n";
1965					$sHTMLValue .= "<input name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}[filename]\" type=\"hidden\" id=\"$iId\" \" value=\"".htmlentities($sFileName,
1966							ENT_QUOTES, 'UTF-8')."\"/>\n";
1967					$sHTMLValue .= "<span id=\"name_$iInputId\"'>".htmlentities($sFileName, ENT_QUOTES,
1968							'UTF-8')."</span><br/>\n";
1969					$sHTMLValue .= "<input title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}[fcontents]\" type=\"file\" id=\"file_$iId\" onChange=\"UpdateFileName('$iId', this.value)\"/>\n";
1970					$sHTMLValue .= "</div>\n";
1971					$sHTMLValue .= "{$sValidationSpan}{$sReloadSpan}\n";
1972					break;
1973
1974				case 'Image':
1975					$aEventsList[] = 'validate';
1976					$aEventsList[] = 'change';
1977					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/edit_image.js');
1978					$oDocument = $value; // Value is an ormDocument object
1979					$sDefaultUrl = $oAttDef->Get('default_image');
1980					if (is_object($oDocument) && !$oDocument->IsEmpty())
1981					{
1982						$sUrl = 'data:'.$oDocument->GetMimeType().';base64,'.base64_encode($oDocument->GetData());
1983					}
1984					else
1985					{
1986						$sUrl = null;
1987					}
1988
1989					$sHTMLValue = "<div class=\"field_input_zone field_input_image\"><div id=\"edit_$iInputId\" class=\"edit-image\"></div></div>\n";
1990					$sHTMLValue .= "{$sValidationSpan}{$sReloadSpan}\n";
1991
1992					$aEditImage = array(
1993						'input_name' => 'attr_'.$sFieldPrefix.$sAttCode.$sNameSuffix,
1994						'max_file_size' => utils::ConvertToBytes(ini_get('upload_max_filesize')),
1995						'max_width_px' => $oAttDef->Get('display_max_width'),
1996						'max_height_px' => $oAttDef->Get('display_max_height'),
1997						'current_image_url' => $sUrl,
1998						'default_image_url' => $sDefaultUrl,
1999						'labels' => array(
2000							'reset_button' => htmlentities(Dict::S('UI:Button:ResetImage'), ENT_QUOTES, 'UTF-8'),
2001							'remove_button' => htmlentities(Dict::S('UI:Button:RemoveImage'), ENT_QUOTES, 'UTF-8'),
2002							'upload_button' => $sHelpText,
2003						),
2004					);
2005					$sEditImageOptions = json_encode($aEditImage);
2006					$oPage->add_ready_script("$('#edit_$iInputId').edit_image($sEditImageOptions);");
2007					break;
2008
2009				case 'StopWatch':
2010					$sHTMLValue = "The edition of a stopwatch is not allowed!!!";
2011					break;
2012
2013				case 'List':
2014					// Not editable for now...
2015					$sHTMLValue = '';
2016					break;
2017
2018				case 'One Way Password':
2019					$aEventsList[] = 'validate';
2020					$oWidget = new UIPasswordWidget($sAttCode, $iId, $sNameSuffix);
2021					$sHTMLValue = $oWidget->Display($oPage, $aArgs);
2022					// Event list & validation is handled  directly by the widget
2023					break;
2024
2025				case 'ExtKey':
2026					$aEventsList[] = 'validate';
2027					$aEventsList[] = 'change';
2028
2029					if ($bPreserveCurrentValue)
2030					{
2031						$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs, '', $value);
2032					}
2033					else
2034					{
2035						$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs);
2036					}
2037					$sFieldName = $sFieldPrefix.$sAttCode.$sNameSuffix;
2038					$aExtKeyParams = $aArgs;
2039					$aExtKeyParams['iFieldSize'] = $oAttDef->GetMaxSize();
2040					$aExtKeyParams['iMinChars'] = $oAttDef->GetMinAutoCompleteChars();
2041					$sHTMLValue = UIExtKeyWidget::DisplayFromAttCode($oPage, $sAttCode, $sClass, $oAttDef->GetLabel(),
2042						$oAllowedValues, $value, $iId, $bMandatory, $sFieldName, $sFormPrefix, $aExtKeyParams);
2043					$sHTMLValue .= "<!-- iFlags: $iFlags bMandatory: $bMandatory -->\n";
2044					break;
2045
2046				case 'RedundancySetting':
2047					$sHTMLValue = '<table>';
2048					$sHTMLValue .= '<tr>';
2049					$sHTMLValue .= '<td>';
2050					$sHTMLValue .= '<div id="'.$iId.'">';
2051					$sHTMLValue .= $oAttDef->GetDisplayForm($value, $oPage, true);
2052					$sHTMLValue .= '</div>';
2053					$sHTMLValue .= '</td>';
2054					$sHTMLValue .= '<td>'.$sValidationSpan.$sReloadSpan.'</td>';
2055					$sHTMLValue .= '</tr>';
2056					$sHTMLValue .= '</table>';
2057					$oPage->add_ready_script("$('#$iId :input').bind('keyup change validate', function(evt, sFormId) { return ValidateRedundancySettings('$iId',sFormId); } );"); // Custom validation function
2058					break;
2059
2060				case 'CustomFields':
2061					$sHTMLValue = '<table>';
2062					$sHTMLValue .= '<tr>';
2063					$sHTMLValue .= '<td>';
2064					$sHTMLValue .= '<div id="'.$iId.'_console_form">';
2065					$sHTMLValue .= '<div id="'.$iId.'_field_set">';
2066					$sHTMLValue .= '</div>';
2067					$sHTMLValue .= '</div>';
2068					$sHTMLValue .= '</td>';
2069					$sHTMLValue .= '<td>'.$sReloadSpan.'</td>'; // No validation span for this one: it does handle its own validation!
2070					$sHTMLValue .= '</tr>';
2071					$sHTMLValue .= '</table>';
2072					$sHTMLValue .= "<input name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" type=\"hidden\" id=\"$iId\" value=\"\"/>\n";
2073
2074					$oForm = $value->GetForm($sFormPrefix);
2075					$oRenderer = new \Combodo\iTop\Renderer\Console\ConsoleFormRenderer($oForm);
2076					$aRenderRes = $oRenderer->Render();
2077
2078					$aFieldSetOptions = array(
2079						'field_identifier_attr' => 'data-field-id',
2080						// convention: fields are rendered into a div and are identified by this attribute
2081						'fields_list' => $aRenderRes,
2082						'fields_impacts' => $oForm->GetFieldsImpacts(),
2083						'form_path' => $oForm->GetId(),
2084					);
2085					$sFieldSetOptions = json_encode($aFieldSetOptions);
2086					$aFormHandlerOptions = array(
2087						'wizard_helper_var_name' => 'oWizardHelper'.$sFormPrefix,
2088						'custom_field_attcode' => $sAttCode,
2089					);
2090					$sFormHandlerOptions = json_encode($aFormHandlerOptions);
2091					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/form_handler.js');
2092					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/console_form_handler.js');
2093					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/field_set.js');
2094					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/form_field.js');
2095					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/subform_field.js');
2096					$oPage->add_ready_script(
2097<<<EOF
2098    $('#{$iId}_field_set').field_set($sFieldSetOptions);
2099
2100    $('#{$iId}_console_form').console_form_handler($sFormHandlerOptions);
2101    $('#{$iId}_console_form').console_form_handler('alignColumns');
2102	$('#{$iId}_console_form').console_form_handler('option', 'field_set', $('#{$iId}_field_set'));
2103    // field_change must be processed to refresh the hidden value at anytime
2104    $('#{$iId}_console_form').bind('value_change', function() { $('#{$iId}').val(JSON.stringify($('#{$iId}_field_set').triggerHandler('get_current_values'))); console.error($('#{$iId}').val()); });
2105    // Initialize the hidden value with current state
2106    // update_value is triggered when preparing the wizard helper object for ajax calls
2107    $('#{$iId}').bind('update_value', function() { $(this).val(JSON.stringify($('#{$iId}_field_set').triggerHandler('get_current_values'))); });
2108    // validate is triggered by CheckFields, on all the input fields, once at page init and once before submitting the form
2109    $('#{$iId}').bind('validate', function(evt, sFormId) {
2110        $(this).val(JSON.stringify($('#{$iId}_field_set').triggerHandler('get_current_values')));
2111        return ValidateCustomFields('$iId', sFormId); // Custom validation function
2112    });
2113EOF
2114);
2115					break;
2116
2117				case 'Set':
2118				case 'TagSet':
2119					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'/js/selectize.min.js');
2120					$oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/selectize.default.css');
2121					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'/js/jquery.itop-set-widget.js');
2122
2123					$oPage->add_dict_entry('Core:AttributeSet:placeholder');
2124
2125					/** @var \ormSet $value */
2126					$sJson = $oAttDef->GetJsonForWidget($value, $aArgs);
2127					$sEscapedJson = htmlentities($sJson, ENT_QUOTES, 'UTF-8');
2128					$sSetInputName = "attr_{$sFormPrefix}{$sAttCode}";
2129
2130					// handle form validation
2131					$aEventsList[] = 'change';
2132					$aEventsList[] = 'validate';
2133					$sNullValue = '';
2134					$sFieldToValidateId = $sFieldToValidateId.AttributeSet::EDITABLE_INPUT_ID_SUFFIX;
2135
2136					// generate form HTML output
2137					$sValidationSpan = "<span class=\"form_validation\" id=\"v_{$sFieldToValidateId}\"></span>";
2138					$sHTMLValue = '<div class="field_input_zone field_input_set"><input id="'.$iId.'" name="'.$sSetInputName.'" type="hidden" value="'.$sEscapedJson.'"></div>'.$sValidationSpan.$sReloadSpan;
2139					$sScript = "$('#$iId').set_widget({inputWidgetIdSuffix: '".AttributeSet::EDITABLE_INPUT_ID_SUFFIX."'});";
2140					$oPage->add_ready_script($sScript);
2141
2142					break;
2143
2144				case 'String':
2145				default:
2146					$aEventsList[] = 'validate';
2147					// #@# todo - add context information (depending on dimensions)
2148					$aAllowedValues = $oAttDef->GetAllowedValues($aArgs);
2149					$iFieldSize = $oAttDef->GetMaxSize();
2150					if ($aAllowedValues !== null)
2151					{
2152						// Discrete list of values, use a SELECT or RADIO buttons depending on the config
2153						$sDisplayStyle = $oAttDef->GetDisplayStyle();
2154						switch ($sDisplayStyle)
2155						{
2156							case 'radio':
2157							case 'radio_horizontal':
2158							case 'radio_vertical':
2159								$aEventsList[] = 'change';
2160								$sHTMLValue = "<div class=\"field_input_zone field_input_{$sDisplayStyle}\">";
2161								$bVertical = ($sDisplayStyle != 'radio_horizontal');
2162								$sHTMLValue .= $oPage->GetRadioButtons($aAllowedValues, $value, $iId,
2163									"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}", $bMandatory, $bVertical, '');
2164								$sHTMLValue .= "</div>{$sValidationSpan}{$sReloadSpan}\n";
2165								break;
2166
2167							case 'select':
2168							default:
2169								$aEventsList[] = 'change';
2170								$sHTMLValue = "<div class=\"field_input_zone field_input_string\"><select title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" id=\"$iId\">\n";
2171								$sHTMLValue .= "<option value=\"\">".Dict::S('UI:SelectOne')."</option>\n";
2172								foreach($aAllowedValues as $key => $display_value)
2173								{
2174									if ((count($aAllowedValues) == 1) && ($bMandatory == 'true'))
2175									{
2176										// When there is only once choice, select it by default
2177										$sSelected = ' selected';
2178									}
2179									else
2180									{
2181										$sSelected = ($value == $key) ? ' selected' : '';
2182									}
2183									$sHTMLValue .= "<option value=\"$key\"$sSelected>$display_value</option>\n";
2184								}
2185								$sHTMLValue .= "</select></div>{$sValidationSpan}{$sReloadSpan}\n";
2186								break;
2187						}
2188					}
2189					else
2190					{
2191						$sHTMLValue = "<div class=\"field_input_zone field_input_string\"><input title=\"$sHelpText\" type=\"text\" maxlength=\"$iFieldSize\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"".htmlentities($sDisplayValue,
2192								ENT_QUOTES, 'UTF-8')."\" id=\"$iId\"/></div>{$sValidationSpan}{$sReloadSpan}";
2193						$aEventsList[] = 'keyup';
2194						$aEventsList[] = 'change';
2195
2196						// Adding tooltip so we can read the whole value when its very long (eg. URL)
2197						if (!empty($sDisplayValue))
2198						{
2199							$oPage->add_ready_script(
2200								<<<EOF
2201								var sEscapedVal = $('<div/>').text($('#{$iId}').val()).html();
2202								$('#{$iId}').qtip( { content: sEscapedVal, show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'bottomLeft' }, position: { corner: { target: 'topLeft', tooltip: 'bottomLeft' }, adjust: { y: -15}} } );
2203
2204								$('#{$iId}').bind('keyup', function(evt, sFormId){
2205									var oQTipAPI = $(this).qtip('api');
2206
2207									if($(this).val() === '')
2208									{
2209										oQTipAPI.hide();
2210										oQTipAPI.disable(true);
2211									}
2212									else
2213									{
2214										oQTipAPI.disable(false);
2215									}
2216									var sEscapedVal = $('<div/>').text($(this).val()).html();
2217									oQTipAPI.updateContent(sEscapedVal);
2218								});
2219EOF
2220							);
2221						}
2222					}
2223					break;
2224			}
2225			$sPattern = addslashes($oAttDef->GetValidationPattern()); //'^([0-9]+)$';
2226			if (!empty($aEventsList))
2227			{
2228				if (!is_numeric($sNullValue))
2229				{
2230					$sNullValue = "'$sNullValue'"; // Add quotes to turn this into a JS string if it's not a number
2231				}
2232				$sOriginalValue = ($iFlags & OPT_ATT_MUSTCHANGE) ? json_encode($value) : 'undefined';
2233				$oPage->add_ready_script("$('#$sFieldToValidateId').bind('".implode(' ',
2234						$aEventsList)."', function(evt, sFormId) { return ValidateField('$sFieldToValidateId', '$sPattern', $bMandatory, sFormId, $sNullValue, $sOriginalValue) } );\n"); // Bind to a custom event: validate
2235			}
2236			$aDependencies = MetaModel::GetDependentAttributes($sClass,
2237				$sAttCode); // List of attributes that depend on the current one
2238			if (count($aDependencies) > 0)
2239			{
2240				// Unbind first to avoid duplicate event handlers in case of reload of the whole (or part of the) form
2241				$oPage->add_ready_script("$('#$iId').unbind('change.dependencies').bind('change.dependencies', function(evt, sFormId) { return oWizardHelper{$sFormPrefix}.UpdateDependentFields(['".implode("','",
2242						$aDependencies)."']) } );\n"); // Bind to a custom event: validate
2243			}
2244		}
2245		$oPage->add_dict_entry('UI:ValueMustBeSet');
2246		$oPage->add_dict_entry('UI:ValueMustBeChanged');
2247		$oPage->add_dict_entry('UI:ValueInvalidFormat');
2248
2249		return "<div id=\"field_{$iId}\" class=\"field_value_container\"><div class=\"attribute-edit\" data-attcode=\"$sAttCode\">{$sHTMLValue}</div></div>";
2250	}
2251
2252	public function DisplayModifyForm(WebPage $oPage, $aExtraParams = array())
2253	{
2254		$sOwnershipToken = null;
2255		$iKey = $this->GetKey();
2256		$sClass = get_class($this);
2257		if ($iKey > 0)
2258		{
2259			// The concurrent access lock makes sense only for already existing objects
2260			$LockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled');
2261			if ($LockEnabled)
2262			{
2263				$sOwnershipToken = utils::ReadPostedParam('ownership_token', null, 'raw_data');
2264				if ($sOwnershipToken !== null)
2265				{
2266					// We're probably inside something like "apply_modify" where the validation failed and we must prompt the user again to edit the object
2267					// let's extend our lock
2268				}
2269				else
2270				{
2271					$aLockInfo = iTopOwnershipLock::AcquireLock($sClass, $iKey);
2272					if ($aLockInfo['success'])
2273					{
2274						$sOwnershipToken = $aLockInfo['token'];
2275					}
2276					else
2277					{
2278						// If the object is locked by the current user, it's worth trying again, since
2279						// the lock may be released by 'onunload' which is called AFTER loading the current page.
2280						//$bTryAgain = $oOwner->GetKey() == UserRights::GetUserId();
2281						self::ReloadAndDisplay($oPage, $this, array('operation' => 'modify'));
2282
2283						return;
2284					}
2285				}
2286			}
2287		}
2288
2289		if (isset($aExtraParams['wizard_container']) && $aExtraParams['wizard_container'])
2290		{
2291			$sClassLabel = MetaModel::GetName($sClass);
2292			$oPage->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $this->GetRawName(),
2293				$sClassLabel)); // Set title will take care of the encoding
2294			$oPage->add("<div class=\"page_header\">\n");
2295			$oPage->add("<h1>".$this->GetIcon()."&nbsp;".Dict::Format('UI:ModificationTitle_Class_Object', $sClassLabel,
2296					$this->GetName())."</h1>\n");
2297			$oPage->add("</div>\n");
2298			$oPage->add("<div class=\"wizContainer\">\n");
2299		}
2300		self::$iGlobalFormId++;
2301		$this->aFieldsMap = array();
2302		$sPrefix = '';
2303		if (isset($aExtraParams['formPrefix']))
2304		{
2305			$sPrefix = $aExtraParams['formPrefix'];
2306		}
2307
2308		$this->m_iFormId = $sPrefix.self::$iGlobalFormId;
2309		$oAppContext = new ApplicationContext();
2310		if (!isset($aExtraParams['action']))
2311		{
2312			$sFormAction = utils::GetAbsoluteUrlAppRoot().'pages/'.$this->GetUIPage(); // No parameter in the URL, the only parameter will be the ones passed through the form
2313		}
2314		else
2315		{
2316			$sFormAction = $aExtraParams['action'];
2317		}
2318		// Custom label for the apply button ?
2319		if (isset($aExtraParams['custom_button']))
2320		{
2321			$sApplyButton = $aExtraParams['custom_button'];
2322		}
2323		else
2324		{
2325			if ($iKey > 0)
2326			{
2327				$sApplyButton = Dict::S('UI:Button:Apply');
2328			}
2329			else
2330			{
2331				$sApplyButton = Dict::S('UI:Button:Create');
2332			}
2333		}
2334		// Custom operation for the form ?
2335		if (isset($aExtraParams['custom_operation']))
2336		{
2337			$sOperation = $aExtraParams['custom_operation'];
2338		}
2339		else
2340		{
2341			if ($iKey > 0)
2342			{
2343				$sOperation = 'apply_modify';
2344			}
2345			else
2346			{
2347				$sOperation = 'apply_new';
2348			}
2349		}
2350		if ($iKey > 0)
2351		{
2352			// The object already exists in the database, it's a modification
2353			$sButtons = "<input id=\"{$sPrefix}_id\" type=\"hidden\" name=\"id\" value=\"$iKey\">\n";
2354			$sButtons .= "<input type=\"hidden\" name=\"operation\" value=\"{$sOperation}\">\n";
2355			$sButtons .= "<button type=\"button\" class=\"action cancel\"><span>".Dict::S('UI:Button:Cancel')."</span></button>&nbsp;&nbsp;&nbsp;&nbsp;\n";
2356			$sButtons .= "<button type=\"submit\" class=\"action\"><span>{$sApplyButton}</span></button>\n";
2357		}
2358		else
2359		{
2360			// The object does not exist in the database it's a creation
2361			$sButtons = "<input type=\"hidden\" name=\"operation\" value=\"$sOperation\">\n";
2362			$sButtons .= "<button type=\"button\" class=\"action cancel\">".Dict::S('UI:Button:Cancel')."</button>&nbsp;&nbsp;&nbsp;&nbsp;\n";
2363			$sButtons .= "<button type=\"submit\" class=\"action\"><span>{$sApplyButton}</span></button>\n";
2364		}
2365
2366		$aTransitions = $this->EnumTransitions();
2367		if (!isset($aExtraParams['custom_operation']) && count($aTransitions))
2368		{
2369			// transitions are displayed only for the standard new/modify actions, not for modify_all or any other case...
2370			$oSetToCheckRights = DBObjectSet::FromObject($this);
2371			$aStimuli = Metamodel::EnumStimuli($sClass);
2372			foreach($aTransitions as $sStimulusCode => $aTransitionDef)
2373			{
2374				$iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? UserRights::IsStimulusAllowed($sClass,
2375					$sStimulusCode, $oSetToCheckRights) : UR_ALLOWED_NO;
2376				switch ($iActionAllowed)
2377				{
2378					case UR_ALLOWED_YES:
2379						$sButtons .= "<button type=\"submit\" name=\"next_action\" value=\"{$sStimulusCode}\" class=\"action\"><span>".$aStimuli[$sStimulusCode]->GetLabel()."</span></button>\n";
2380						break;
2381
2382					default:
2383						// Do nothing
2384				}
2385			}
2386		}
2387
2388		$sButtonsPosition = MetaModel::GetConfig()->Get('buttons_position');
2389		$iTransactionId = isset($aExtraParams['transaction_id']) ? $aExtraParams['transaction_id'] : utils::GetNewTransactionId();
2390		$oPage->SetTransactionId($iTransactionId);
2391		$oPage->add("<form action=\"$sFormAction\" id=\"form_{$this->m_iFormId}\" enctype=\"multipart/form-data\" method=\"post\" onSubmit=\"return OnSubmit('form_{$this->m_iFormId}');\">\n");
2392		$sStatesSelection = '';
2393		if (!isset($aExtraParams['custom_operation']) && $this->IsNew())
2394		{
2395			$aInitialStates = MetaModel::EnumInitialStates($sClass);
2396			//$aInitialStates = array('new' => 'foo', 'closed' => 'bar');
2397			if (count($aInitialStates) > 1)
2398			{
2399				$sStatesSelection = Dict::Format('UI:Create_Class_InState',
2400						MetaModel::GetName($sClass)).'<select name="obj_state" class="state_select_'.$this->m_iFormId.'">';
2401				foreach($aInitialStates as $sStateCode => $sStateData)
2402				{
2403					$sSelected = '';
2404					if ($sStateCode == $this->GetState())
2405					{
2406						$sSelected = ' selected';
2407					}
2408					$sStatesSelection .= '<option value="'.$sStateCode.'" '.$sSelected.'>'.MetaModel::GetStateLabel($sClass,
2409							$sStateCode).'</option>';
2410				}
2411				$sStatesSelection .= '</select>';
2412				$oPage->add_ready_script("$('.state_select_{$this->m_iFormId}').change( function() { oWizardHelper$sPrefix.ReloadObjectCreationForm('form_{$this->m_iFormId}', $(this).val()); } );");
2413			}
2414		}
2415
2416		$sConfirmationMessage = addslashes(Dict::S('UI:NavigateAwayConfirmationMessage'));
2417		$sJSToken = json_encode($sOwnershipToken);
2418		$oPage->add_ready_script(
2419			<<<EOF
2420	$(window).on('unload',function() { return OnUnload('$iTransactionId', '$sClass', $iKey, $sJSToken) } );
2421	window.onbeforeunload = function() {
2422		if (!window.bInSubmit && !window.bInCancel)
2423		{
2424			return '$sConfirmationMessage';
2425		}
2426		// return nothing ! safer for IE
2427	};
2428EOF
2429		);
2430
2431		if ($sButtonsPosition != 'bottom')
2432		{
2433			// top or both, display the buttons here
2434			$oPage->p($sStatesSelection);
2435			$oPage->add($sButtons);
2436		}
2437
2438		$oPage->AddTabContainer(OBJECT_PROPERTIES_TAB, $sPrefix);
2439		$oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB);
2440		$oPage->SetCurrentTab(Dict::S('UI:PropertiesTab'));
2441
2442		$aFieldsMap = $this->DisplayBareProperties($oPage, true, $sPrefix, $aExtraParams);
2443		if (!is_array($aFieldsMap))
2444		{
2445			$aFieldsMap = array();
2446		}
2447		if ($iKey > 0)
2448		{
2449			$aFieldsMap['id'] = $sPrefix.'_id';
2450		}
2451		// Now display the relations, one tab per relation
2452		if (!isset($aExtraParams['noRelations']))
2453		{
2454			$this->DisplayBareRelations($oPage, true); // Edit mode, will fill $this->aFieldsMap
2455			$aFieldsMap = array_merge($aFieldsMap, $this->aFieldsMap);
2456		}
2457
2458		$oPage->SetCurrentTab('');
2459		$oPage->add("<input type=\"hidden\" name=\"class\" value=\"$sClass\">\n");
2460		$oPage->add("<input type=\"hidden\" name=\"transaction_id\" value=\"$iTransactionId\">\n");
2461		foreach($aExtraParams as $sName => $value)
2462		{
2463			if (is_scalar($value))
2464			{
2465				$oPage->add("<input type=\"hidden\" name=\"$sName\" value=\"$value\">\n");
2466			}
2467		}
2468		if ($sOwnershipToken !== null)
2469		{
2470			$oPage->add("<input type=\"hidden\" name=\"ownership_token\" value=\"".htmlentities($sOwnershipToken,
2471					ENT_QUOTES, 'UTF-8')."\">\n");
2472		}
2473		$oPage->add($oAppContext->GetForForm());
2474		if ($sButtonsPosition != 'top')
2475		{
2476			// bottom or both: display the buttons here
2477			$oPage->p($sStatesSelection);
2478			$oPage->add($sButtons);
2479		}
2480
2481		// Hook the cancel button via jQuery so that it can be unhooked easily as well if needed
2482		$sDefaultUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=cancel&'.$oAppContext->GetForLink();
2483		$oPage->add_ready_script("$('#form_{$this->m_iFormId} button.cancel').click( function() { BackToDetails('$sClass', $iKey, '$sDefaultUrl', $sJSToken)} );");
2484		$oPage->add("</form>\n");
2485
2486		if (isset($aExtraParams['wizard_container']) && $aExtraParams['wizard_container'])
2487		{
2488			$oPage->add("</div>\n");
2489		}
2490
2491		$iFieldsCount = count($aFieldsMap);
2492		$sJsonFieldsMap = json_encode($aFieldsMap);
2493		$sState = $this->GetState();
2494		$sSessionStorageKey = $sClass.'_'.$iKey;
2495		$sTempId = utils::GetUploadTempId($iTransactionId);
2496		$oPage->add_ready_script(InlineImage::EnableCKEditorImageUpload($this, $sTempId));
2497
2498		$oPage->add_script(
2499			<<<EOF
2500		sessionStorage.removeItem('$sSessionStorageKey');
2501
2502		// Create the object once at the beginning of the page...
2503		var oWizardHelper$sPrefix = new WizardHelper('$sClass', '$sPrefix', '$sState');
2504		oWizardHelper$sPrefix.SetFieldsMap($sJsonFieldsMap);
2505		oWizardHelper$sPrefix.SetFieldsCount($iFieldsCount);
2506EOF
2507		);
2508		$oPage->add_ready_script(
2509			<<<EOF
2510		oWizardHelper$sPrefix.UpdateWizard();
2511		// Starts the validation when the page is ready
2512		CheckFields('form_{$this->m_iFormId}', false);
2513
2514EOF
2515		);
2516		if ($sOwnershipToken !== null)
2517		{
2518			$this->GetOwnershipJSHandler($oPage, $sOwnershipToken);
2519		}
2520		else
2521		{
2522			// Probably a new object (or no concurrent lock), let's add a watchdog so that the session is kept open while editing
2523			$iInterval = MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay') * 1000 / 2;
2524			if ($iInterval > 0)
2525			{
2526				$iInterval = max(MIN_WATCHDOG_INTERVAL * 1000,
2527					$iInterval); // Minimum interval for the watchdog is MIN_WATCHDOG_INTERVAL
2528				$oPage->add_ready_script(
2529					<<<EOF
2530				window.setInterval(function() {
2531					$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'watchdog'});
2532				}, $iInterval);
2533EOF
2534				);
2535			}
2536		}
2537	}
2538
2539	public static function DisplayCreationForm(WebPage $oPage, $sClass, $oObjectToClone = null, $aArgs = array(), $aExtraParams = array())
2540	{
2541		$sClass = ($oObjectToClone == null) ? $sClass : get_class($oObjectToClone);
2542
2543		if ($oObjectToClone == null)
2544		{
2545			$oObj = DBObject::MakeDefaultInstance($sClass);
2546		}
2547		else
2548		{
2549			$oObj = clone $oObjectToClone;
2550		}
2551
2552		// Pre-fill the object with default values, when there is only on possible choice
2553		// AND the field is mandatory (otherwise there is always the possiblity to let it empty)
2554		$aArgs['this'] = $oObj;
2555		$aDetailsList = self::FLattenZList(MetaModel::GetZListItems($sClass, 'details'));
2556		// Order the fields based on their dependencies
2557		$aDeps = array();
2558		foreach($aDetailsList as $sAttCode)
2559		{
2560			$aDeps[$sAttCode] = MetaModel::GetPrerequisiteAttributes($sClass, $sAttCode);
2561		}
2562		$aList = self::OrderDependentFields($aDeps);
2563
2564		// Now fill-in the fields with default/supplied values
2565		foreach($aList as $sAttCode)
2566		{
2567			if (isset($aArgs['default'][$sAttCode]))
2568			{
2569				$oObj->Set($sAttCode, $aArgs['default'][$sAttCode]);
2570			}
2571			else
2572			{
2573				$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
2574
2575				// If the field is mandatory, set it to the only possible value
2576				$iFlags = $oObj->GetInitialStateAttributeFlags($sAttCode);
2577				if ((!$oAttDef->IsNullAllowed()) || ($iFlags & OPT_ATT_MANDATORY))
2578				{
2579					if ($oAttDef->IsExternalKey())
2580					{
2581						/** @var DBObjectSet $oAllowedValues */
2582						$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs);
2583						if ($oAllowedValues->CountWithLimit(2) == 1)
2584						{
2585							$oRemoteObj = $oAllowedValues->Fetch();
2586							$oObj->Set($sAttCode, $oRemoteObj->GetKey());
2587						}
2588					}
2589					else
2590					{
2591						$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
2592						if (is_array($aAllowedValues) && (count($aAllowedValues) == 1))
2593						{
2594							$aValues = array_keys($aAllowedValues);
2595							$oObj->Set($sAttCode, $aValues[0]);
2596						}
2597					}
2598				}
2599			}
2600		}
2601
2602		return $oObj->DisplayModifyForm($oPage, $aExtraParams);
2603	}
2604
2605	public function DisplayStimulusForm(WebPage $oPage, $sStimulus, $aPrefillFormParam = null)
2606	{
2607		$sClass = get_class($this);
2608		$iKey = $this->GetKey();
2609		$aTransitions = $this->EnumTransitions();
2610		$aStimuli = MetaModel::EnumStimuli($sClass);
2611		if (!isset($aTransitions[$sStimulus]))
2612		{
2613			// Invalid stimulus
2614			throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sStimulus,
2615				$this->GetName(), $this->GetStateLabel()));
2616		}
2617		// Check for concurrent access lock
2618		$LockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled');
2619		$sOwnershipToken = null;
2620		if ($LockEnabled)
2621		{
2622			$aLockInfo = iTopOwnershipLock::AcquireLock($sClass, $iKey);
2623			if ($aLockInfo['success'])
2624			{
2625				$sOwnershipToken = $aLockInfo['token'];
2626			}
2627			else
2628			{
2629				// If the object is locked by the current user, it's worth trying again, since
2630				// the lock may be released by 'onunload' which is called AFTER loading the current page.
2631				//$bTryAgain = $oOwner->GetKey() == UserRights::GetUserId();
2632				self::ReloadAndDisplay($oPage, $this, array('operation' => 'stimulus', 'stimulus' => $sStimulus));
2633
2634				return;
2635			}
2636		}
2637		$sActionLabel = $aStimuli[$sStimulus]->GetLabel();
2638		$sActionDetails = $aStimuli[$sStimulus]->GetDescription();
2639		$oPage->add("<div class=\"page_header\">\n");
2640		$oPage->add("<h1>$sActionLabel - <span class=\"hilite\">{$this->GetName()}</span></h1>\n");
2641		$oPage->set_title($sActionLabel);
2642		$oPage->add("</div>\n");
2643		$oPage->add("<h1>$sActionDetails</h1>\n");
2644		$sTargetState = $aTransitions[$sStimulus]['target_state'];
2645		$aExpectedAttributes = $this->GetTransitionAttributes($sStimulus /*, current state*/);
2646		if ($aPrefillFormParam != null)
2647		{
2648			$aPrefillFormParam['expected_attributes'] = $aExpectedAttributes;
2649			$this->PrefillForm('state_change', $aPrefillFormParam);
2650			$aExpectedAttributes = $aPrefillFormParam['expected_attributes'];
2651		}
2652		$sButtonsPosition = MetaModel::GetConfig()->Get('buttons_position');
2653		if ($sButtonsPosition == 'bottom')
2654		{
2655			// bottom: Displays the ticket details BEFORE the actions
2656			$oPage->add('<div class="ui-widget-content">');
2657			$this->DisplayBareProperties($oPage);
2658			$oPage->add('</div>');
2659		}
2660		$oPage->add("<div class=\"wizContainer\">\n");
2661		$oPage->add("<form id=\"apply_stimulus\" method=\"post\" enctype=\"multipart/form-data\" onSubmit=\"return OnSubmit('apply_stimulus');\">\n");
2662		$aDetails = array();
2663		$iFieldIndex = 0;
2664		$aFieldsMap = array();
2665
2666		// The list of candidate fields is made of the ordered list of "details" attributes + other attributes
2667		$aAttributes = array();
2668		foreach($this->FlattenZList(MetaModel::GetZListItems($sClass, 'details')) as $sAttCode)
2669		{
2670			$aAttributes[$sAttCode] = true;
2671		}
2672		foreach(MetaModel::GetAttributesList($sClass) as $sAttCode)
2673		{
2674			if (!array_key_exists($sAttCode, $aAttributes))
2675			{
2676				$aAttributes[$sAttCode] = true;
2677			}
2678		}
2679		// Order the fields based on their dependencies, set the fields for which there is only one possible value
2680		// and perform this in the order of dependencies to avoid dead-ends
2681		$aDeps = array();
2682		foreach($aAttributes as $sAttCode => $trash)
2683		{
2684			$aDeps[$sAttCode] = MetaModel::GetPrerequisiteAttributes($sClass, $sAttCode);
2685		}
2686		$aList = $this->OrderDependentFields($aDeps);
2687
2688		foreach($aList as $sAttCode)
2689		{
2690			// Consider only the "expected" fields for the target state
2691			if (array_key_exists($sAttCode, $aExpectedAttributes))
2692			{
2693				$iExpectCode = $aExpectedAttributes[$sAttCode];
2694				// Prompt for an attribute if
2695				// - the attribute must be changed or must be displayed to the user for confirmation
2696				// - or the field is mandatory and currently empty
2697				if (($iExpectCode & (OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) ||
2698					(($iExpectCode & OPT_ATT_MANDATORY) && ($this->Get($sAttCode) == '')))
2699				{
2700					$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
2701					$aArgs = array('this' => $this);
2702					// If the field is mandatory, set it to the only possible value
2703					if ((!$oAttDef->IsNullAllowed()) || ($iExpectCode & OPT_ATT_MANDATORY))
2704					{
2705						if ($oAttDef->IsExternalKey())
2706						{
2707							/** @var DBObjectSet $oAllowedValues */
2708							$oAllowedValues = MetaModel::GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs, '',
2709								$this->Get($sAttCode));
2710							if ($oAllowedValues->CountWithLimit(2) == 1)
2711							{
2712								$oRemoteObj = $oAllowedValues->Fetch();
2713								$this->Set($sAttCode, $oRemoteObj->GetKey());
2714							}
2715						}
2716						else
2717						{
2718							$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
2719							if (is_array($aAllowedValues) && count($aAllowedValues) == 1)
2720							{
2721								$aValues = array_keys($aAllowedValues);
2722								$this->Set($sAttCode, $aValues[0]);
2723							}
2724						}
2725					}
2726					$sHTMLValue = cmdbAbstractObject::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef,
2727						$this->Get($sAttCode), $this->GetEditValue($sAttCode), 'att_'.$iFieldIndex, '', $iExpectCode,
2728						$aArgs);
2729					$aDetails[] = array(
2730						'label' => '<span>'.$oAttDef->GetLabel().'</span>',
2731						'value' => "<span id=\"field_att_$iFieldIndex\">$sHTMLValue</span>",
2732					);
2733					$aFieldsMap[$sAttCode] = 'att_'.$iFieldIndex;
2734					$iFieldIndex++;
2735				}
2736			}
2737		}
2738
2739		$oPage->add('<table><tr><td>');
2740		$oPage->details($aDetails);
2741		$oPage->add('</td></tr></table>');
2742		$oPage->add("<input type=\"hidden\" name=\"id\" value=\"".$this->GetKey()."\" id=\"id\">\n");
2743		$aFieldsMap['id'] = 'id';
2744		$oPage->add("<input type=\"hidden\" name=\"class\" value=\"$sClass\">\n");
2745		$oPage->add("<input type=\"hidden\" name=\"operation\" value=\"apply_stimulus\">\n");
2746		$oPage->add("<input type=\"hidden\" name=\"stimulus\" value=\"$sStimulus\">\n");
2747		$iTransactionId = utils::GetNewTransactionId();
2748		$oPage->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".$iTransactionId."\">\n");
2749		if ($sOwnershipToken !== null)
2750		{
2751			$oPage->add("<input type=\"hidden\" name=\"ownership_token\" value=\"".htmlentities($sOwnershipToken,
2752					ENT_QUOTES, 'UTF-8')."\">\n");
2753		}
2754		$oAppContext = new ApplicationContext();
2755		$oPage->add($oAppContext->GetForForm());
2756		$oPage->add("<button type=\"button\" class=\"action cancel\" onClick=\"BackToDetails('$sClass', ".$this->GetKey().", '', '$sOwnershipToken')\"><span>".Dict::S('UI:Button:Cancel')."</span></button>&nbsp;&nbsp;&nbsp;&nbsp;\n");
2757		$oPage->add("<button type=\"submit\" class=\"action\"><span>$sActionLabel</span></button>\n");
2758		$oPage->add("</form>\n");
2759		$oPage->add("</div>\n");
2760		if ($sButtonsPosition != 'top')
2761		{
2762			// bottom or both: Displays the ticket details AFTER the actions
2763			$oPage->add('<div class="ui-widget-content">');
2764			$this->DisplayBareProperties($oPage);
2765			$oPage->add('</div>');
2766		}
2767
2768		$iFieldsCount = count($aFieldsMap);
2769		$sJsonFieldsMap = json_encode($aFieldsMap);
2770
2771		$oPage->add_script(
2772			<<<EOF
2773		// Initializes the object once at the beginning of the page...
2774		var oWizardHelper = new WizardHelper('$sClass', '', '$sTargetState', '{$this->GetState()}', '$sStimulus');
2775		oWizardHelper.SetFieldsMap($sJsonFieldsMap);
2776		oWizardHelper.SetFieldsCount($iFieldsCount);
2777EOF
2778		);
2779		$sJSToken = json_encode($sOwnershipToken);
2780		$oPage->add_ready_script(
2781			<<<EOF
2782		// Starts the validation when the page is ready
2783		CheckFields('apply_stimulus', false);
2784		$(window).on('unload', function() { return OnUnload('$iTransactionId', '$sClass', $iKey, $sJSToken) } );
2785EOF
2786		);
2787
2788		if ($sOwnershipToken !== null)
2789		{
2790			$this->GetOwnershipJSHandler($oPage, $sOwnershipToken);
2791		}
2792
2793		// Note: This part (inline images activation) is duplicated in self::DisplayModifyForm and several other places. Maybe it should be refactored so it automatically activates when an HTML field is present, or be an option of the attribute. See bug n°1240.
2794		$sTempId = utils::GetUploadTempId($iTransactionId);
2795		$oPage->add_ready_script(InlineImage::EnableCKEditorImageUpload($this, $sTempId));
2796	}
2797
2798	public static function ProcessZlist($aList, $aDetails, $sCurrentTab, $sCurrentCol, $sCurrentSet)
2799	{
2800		$index = 0;
2801		foreach($aList as $sKey => $value)
2802		{
2803			if (is_array($value))
2804			{
2805				if (preg_match('/^(.*):(.*)$/U', $sKey, $aMatches))
2806				{
2807					$sCode = $aMatches[1];
2808					$sName = $aMatches[2];
2809					switch ($sCode)
2810					{
2811						case 'tab':
2812							if (!isset($aDetails[$sName]))
2813							{
2814								$aDetails[$sName] = array('col1' => array());
2815							}
2816							$aDetails = self::ProcessZlist($value, $aDetails, $sName, 'col1', '');
2817							break;
2818
2819						case 'fieldset':
2820							if (!isset($aDetailsStruct[$sCurrentTab][$sCurrentCol][$sName]))
2821							{
2822								$aDetails[$sCurrentTab][$sCurrentCol][$sName] = array();
2823							}
2824							$aDetails = self::ProcessZlist($value, $aDetails, $sCurrentTab, $sCurrentCol, $sName);
2825							break;
2826
2827						default:
2828						case 'col':
2829							if (!isset($aDetails[$sCurrentTab][$sName]))
2830							{
2831								$aDetails[$sCurrentTab][$sName] = array();
2832							}
2833							$aDetails = self::ProcessZlist($value, $aDetails, $sCurrentTab, $sName, '');
2834							break;
2835					}
2836				}
2837			}
2838			else
2839			{
2840				if (empty($sCurrentSet))
2841				{
2842					$aDetails[$sCurrentTab][$sCurrentCol]['_'.$index][] = $value;
2843				}
2844				else
2845				{
2846					$aDetails[$sCurrentTab][$sCurrentCol][$sCurrentSet][] = $value;
2847				}
2848			}
2849			$index++;
2850		}
2851
2852		return $aDetails;
2853	}
2854
2855	static function FlattenZList($aList)
2856	{
2857		$aResult = array();
2858		foreach($aList as $value)
2859		{
2860			if (!is_array($value))
2861			{
2862				$aResult[] = $value;
2863			}
2864			else
2865			{
2866				$aResult = array_merge($aResult, self::FlattenZList($value));
2867			}
2868		}
2869
2870		return $aResult;
2871	}
2872
2873	protected function GetFieldAsHtml($sClass, $sAttCode, $sStateAttCode)
2874	{
2875		$retVal = null;
2876		if ($this->IsNew())
2877		{
2878			$iFlags = $this->GetInitialStateAttributeFlags($sAttCode);
2879		}
2880		else
2881		{
2882			$iFlags = $this->GetAttributeFlags($sAttCode);
2883		}
2884		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
2885		if ((!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0) && !($oAttDef instanceof AttributeDashboard))
2886		{
2887			// The field is visible in the current state of the object
2888			if ($sStateAttCode == $sAttCode)
2889			{
2890				// Special display for the 'state' attribute itself
2891				$sDisplayValue = $this->GetStateLabel();
2892			}
2893			else
2894			{
2895				if ($oAttDef->GetEditClass() == 'Document')
2896				{
2897					$oDocument = $this->Get($sAttCode);
2898					$sDisplayValue = $this->GetAsHTML($sAttCode);
2899					$sDisplayValue .= "<br/>".Dict::Format('UI:OpenDocumentInNewWindow_',
2900							$oDocument->GetDisplayLink(get_class($this), $this->GetKey(), $sAttCode)).", \n";
2901					$sDisplayValue .= "<br/>".Dict::Format('UI:DownloadDocument_',
2902							$oDocument->GetDownloadLink(get_class($this), $this->GetKey(), $sAttCode)).", \n";
2903				}
2904				elseif ($oAttDef instanceof AttributeDashboard)
2905				{
2906					$sDisplayValue = '';
2907				}
2908				else
2909				{
2910					$sDisplayValue = $this->GetAsHTML($sAttCode);
2911				}
2912			}
2913			$retVal = array(
2914				'label' => '<span title="'.MetaModel::GetDescription($sClass,
2915						$sAttCode).'">'.MetaModel::GetLabel($sClass, $sAttCode).'</span>',
2916				'value' => $sDisplayValue,
2917				'attcode' => $sAttCode,
2918			);
2919
2920			// Checking how the field should be rendered
2921			// Note: For edit mode, this is done in self::GetBareProperties()
2922			// Note 2: Shouldn't this be a AttDef property instead of an array to maintain?
2923			if (in_array($oAttDef->GetEditClass(), array('Text', 'HTML', 'CaseLog', 'CustomFields', 'OQLExpression')))
2924			{
2925				$retVal['layout'] = 'large';
2926			}
2927			else
2928			{
2929				$retVal['layout'] = 'small';
2930			}
2931		}
2932
2933		return $retVal;
2934	}
2935
2936	/**
2937	 * Displays a blob document *inline* (if possible, depending on the type of the document)
2938	 *
2939	 * @param \WebPage $oPage
2940	 * @param $sAttCode
2941	 *
2942	 * @return string
2943	 * @throws \CoreException
2944	 */
2945	public function DisplayDocumentInline(WebPage $oPage, $sAttCode)
2946	{
2947		$oDoc = $this->Get($sAttCode);
2948		$sClass = get_class($this);
2949		$Id = $this->GetKey();
2950		switch ($oDoc->GetMainMimeType())
2951		{
2952			case 'text':
2953			case 'html':
2954				$data = $oDoc->GetData();
2955				switch ($oDoc->GetMimeType())
2956				{
2957					case 'text/html':
2958					case 'text/xml':
2959						$oPage->add("<iframe id='preview_$sAttCode' src=\"".utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=display_document&class=$sClass&id=$Id&field=$sAttCode\" width=\"100%\" height=\"400\">Loading...</iframe>\n");
2960						break;
2961
2962					default:
2963						$oPage->add("<pre>".htmlentities(MyHelpers::beautifulstr($data, 1000, true), ENT_QUOTES,
2964								'UTF-8')."</pre>\n");
2965				}
2966				break;
2967
2968			case 'application':
2969				switch ($oDoc->GetMimeType())
2970				{
2971					case 'application/pdf':
2972						$oPage->add("<iframe id='preview_$sAttCode' src=\"".utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=display_document&class=$sClass&id=$Id&field=$sAttCode\" width=\"100%\" height=\"400\">Loading...</iframe>\n");
2973						break;
2974
2975					default:
2976						$oPage->add(Dict::S('UI:Document:NoPreview'));
2977				}
2978				break;
2979
2980			case 'image':
2981				$oPage->add("<img src=\"".utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=display_document&class=$sClass&id=$Id&field=$sAttCode\" />\n");
2982				break;
2983
2984			default:
2985				$oPage->add(Dict::S('UI:Document:NoPreview'));
2986		}
2987		return '';
2988	}
2989
2990	// $m_highlightComparison[previous][new] => next value
2991	protected static $m_highlightComparison = array(
2992		HILIGHT_CLASS_CRITICAL => array(
2993			HILIGHT_CLASS_CRITICAL => HILIGHT_CLASS_CRITICAL,
2994			HILIGHT_CLASS_WARNING => HILIGHT_CLASS_CRITICAL,
2995			HILIGHT_CLASS_OK => HILIGHT_CLASS_CRITICAL,
2996			HILIGHT_CLASS_NONE => HILIGHT_CLASS_CRITICAL,
2997		),
2998		HILIGHT_CLASS_WARNING => array(
2999			HILIGHT_CLASS_CRITICAL => HILIGHT_CLASS_CRITICAL,
3000			HILIGHT_CLASS_WARNING => HILIGHT_CLASS_WARNING,
3001			HILIGHT_CLASS_OK => HILIGHT_CLASS_WARNING,
3002			HILIGHT_CLASS_NONE => HILIGHT_CLASS_WARNING,
3003		),
3004		HILIGHT_CLASS_OK => array(
3005			HILIGHT_CLASS_CRITICAL => HILIGHT_CLASS_CRITICAL,
3006			HILIGHT_CLASS_WARNING => HILIGHT_CLASS_WARNING,
3007			HILIGHT_CLASS_OK => HILIGHT_CLASS_OK,
3008			HILIGHT_CLASS_NONE => HILIGHT_CLASS_OK,
3009		),
3010		HILIGHT_CLASS_NONE => array(
3011			HILIGHT_CLASS_CRITICAL => HILIGHT_CLASS_CRITICAL,
3012			HILIGHT_CLASS_WARNING => HILIGHT_CLASS_WARNING,
3013			HILIGHT_CLASS_OK => HILIGHT_CLASS_OK,
3014			HILIGHT_CLASS_NONE => HILIGHT_CLASS_NONE,
3015		),
3016	);
3017
3018	/**
3019	 * This function returns a 'hilight' CSS class, used to hilight a given row in a table
3020	 * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL,
3021	 * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE
3022	 * To Be overridden by derived classes
3023	 *
3024	 * @param void
3025	 *
3026	 * @return String The desired higlight class for the object/row
3027	 */
3028	public function GetHilightClass()
3029	{
3030		// Possible return values are:
3031		// HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE
3032		$current = parent::GetHilightClass(); // Default computation
3033
3034		// Invoke extensions before the deletion (the deletion will do some cleanup and we might loose some information
3035		foreach(MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
3036		{
3037			$new = $oExtensionInstance->GetHilightClass($this);
3038			@$current = self::$m_highlightComparison[$current][$new];
3039		}
3040
3041		return $current;
3042	}
3043
3044	/**
3045	 * Re-order the fields based on their inter-dependencies
3046	 *
3047	 * @params hash @aFields field_code => array_of_depencies
3048	 *
3049	 * @param $aFields
3050	 * @return array Ordered array of fields or throws an exception
3051	 * @throws \Exception
3052	 */
3053	public static function OrderDependentFields($aFields)
3054	{
3055		$aResult = array();
3056		$iCount = 0;
3057		do
3058		{
3059			$bSet = false;
3060			$iCount++;
3061			foreach($aFields as $sFieldCode => $aDeps)
3062			{
3063				foreach($aDeps as $key => $sDependency)
3064				{
3065					if (in_array($sDependency, $aResult))
3066					{
3067						// Dependency is resolved, remove it
3068						unset($aFields[$sFieldCode][$key]);
3069					}
3070					else
3071					{
3072						if (!array_key_exists($sDependency, $aFields))
3073						{
3074							// The current fields depends on a field not present in the form
3075							// let's ignore it (since it cannot change)
3076							unset($aFields[$sFieldCode][$key]);
3077						}
3078					}
3079				}
3080				if (count($aFields[$sFieldCode]) == 0)
3081				{
3082					// No more pending depencies for this field, add it to the list
3083					$aResult[] = $sFieldCode;
3084					unset($aFields[$sFieldCode]);
3085					$bSet = true;
3086				}
3087			}
3088		} while ($bSet && (count($aFields) > 0));
3089
3090		if (count($aFields) > 0)
3091		{
3092			$sMessage = "Error: Circular dependencies between the fields! <pre>".print_r($aFields, true)."</pre>";
3093			throw(new Exception($sMessage));
3094		}
3095
3096		return $aResult;
3097	}
3098
3099	/**
3100	 * Get the list of actions to be displayed as 'shortcuts' (i.e buttons) instead of inside the Actions popup menu
3101	 *
3102	 * @param $sFinalClass string The actual class of the objects for which to display the menu
3103	 *
3104	 * @return array the list of menu codes (i.e dictionary entries) that can be displayed as shortcuts next to the
3105	 *     actions menu
3106	 */
3107	public static function GetShortcutActions($sFinalClass)
3108	{
3109		$sShortcutActions = MetaModel::GetConfig()->Get('shortcut_actions');
3110		$aShortcutActions = explode(',', $sShortcutActions);
3111
3112		return $aShortcutActions;
3113	}
3114
3115	/**
3116	 * Maps the given context parameter name to the appropriate filter/search code for this class
3117	 *
3118	 * @param string $sContextParam Name of the context parameter, i.e. 'org_id'
3119	 *
3120	 * @return string Filter code, i.e. 'customer_id'
3121	 */
3122	public static function MapContextParam($sContextParam)
3123	{
3124		if ($sContextParam == 'menu')
3125		{
3126			return null;
3127		}
3128		else
3129		{
3130			return $sContextParam;
3131		}
3132	}
3133
3134	/**
3135	 * Updates the object from a flat array of values
3136	 *
3137	 * @param $aAttList array $aAttList array of attcode
3138	 * @param $aErrors array Returns information about slave attributes
3139	 * @param $aAttFlags array Attribute codes => Flags to use instead of those from the MetaModel
3140	 *
3141	 * @return array of attcodes that can be used for writing on the current object
3142	 * @throws \CoreException
3143	 */
3144	public function GetWriteableAttList($aAttList, &$aErrors, $aAttFlags = array())
3145	{
3146		if (!is_array($aAttList))
3147		{
3148			$aAttList = $this->FlattenZList(MetaModel::GetZListItems(get_class($this), 'details'));
3149			// Special case to process the case log, if any...
3150			// WARNING: if you change this also check the functions DisplayModifyForm and DisplayCaseLog
3151			foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
3152			{
3153
3154				if (array_key_exists($sAttCode, $aAttFlags))
3155				{
3156					$iFlags = $aAttFlags[$sAttCode];
3157				}
3158				elseif ($this->IsNew())
3159				{
3160					$iFlags = $this->GetInitialStateAttributeFlags($sAttCode);
3161				}
3162				else
3163				{
3164					$aVoid = array();
3165					$iFlags = $this->GetAttributeFlags($sAttCode, $aVoid);
3166				}
3167				if ($oAttDef instanceof AttributeCaseLog)
3168				{
3169					if (!($iFlags & (OPT_ATT_HIDDEN | OPT_ATT_SLAVE | OPT_ATT_READONLY)))
3170					{
3171						// The case log is editable, append it to the list of fields to retrieve
3172						$aAttList[] = $sAttCode;
3173					}
3174				}
3175			}
3176		}
3177		$aWriteableAttList = array();
3178		foreach($aAttList as $sAttCode)
3179		{
3180			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
3181
3182			if (array_key_exists($sAttCode, $aAttFlags))
3183			{
3184				$iFlags = $aAttFlags[$sAttCode];
3185			}
3186			elseif ($this->IsNew())
3187			{
3188				$iFlags = $this->GetInitialStateAttributeFlags($sAttCode);
3189			}
3190			else
3191			{
3192				$aVoid = array();
3193				$iFlags = $this->GetAttributeFlags($sAttCode, $aVoid);
3194			}
3195			if ($oAttDef->IsWritable())
3196			{
3197				if ($iFlags & (OPT_ATT_HIDDEN | OPT_ATT_READONLY))
3198				{
3199					// Non-visible, or read-only attribute, do nothing
3200				}
3201				elseif ($iFlags & OPT_ATT_SLAVE)
3202				{
3203					$aErrors[$sAttCode] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $oAttDef->GetLabel());
3204				}
3205				else
3206				{
3207					$aWriteableAttList[$sAttCode] = $oAttDef;
3208				}
3209			}
3210		}
3211
3212		return $aWriteableAttList;
3213	}
3214
3215	/**
3216	 * Compute the attribute flags depending on the object state
3217	 */
3218	public function GetFormAttributeFlags($sAttCode)
3219	{
3220		if ($this->IsNew())
3221		{
3222			$iFlags = $this->GetInitialStateAttributeFlags($sAttCode);
3223		}
3224		else
3225		{
3226			$iFlags = $this->GetAttributeFlags($sAttCode);
3227		}
3228		if (($iFlags & OPT_ATT_MANDATORY) && $this->IsNew())
3229		{
3230			$iFlags = $iFlags & ~OPT_ATT_READONLY; // Mandatory fields cannot be read-only when creating an object
3231		}
3232
3233		return $iFlags;
3234	}
3235
3236	/**
3237	 * Updates the object from a flat array of values
3238	 *
3239	 * @param $aValues array of attcode => scalar or array (N-N links)
3240	 *
3241	 * @return void
3242	 * @throws \ArchivedObjectException
3243	 * @throws \CoreException
3244	 * @throws \CoreUnexpectedValue
3245	 */
3246	public function UpdateObjectFromArray($aValues)
3247	{
3248		foreach($aValues as $sAttCode => $value)
3249		{
3250			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
3251			switch ($oAttDef->GetEditClass())
3252			{
3253				case 'Document':
3254					// There should be an uploaded file with the named attr_<attCode>
3255					$oDocument = $value['fcontents'];
3256					if (!$oDocument->IsEmpty())
3257					{
3258						// A new file has been uploaded
3259						$this->Set($sAttCode, $oDocument);
3260					}
3261					break;
3262				case 'Image':
3263					// There should be an uploaded file with the named attr_<attCode>
3264					if ($value['remove'])
3265					{
3266						$this->Set($sAttCode, null);
3267					}
3268					else
3269					{
3270						$oDocument = $value['fcontents'];
3271						if (!$oDocument->IsEmpty())
3272						{
3273							// A new file has been uploaded
3274							$this->Set($sAttCode, $oDocument);
3275						}
3276					}
3277					break;
3278				case 'One Way Password':
3279					// Check if the password was typed/changed
3280					$aPwdData = $value;
3281					if (!is_null($aPwdData) && $aPwdData['changed'])
3282					{
3283						// The password has been changed or set
3284						$this->Set($sAttCode, $aPwdData['value']);
3285					}
3286					break;
3287				case 'Duration':
3288					$aDurationData = $value;
3289					if (!is_array($aDurationData))
3290					{
3291						break;
3292					}
3293
3294					$iValue = (((24 * $aDurationData['d']) + $aDurationData['h']) * 60 + $aDurationData['m']) * 60 + $aDurationData['s'];
3295					$this->Set($sAttCode, $iValue);
3296					$previousValue = $this->Get($sAttCode);
3297					if ($previousValue !== $iValue)
3298					{
3299						$this->Set($sAttCode, $iValue);
3300					}
3301					break;
3302				case 'CustomFields':
3303					$this->Set($sAttCode, $value);
3304					break;
3305				case 'LinkedSet':
3306					if ($this->IsValueModified($value))
3307					{
3308						$oLinkSet = $this->Get($sAttCode);
3309						$sLinkedClass = $oAttDef->GetLinkedClass();
3310						if (array_key_exists('to_be_created', $value) && (count($value['to_be_created']) > 0))
3311						{
3312							// Now handle the links to be created
3313							foreach ($value['to_be_created'] as $aData)
3314							{
3315								$sSubClass = $aData['class'];
3316								if (($sLinkedClass == $sSubClass) || (is_subclass_of($sSubClass, $sLinkedClass)))
3317								{
3318									$aObjData = $aData['data'];
3319									$oLink = MetaModel::NewObject($sSubClass);
3320									$oLink->UpdateObjectFromArray($aObjData);
3321									$oLinkSet->AddItem($oLink);
3322								}
3323							}
3324						}
3325						if (array_key_exists('to_be_added', $value) && (count($value['to_be_added']) > 0))
3326						{
3327							// Now handle the links to be added by making the remote object point to self
3328							foreach ($value['to_be_added'] as $iObjKey)
3329							{
3330								$oLink = MetaModel::GetObject($sLinkedClass, $iObjKey, false);
3331								if ($oLink)
3332								{
3333									$oLinkSet->AddItem($oLink);
3334								}
3335							}
3336						}
3337						if (array_key_exists('to_be_modified', $value) && (count($value['to_be_modified']) > 0))
3338						{
3339							// Now handle the links to be added by making the remote object point to self
3340							foreach ($value['to_be_modified'] as $iObjKey => $aData)
3341							{
3342								$oLink = MetaModel::GetObject($sLinkedClass, $iObjKey, false);
3343								if ($oLink)
3344								{
3345									$aObjData = $aData['data'];
3346									$oLink->UpdateObjectFromArray($aObjData);
3347									$oLinkSet->ModifyItem($oLink);
3348								}
3349							}
3350						}
3351						if (array_key_exists('to_be_removed', $value) && (count($value['to_be_removed']) > 0))
3352						{
3353							foreach ($value['to_be_removed'] as $iObjKey)
3354							{
3355								$oLinkSet->RemoveItem($iObjKey);
3356							}
3357						}
3358						if (array_key_exists('to_be_deleted', $value) && (count($value['to_be_deleted']) > 0))
3359						{
3360							foreach ($value['to_be_deleted'] as $iObjKey)
3361							{
3362								$oLinkSet->RemoveItem($iObjKey);
3363							}
3364						}
3365						$this->Set($sAttCode, $oLinkSet);
3366					}
3367					break;
3368
3369				case 'TagSet':
3370					/** @var ormTagSet $oTagSet */
3371					$oTagSet = $this->Get($sAttCode);
3372					if (is_null($oTagSet))
3373					{
3374						$oTagSet = new ormTagSet(get_class($this), $sAttCode, $oAttDef->GetMaxItems());
3375					}
3376					$oTagSet->ApplyDelta($value);
3377					$this->Set($sAttCode, $oTagSet);
3378					break;
3379
3380				case 'Set':
3381					/** @var ormSet $oSet */
3382					$oSet = $this->Get($sAttCode);
3383					if (is_null($oSet))
3384					{
3385						$oSet = new ormSet(get_class($this), $sAttCode, $oAttDef->GetMaxItems());
3386					}
3387					$oSet->ApplyDelta($value);
3388					$this->Set($sAttCode, $oSet);
3389					break;
3390
3391				default:
3392					if (!is_null($value))
3393					{
3394						$aAttributes[$sAttCode] = trim($value);
3395						$previousValue = $this->Get($sAttCode);
3396						if ($previousValue !== $aAttributes[$sAttCode])
3397						{
3398							$this->Set($sAttCode, $aAttributes[$sAttCode]);
3399						}
3400					}
3401			}
3402		}
3403	}
3404
3405	private function IsValueModified($value)
3406	{
3407		$aModifiedKeys = ['to_be_created', 'to_be_added', 'to_be_modified', 'to_be_removed', 'to_be_deleted'];
3408		foreach ($aModifiedKeys as $sModifiedKey) {
3409			if (array_key_exists( $sModifiedKey, $value) && (count($value[$sModifiedKey]) > 0))
3410			{
3411				return true;
3412			}
3413		}
3414		return false;
3415	}
3416
3417	/**
3418	 * Updates the object from the POSTed parameters (form)
3419	 */
3420	public function UpdateObjectFromPostedForm($sFormPrefix = '', $aAttList = null, $aAttFlags = array())
3421	{
3422		if (is_null($aAttList))
3423		{
3424			$aAttList = array_keys(MetaModel::ListAttributeDefs(get_class($this)));
3425		}
3426		$aValues = array();
3427		foreach($aAttList as $sAttCode)
3428		{
3429			$value = $this->PrepareValueFromPostedForm($sFormPrefix, $sAttCode);
3430			if (!is_null($value))
3431			{
3432				$aValues[$sAttCode] = $value;
3433			}
3434		}
3435
3436		$aErrors = array();
3437		$aFinalValues = array();
3438		foreach($this->GetWriteableAttList(array_keys($aValues), $aErrors, $aAttFlags) as $sAttCode => $oAttDef)
3439		{
3440			$aFinalValues[$sAttCode] = $aValues[$sAttCode];
3441		}
3442		try
3443		{
3444			$this->UpdateObjectFromArray($aFinalValues);
3445		}
3446		catch (CoreException $e)
3447		{
3448			$aErrors[] = $e->getMessage();
3449		}
3450		if (!$this->IsNew()) // for new objects this is performed in DBInsertNoReload()
3451		{
3452			InlineImage::FinalizeInlineImages($this);
3453		}
3454
3455		// Invoke extensions after the update of the object from the form
3456		foreach(MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
3457		{
3458			$oExtensionInstance->OnFormSubmit($this, $sFormPrefix);
3459		}
3460
3461		return $aErrors;
3462	}
3463
3464	/**
3465	 * @param string $sFormPrefix
3466	 * @param string $sAttCode
3467	 * @param string $sClass Optional parameter, host object's class for the $sAttCode
3468	 * @param array $aPostedData Optional parameter, used through recursive calls
3469	 *
3470	 * @return array|null
3471	 * @throws \FileUploadException
3472	 */
3473	protected function PrepareValueFromPostedForm($sFormPrefix, $sAttCode, $sClass = null, $aPostedData = null)
3474	{
3475		if ($sClass === null)
3476		{
3477			$sClass = get_class($this);
3478		}
3479
3480		$value = null;
3481
3482		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
3483		switch ($oAttDef->GetEditClass())
3484		{
3485			case  'Document':
3486				$value = array('fcontents' => utils::ReadPostedDocument("attr_{$sFormPrefix}{$sAttCode}", 'fcontents'));
3487				break;
3488
3489			case 'Image':
3490				$oImage = utils::ReadPostedDocument("attr_{$sFormPrefix}{$sAttCode}", 'fcontents');
3491				$aSize = utils::GetImageSize($oImage->GetData());
3492				$oImage = utils::ResizeImageToFit($oImage, $aSize[0], $aSize[1], $oAttDef->Get('storage_max_width'),
3493					$oAttDef->Get('storage_max_height'));
3494				$aOtherData = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
3495				if (is_array($aOtherData))
3496				{
3497					$value = array('fcontents' => $oImage, 'remove' => $aOtherData['remove']);
3498				}
3499				else
3500				{
3501					$value = null;
3502				}
3503				break;
3504
3505			case 'RedundancySetting':
3506				$value = $oAttDef->ReadValueFromPostedForm($sFormPrefix);
3507				break;
3508
3509			case 'CustomFields':
3510				$value = $oAttDef->ReadValueFromPostedForm($this, $sFormPrefix);
3511				break;
3512
3513			case 'LinkedSet':
3514				/** @var AttributeLinkedSet $oAttDef */
3515				$aRawToBeCreated = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbc", '{}',
3516					'raw_data'), true);
3517				$aToBeCreated = array();
3518				foreach($aRawToBeCreated as $aData)
3519				{
3520					$sSubFormPrefix = $aData['formPrefix'];
3521					$sObjClass = isset($aData['class']) ? $aData['class'] : $oAttDef->GetLinkedClass();
3522					$aObjData = array();
3523					foreach($aData as $sKey => $value)
3524					{
3525						if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches))
3526						{
3527							$oLinkAttDef = MetaModel::GetAttributeDef($sObjClass, $aMatches[1]);
3528							// Recursing over n:n link datetime attributes
3529							// Note: We might need to do it with other attribute types, like Document or redundancy setting.
3530							if ($oLinkAttDef instanceof AttributeDateTime)
3531							{
3532								$aObjData[$aMatches[1]] = $this->PrepareValueFromPostedForm($sSubFormPrefix,
3533									$aMatches[1], $sObjClass, $aData);
3534							}
3535							else
3536							{
3537								$aObjData[$aMatches[1]] = $value;
3538							}
3539						}
3540					}
3541					$aToBeCreated[] = array('class' => $sObjClass, 'data' => $aObjData);
3542				}
3543
3544				$aRawToBeModified = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbm", '{}',
3545					'raw_data'), true);
3546				$aToBeModified = array();
3547				foreach($aRawToBeModified as $iObjKey => $aData)
3548				{
3549					$sSubFormPrefix = $aData['formPrefix'];
3550					$sObjClass = isset($aData['class']) ? $aData['class'] : $oAttDef->GetLinkedClass();
3551					$aObjData = array();
3552					foreach($aData as $sKey => $value)
3553					{
3554						if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches))
3555						{
3556							$oLinkAttDef = MetaModel::GetAttributeDef($sObjClass, $aMatches[1]);
3557							// Recursing over n:n link datetime attributes
3558							// Note: We might need to do it with other attribute types, like Document or redundancy setting.
3559							if ($oLinkAttDef instanceof AttributeDateTime)
3560							{
3561								$aObjData[$aMatches[1]] = $this->PrepareValueFromPostedForm($sSubFormPrefix,
3562									$aMatches[1], $sObjClass, $aData);
3563							}
3564							else
3565							{
3566								$aObjData[$aMatches[1]] = $value;
3567							}
3568						}
3569					}
3570					$aToBeModified[$iObjKey] = array('data' => $aObjData);
3571				}
3572
3573				$value = array(
3574					'to_be_created' => $aToBeCreated,
3575					'to_be_modified' => $aToBeModified,
3576					'to_be_deleted' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbd", '[]',
3577						'raw_data'), true),
3578					'to_be_added' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tba", '[]',
3579						'raw_data'), true),
3580					'to_be_removed' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbr", '[]',
3581						'raw_data'), true),
3582				);
3583				break;
3584
3585			case 'Set':
3586			case 'TagSet':
3587				$sTagSetJson = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
3588				$value = json_decode($sTagSetJson, true);
3589				break;
3590
3591			default:
3592				if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime
3593				{
3594					// Retrieving value from array when present (means what we are in a recursion)
3595					if ($aPostedData !== null && isset($aPostedData['attr_'.$sFormPrefix.$sAttCode]))
3596					{
3597						$value = $aPostedData['attr_'.$sFormPrefix.$sAttCode];
3598					}
3599					else
3600					{
3601						$value = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
3602					}
3603
3604					if ($value != null)
3605					{
3606						$oDate = $oAttDef->GetFormat()->Parse($value);
3607						if ($oDate instanceof DateTime)
3608						{
3609							$value = $oDate->format($oAttDef->GetInternalFormat());
3610						}
3611						else
3612						{
3613							$value = null;
3614						}
3615					}
3616				}
3617				else
3618				{
3619					// Retrieving value from array when present (means what we are in a recursion)
3620					if ($aPostedData !== null && isset($aPostedData['attr_'.$sFormPrefix.$sAttCode]))
3621					{
3622						$value = $aPostedData['attr_'.$sFormPrefix.$sAttCode];
3623					}
3624					else
3625					{
3626						$value = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
3627					}
3628				}
3629				break;
3630		}
3631
3632		return $value;
3633	}
3634
3635	/**
3636	 * Updates the object from a given page argument
3637	 */
3638	public function UpdateObjectFromArg($sArgName, $aAttList = null, $aAttFlags = array())
3639	{
3640		if (is_null($aAttList))
3641		{
3642			$aAttList = array_keys(MetaModel::ListAttributeDefs(get_class($this)));
3643		}
3644		$aRawValues = utils::ReadParam($sArgName, array(), '', 'raw_data');
3645		$aValues = array();
3646		foreach($aAttList as $sAttCode)
3647		{
3648			if (isset($aRawValues[$sAttCode]))
3649			{
3650				$aValues[$sAttCode] = $aRawValues[$sAttCode];
3651			}
3652		}
3653
3654		$aErrors = array();
3655		$aFinalValues = array();
3656		foreach($this->GetWriteableAttList(array_keys($aValues), $aErrors, $aAttFlags) as $sAttCode => $oAttDef)
3657		{
3658			if ($oAttDef->IsLinkSet())
3659			{
3660				$aFinalValues[$sAttCode] = json_decode($aValues[$sAttCode], true);
3661			}
3662			else
3663			{
3664				$aFinalValues[$sAttCode] = $aValues[$sAttCode];
3665			}
3666		}
3667		try
3668		{
3669			$this->UpdateObjectFromArray($aFinalValues);
3670		}
3671		catch (CoreException $e)
3672		{
3673			$aErrors[] = $e->getMessage();
3674		}
3675		return $aErrors;
3676	}
3677
3678	/**
3679	 * @inheritdoc
3680	 */
3681	public function DBInsertNoReload()
3682	{
3683		$res = parent::DBInsertNoReload();
3684
3685		$this->SetWarningsAsSessionMessages('create');
3686
3687		// Invoke extensions after insertion (the object must exist, have an id, etc.)
3688		/** @var \iApplicationObjectExtension $oExtensionInstance */
3689		foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
3690		{
3691			$oExtensionInstance->OnDBInsert($this, self::GetCurrentChange());
3692		}
3693
3694		return $res;
3695	}
3696
3697	/**
3698	 * @inheritdoc
3699	 * Attaches InlineImages to the current object
3700	 */
3701	protected function OnObjectKeyReady()
3702	{
3703		InlineImage::FinalizeInlineImages($this);
3704	}
3705
3706	protected function DBCloneTracked_Internal($newKey = null)
3707	{
3708		$oNewObj = parent::DBCloneTracked_Internal($newKey);
3709
3710		// Invoke extensions after insertion (the object must exist, have an id, etc.)
3711		/** @var \iApplicationObjectExtension $oExtensionInstance */
3712		foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
3713		{
3714			$oExtensionInstance->OnDBInsert($oNewObj, self::GetCurrentChange());
3715		}
3716
3717		return $oNewObj;
3718	}
3719
3720	public function DBUpdate()
3721	{
3722		$res = parent::DBUpdate();
3723
3724		$this->SetWarningsAsSessionMessages('update');
3725
3726		// Protection against reentrance (e.g. cascading the update of ticket logs)
3727		// Note: This is based on the fix made on r 3190 in DBObject::DBUpdate()
3728		static $aUpdateReentrance = array();
3729		$sKey = get_class($this).'::'.$this->GetKey();
3730		if (array_key_exists($sKey, $aUpdateReentrance))
3731		{
3732			return $res;
3733		}
3734		$aUpdateReentrance[$sKey] = true;
3735
3736		try
3737		{
3738			// Invoke extensions after the update (could be before)
3739			/** @var \iApplicationObjectExtension $oExtensionInstance */
3740			foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
3741			{
3742				$oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
3743			}
3744		} catch (Exception $e)
3745		{
3746			unset($aUpdateReentrance[$sKey]);
3747			throw $e;
3748		}
3749
3750		unset($aUpdateReentrance[$sKey]);
3751
3752		return $res;
3753	}
3754
3755	/**
3756	 * @param string $sMessageIdPrefix
3757	 *
3758	 * @since 2.6
3759	 */
3760	protected function SetWarningsAsSessionMessages($sMessageIdPrefix)
3761	{
3762		if (!empty($this->m_aCheckWarnings) && is_array($this->m_aCheckWarnings))
3763		{
3764			$iMsgNb = 0;
3765			foreach ($this->m_aCheckWarnings as $sWarningMessage)
3766			{
3767				$iMsgNb++;
3768				$sMessageId = "$sMessageIdPrefix-$iMsgNb"; // each message must have its own messageId !
3769				$this->SetSessionMessageFromInstance($sMessageId, $sWarningMessage, 'info', 0);
3770			}
3771		}
3772	}
3773
3774	protected static function BulkUpdateTracked_Internal(DBSearch $oFilter, array $aValues)
3775	{
3776		// Todo - invoke the extension
3777		return parent::BulkUpdateTracked_Internal($oFilter, $aValues);
3778	}
3779
3780	protected function DBDeleteTracked_Internal(&$oDeletionPlan = null)
3781	{
3782		// Invoke extensions before the deletion (the deletion will do some cleanup and we might loose some information
3783		/** @var \iApplicationObjectExtension $oExtensionInstance */
3784		foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
3785		{
3786			$oExtensionInstance->OnDBDelete($this, self::GetCurrentChange());
3787		}
3788
3789		return parent::DBDeleteTracked_Internal($oDeletionPlan);
3790	}
3791
3792	public function IsModified()
3793	{
3794		if (parent::IsModified())
3795		{
3796			return true;
3797		}
3798
3799		// Plugins
3800		//
3801		/** @var \iApplicationObjectExtension $oExtensionInstance */
3802		foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
3803		{
3804			if ($oExtensionInstance->OnIsModified($this))
3805			{
3806				return true;
3807			}
3808		}
3809
3810		return false;
3811	}
3812
3813	/**
3814	 * Bypass the check of the user rights when writing this object
3815	 *
3816	 * @param bool $bAllow True to bypass the checks, false to restore the default behavior
3817	 */
3818	public function AllowWrite($bAllow = true)
3819	{
3820		$this->bAllowWrite = $bAllow;
3821	}
3822
3823	/**
3824	 * @inheritdoc
3825	 * @throws \ArchivedObjectException
3826	 * @throws \CoreException
3827	 * @throws \OQLException
3828	 */
3829	public function DoCheckToWrite()
3830	{
3831		parent::DoCheckToWrite();
3832
3833		// Plugins
3834		//
3835		/** @var \iApplicationObjectExtension $oExtensionInstance */
3836		foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
3837		{
3838			$aNewIssues = $oExtensionInstance->OnCheckToWrite($this);
3839			if (is_array($aNewIssues) && (count($aNewIssues) > 0)) // Some extensions return null instead of an empty array
3840			{
3841				$this->m_aCheckIssues = array_merge($this->m_aCheckIssues, $aNewIssues);
3842			}
3843		}
3844
3845		// User rights
3846		//
3847		if (!$this->bAllowWrite)
3848		{
3849			$aChanges = $this->ListChanges();
3850			if (count($aChanges) > 0)
3851			{
3852				$aForbiddenFields = array();
3853				foreach($this->ListChanges() as $sAttCode => $value)
3854				{
3855					$bUpdateAllowed = UserRights::IsActionAllowedOnAttribute(get_class($this), $sAttCode,
3856						UR_ACTION_MODIFY, DBObjectSet::FromObject($this));
3857					if (!$bUpdateAllowed)
3858					{
3859						$oAttCode = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
3860						$aForbiddenFields[] = $oAttCode->GetLabel();
3861					}
3862				}
3863				if (count($aForbiddenFields) > 0)
3864				{
3865					// Security issue
3866					$this->m_bSecurityIssue = true;
3867					$this->m_aCheckIssues[] = Dict::Format('UI:Delete:NotAllowedToUpdate_Fields',
3868						implode(', ', $aForbiddenFields));
3869				}
3870			}
3871		}
3872	}
3873
3874	protected function DoCheckToDelete(&$oDeletionPlan)
3875	{
3876		parent::DoCheckToDelete($oDeletionPlan);
3877
3878		// Plugins
3879		//
3880		/** @var \iApplicationObjectExtension $oExtensionInstance */
3881		foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
3882		{
3883			$aNewIssues = $oExtensionInstance->OnCheckToDelete($this);
3884			if (is_array($aNewIssues) && count($aNewIssues) > 0)
3885			{
3886				$this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues, $aNewIssues);
3887			}
3888		}
3889
3890		// User rights
3891		//
3892		$bDeleteAllowed = UserRights::IsActionAllowed(get_class($this), UR_ACTION_DELETE,
3893			DBObjectSet::FromObject($this));
3894		if (!$bDeleteAllowed)
3895		{
3896			// Security issue
3897			$this->m_bSecurityIssue = true;
3898			$this->m_aDeleteIssues[] = Dict::S('UI:Delete:NotAllowedToDelete');
3899		}
3900	}
3901
3902	/**
3903	 * Special display where the case log uses the whole "screen" at the bottom of the "Properties" tab
3904	 */
3905	public function DisplayCaseLog(WebPage $oPage, $sAttCode, $sComment = '', $sPrefix = '', $bEditMode = false)
3906	{
3907		$oPage->SetCurrentTab(Dict::S('UI:PropertiesTab'));
3908		$sClass = get_class($this);
3909		if ($this->IsNew())
3910		{
3911			$iFlags = $this->GetInitialStateAttributeFlags($sAttCode);
3912		}
3913		else
3914		{
3915			$iFlags = $this->GetAttributeFlags($sAttCode);
3916		}
3917		if ($iFlags & OPT_ATT_HIDDEN)
3918		{
3919			// The case log is hidden do nothing
3920		}
3921		else
3922		{
3923			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
3924			$sInputId = $this->m_iFormId.'_'.$sAttCode;
3925
3926			if ((!$bEditMode) || ($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE)))
3927			{
3928				// Check if the attribute is not read-only because of a synchro...
3929				$sSynchroIcon = '';
3930				if ($iFlags & OPT_ATT_SLAVE)
3931				{
3932					$aReasons = array();
3933					$iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
3934					$sSynchroIcon = "&nbsp;<img id=\"synchro_$sInputId\" src=\"../images/transp-lock.png\" style=\"vertical-align:middle\"/>";
3935					$sTip = '';
3936					foreach($aReasons as $aRow)
3937					{
3938						$sDescription = htmlentities($aRow['description'], ENT_QUOTES, 'UTF-8');
3939						$sDescription = str_replace(array("\r\n", "\n"), "<br/>", $sDescription);
3940						$sTip .= "<div class='synchro-source'>";
3941						$sTip .= "<div class='synchro-source-title'>Synchronized with {$aRow['name']}</div>";
3942						$sTip .= "<div class='synchro-source-description'>$sDescription</div>";
3943					}
3944					$oPage->add_ready_script("$('#synchro_$sInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );");
3945				}
3946
3947				// Attribute is read-only
3948				$sHTMLValue = $this->GetAsHTML($sAttCode);
3949				$sHTMLValue .= '<input type="hidden" id="'.$sInputId.'" name="attr_'.$sPrefix.$sAttCode.'" value="'.htmlentities($this->GetEditValue($sAttCode),
3950						ENT_QUOTES, 'UTF-8').'"/>';
3951				$aFieldsMap[$sAttCode] = $sInputId;
3952				$sComment .= $sSynchroIcon;
3953			}
3954			else
3955			{
3956				$sValue = $this->Get($sAttCode);
3957				$sDisplayValue = $this->GetEditValue($sAttCode);
3958				$aArgs = array('this' => $this, 'formPrefix' => $sPrefix);
3959				$sHTMLValue = '';
3960				if ($sComment != '')
3961				{
3962					$sHTMLValue = '<span>'.$sComment.'</span><br/>';
3963				}
3964				$sHTMLValue .= "<span style=\"font-family:Tahoma,Verdana,Arial,Helvetica;font-size:12px;\" id=\"field_{$sInputId}\">".self::GetFormElementForField($oPage,
3965						$sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags,
3966						$aArgs).'</span>';
3967				$aFieldsMap[$sAttCode] = $sInputId;
3968			}
3969			$oPage->add('<fieldset><legend>'.$oAttDef->GetLabel().'</legend>');
3970			$oPage->add($sHTMLValue);
3971			$oPage->add('</fieldset>');
3972		}
3973	}
3974
3975	/**
3976	 * @param $sCurrentState
3977	 * @param $sStimulus
3978	 * @param $bOnlyNewOnes
3979	 *
3980	 * @return array
3981	 * @throws \ApplicationException
3982	 * @throws \CoreException
3983	 * @deprecated Since iTop 2.4, use DBObject::GetTransitionAttributes() instead.
3984	 */
3985	public function GetExpectedAttributes($sCurrentState, $sStimulus, $bOnlyNewOnes)
3986	{
3987		$aTransitions = $this->EnumTransitions();
3988		$aStimuli = MetaModel::EnumStimuli(get_class($this));
3989		if (!isset($aTransitions[$sStimulus]))
3990		{
3991			// Invalid stimulus
3992			throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sStimulus,
3993				$this->GetName(), $this->GetStateLabel()));
3994		}
3995		$aTransition = $aTransitions[$sStimulus];
3996		$sTargetState = $aTransition['target_state'];
3997		$aTargetStates = MetaModel::EnumStates(get_class($this));
3998		$aTargetState = $aTargetStates[$sTargetState];
3999		$aCurrentState = $aTargetStates[$this->GetState()];
4000		$aExpectedAttributes = $aTargetState['attribute_list'];
4001		$aCurrentAttributes = $aCurrentState['attribute_list'];
4002
4003		$aComputedAttributes = array();
4004		foreach($aExpectedAttributes as $sAttCode => $iExpectCode)
4005		{
4006			if (!array_key_exists($sAttCode, $aCurrentAttributes))
4007			{
4008				$aComputedAttributes[$sAttCode] = $iExpectCode;
4009			}
4010			else
4011			{
4012				if (!($aCurrentAttributes[$sAttCode] & (OPT_ATT_HIDDEN | OPT_ATT_READONLY)))
4013				{
4014					$iExpectCode = $iExpectCode & ~(OPT_ATT_MUSTPROMPT | OPT_ATT_MUSTCHANGE); // Already prompted/changed, reset the flags
4015				}
4016				// Later: better check if the attribute is not *null*
4017				if (($iExpectCode & OPT_ATT_MANDATORY) && ($this->Get($sAttCode) != ''))
4018				{
4019					$iExpectCode = $iExpectCode & ~(OPT_ATT_MANDATORY); // If the attribute is present, then no need to request its presence
4020				}
4021
4022				$aComputedAttributes[$sAttCode] = $iExpectCode;
4023			}
4024
4025			$aComputedAttributes[$sAttCode] = $aComputedAttributes[$sAttCode] & ~(OPT_ATT_READONLY | OPT_ATT_HIDDEN); // Don't care about this form now
4026
4027			if ($aComputedAttributes[$sAttCode] == 0)
4028			{
4029				unset($aComputedAttributes[$sAttCode]);
4030			}
4031		}
4032
4033		return $aComputedAttributes;
4034	}
4035
4036	/**
4037	 * Display a form for modifying several objects at once
4038	 * The form will be submitted to the current page, with the specified additional values
4039	 *
4040	 * @param \iTopWebPage $oP
4041	 * @param string $sClass
4042	 * @param array $aSelectedObj
4043	 * @param string $sCustomOperation
4044	 * @param string $sCancelUrl
4045	 * @param array $aExcludeAttributes
4046	 * @param array $aContextData
4047	 *
4048	 * @throws \CoreException
4049	 * @throws \CoreUnexpectedValue
4050	 * @throws \MySQLException
4051	 * @throws \OQLException
4052	 */
4053	public static function DisplayBulkModifyForm($oP, $sClass, $aSelectedObj, $sCustomOperation, $sCancelUrl, $aExcludeAttributes = array(), $aContextData = array())
4054	{
4055		if (count($aSelectedObj) > 0)
4056		{
4057			$iAllowedCount = count($aSelectedObj);
4058			$sSelectedObj = implode(',', $aSelectedObj);
4059
4060			$sOQL = "SELECT $sClass WHERE id IN (".$sSelectedObj.")";
4061			$oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL));
4062
4063			// Compute the distribution of the values for each field to determine which of the "scalar" fields are homogeneous
4064			$aList = MetaModel::ListAttributeDefs($sClass);
4065			$aValues = array();
4066			foreach($aList as $sAttCode => $oAttDef)
4067			{
4068				if ($oAttDef->IsScalar())
4069				{
4070					$aValues[$sAttCode] = array();
4071				}
4072			}
4073			while ($oObj = $oSet->Fetch())
4074			{
4075				foreach($aList as $sAttCode => $oAttDef)
4076				{
4077					if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
4078					{
4079						$currValue = $oObj->Get($sAttCode);
4080						if ($oAttDef instanceof AttributeCaseLog)
4081						{
4082							$currValue = ''; // Put a single scalar value to force caselog to mock a new entry. For more info see N°1059.
4083						}
4084						elseif ($currValue instanceof ormSet)
4085						{
4086							$currValue = $oAttDef->GetEditValue($currValue, $oObj);
4087						}
4088						if (is_object($currValue))
4089						{
4090							continue;
4091						} // Skip non scalar values...
4092						if (!array_key_exists($currValue, $aValues[$sAttCode]))
4093						{
4094							$aValues[$sAttCode][$currValue] = array(
4095								'count' => 1,
4096								'display' => $oObj->GetAsHTML($sAttCode),
4097							);
4098						}
4099						else
4100						{
4101							$aValues[$sAttCode][$currValue]['count']++;
4102						}
4103					}
4104				}
4105			}
4106			// Now create an object that has values for the homogeneous values only
4107			/** @var \cmdbAbstractObject $oDummyObj */
4108			$oDummyObj = new $sClass(); // @@ What if the class is abstract ?
4109			$aComments = array();
4110			function MyComparison($a, $b) // Sort descending
4111			{
4112				if ($a['count'] == $b['count'])
4113				{
4114					return 0;
4115				}
4116
4117				return ($a['count'] > $b['count']) ? -1 : 1;
4118			}
4119
4120			$iFormId = cmdbAbstractObject::GetNextFormId(); // Identifier that prefixes all the form fields
4121			$sReadyScript = '';
4122			$sFormPrefix = '2_';
4123			foreach($aList as $sAttCode => $oAttDef)
4124			{
4125				$aPrerequisites = MetaModel::GetPrerequisiteAttributes($sClass,
4126					$sAttCode); // List of attributes that are needed for the current one
4127				if (count($aPrerequisites) > 0)
4128				{
4129					// When 'enabling' a field, all its prerequisites must be enabled too
4130					$sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aPrerequisites)."']";
4131					$oP->add_ready_script("$('#enable_{$sFormPrefix}{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n");
4132				}
4133				$aDependents = MetaModel::GetDependentAttributes($sClass,
4134					$sAttCode); // List of attributes that are needed for the current one
4135				if (count($aDependents) > 0)
4136				{
4137					// When 'disabling' a field, all its dependent fields must be disabled too
4138					$sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aDependents)."']";
4139					$oP->add_ready_script("$('#enable_{$sFormPrefix}{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n");
4140				}
4141				if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
4142				{
4143					if ($oAttDef->GetEditClass() == 'One Way Password')
4144					{
4145
4146						$sTip = "Unknown values";
4147						$sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );";
4148
4149						$oDummyObj->Set($sAttCode, null);
4150						$aComments[$sAttCode] = '<input type="checkbox" id="enable_'.$iFormId.'_'.$sAttCode.'" onClick="ToggleField(this.checked, \''.$iFormId.'_'.$sAttCode.'\')"/>';
4151						$aComments[$sAttCode] .= '<div class="multi_values" id="multi_values_'.$sAttCode.'"> ? </div>';
4152						$sReadyScript .= 'ToggleField(false, \''.$iFormId.'_'.$sAttCode.'\');'."\n";
4153					}
4154					else
4155					{
4156						$iCount = count($aValues[$sAttCode]);
4157						if ($iCount == 1)
4158						{
4159							// Homogeneous value
4160							reset($aValues[$sAttCode]);
4161							$aKeys = array_keys($aValues[$sAttCode]);
4162							$currValue = $aKeys[0]; // The only value is the first key
4163							//echo "<p>current value for $sAttCode : $currValue</p>";
4164							$oDummyObj->Set($sAttCode, $currValue);
4165							$aComments[$sAttCode] = '';
4166							if ($sAttCode != MetaModel::GetStateAttributeCode($sClass))
4167							{
4168								$aComments[$sAttCode] .= '<input type="checkbox" checked id="enable_'.$iFormId.'_'.$sAttCode.'"  onClick="ToggleField(this.checked, \''.$iFormId.'_'.$sAttCode.'\')"/>';
4169							}
4170							$aComments[$sAttCode] .= '<div class="mono_value">1</div>';
4171						}
4172						else
4173						{
4174							// Non-homogeneous value
4175							$aMultiValues = $aValues[$sAttCode];
4176							uasort($aMultiValues, 'MyComparison');
4177							$iMaxCount = 5;
4178							$sTip = "<p><b>".Dict::Format('UI:BulkModify_Count_DistinctValues', $iCount)."</b><ul>";
4179							$index = 0;
4180							foreach($aMultiValues as $sCurrValue => $aVal)
4181							{
4182								$sDisplayValue = empty($aVal['display']) ? '<i>'.Dict::S('Enum:Undefined').'</i>' : str_replace(array(
4183									"\n",
4184									"\r",
4185								), " ", $aVal['display']);
4186								$sTip .= "<li>".Dict::Format('UI:BulkModify:Value_Exists_N_Times', $sDisplayValue,
4187										$aVal['count'])."</li>";
4188								$index++;
4189								if ($iMaxCount == $index)
4190								{
4191									$sTip .= "<li>".Dict::Format('UI:BulkModify:N_MoreValues',
4192											count($aMultiValues) - $iMaxCount)."</li>";
4193									break;
4194								}
4195							}
4196							$sTip .= "</ul></p>";
4197							$sTip = addslashes($sTip);
4198							$sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );";
4199
4200							if (($oAttDef->GetEditClass() == 'TagSet') || ($oAttDef->GetEditClass() == 'Set'))
4201							{
4202								// Set the value by adding the values to the first one
4203								reset($aMultiValues);
4204								$aKeys = array_keys($aMultiValues);
4205								$currValue = $aKeys[0];
4206								$oDummyObj->Set($sAttCode, $currValue);
4207								/** @var ormTagSet $oTagSet */
4208								$oTagSet = $oDummyObj->Get($sAttCode);
4209								$oTagSet->SetDisplayPartial(true);
4210								foreach($aKeys as $iIndex => $sValues)
4211								{
4212									if ($iIndex == 0)
4213									{
4214										continue;
4215									}
4216									$aTagCodes = $oAttDef->FromStringToArray($sValues);
4217									$oTagSet->GenerateDiffFromArray($aTagCodes);
4218								}
4219								$oDummyObj->Set($sAttCode, $oTagSet);
4220							}
4221							else
4222							{
4223								$oDummyObj->Set($sAttCode, null);
4224							}
4225							$aComments[$sAttCode] = '';
4226							if ($sAttCode != MetaModel::GetStateAttributeCode($sClass))
4227							{
4228								$aComments[$sAttCode] .= '<input type="checkbox" id="enable_'.$iFormId.'_'.$sAttCode.'" onClick="ToggleField(this.checked, \''.$iFormId.'_'.$sAttCode.'\')"/>';
4229							}
4230							$aComments[$sAttCode] .= '<div class="multi_values" id="multi_values_'.$sAttCode.'">'.$iCount.'</div>';
4231						}
4232						$sReadyScript .= 'ToggleField('.(($iCount == 1) ? 'true' : 'false').', \''.$iFormId.'_'.$sAttCode.'\');'."\n";
4233					}
4234				}
4235			}
4236
4237			$sStateAttCode = MetaModel::GetStateAttributeCode($sClass);
4238			if (($sStateAttCode != '') && ($oDummyObj->GetState() == ''))
4239			{
4240				// Hmmm, it's not gonna work like this ! Set a default value for the "state"
4241				// Maybe we should use the "state" that is the most common among the objects...
4242				$aMultiValues = $aValues[$sStateAttCode];
4243				uasort($aMultiValues, 'MyComparison');
4244				foreach($aMultiValues as $sCurrValue => $aVal)
4245				{
4246					$oDummyObj->Set($sStateAttCode, $sCurrValue);
4247					break;
4248				}
4249				//$oStateAtt = MetaModel::GetAttributeDef($sClass, $sStateAttCode);
4250				//$oDummyObj->Set($sStateAttCode, $oStateAtt->GetDefaultValue());
4251			}
4252			$oP->add("<div class=\"page_header\">\n");
4253			$oP->add("<h1>".$oDummyObj->GetIcon()."&nbsp;".Dict::Format('UI:Modify_M_ObjectsOf_Class_OutOf_N',
4254					$iAllowedCount, $sClass, $iAllowedCount)."</h1>\n");
4255			$oP->add("</div>\n");
4256
4257			$oP->add("<div class=\"wizContainer\">\n");
4258			$sDisableFields = json_encode($aExcludeAttributes);
4259
4260			$aParams = array
4261			(
4262				'fieldsComments' => $aComments,
4263				'noRelations' => true,
4264				'custom_operation' => $sCustomOperation,
4265				'custom_button' => Dict::S('UI:Button:PreviewModifications'),
4266				'selectObj' => $sSelectedObj,
4267				'preview_mode' => true,
4268				'disabled_fields' => $sDisableFields,
4269				'disable_plugins' => true,
4270			);
4271			$aParams = $aParams + $aContextData; // merge keeping associations
4272
4273			$oDummyObj->DisplayModifyForm($oP, $aParams);
4274			$oP->add("</div>\n");
4275			$oP->add_ready_script($sReadyScript);
4276			$oP->add_ready_script(
4277				<<<EOF
4278$('.wizContainer button.cancel').unbind('click');
4279$('.wizContainer button.cancel').click( function() { window.location.href = '$sCancelUrl'; } );
4280EOF
4281			);
4282
4283		} // Else no object selected ???
4284		else
4285		{
4286			$oP->p("No object selected !, nothing to do");
4287		}
4288	}
4289
4290	/**
4291	 * Process the reply made from a form built with DisplayBulkModifyForm
4292	 */
4293	public static function DoBulkModify($oP, $sClass, $aSelectedObj, $sCustomOperation, $bPreview, $sCancelUrl, $aContextData = array())
4294	{
4295		$aHeaders = array(
4296			'form::select' => array(
4297				'label' => "<input type=\"checkbox\" onClick=\"CheckAll('.selectList:not(:disabled)', this.checked);\"></input>",
4298				'description' => Dict::S('UI:SelectAllToggle+'),
4299			),
4300			'object' => array('label' => MetaModel::GetName($sClass), 'description' => Dict::S('UI:ModifiedObject')),
4301			'status' => array(
4302				'label' => Dict::S('UI:BulkModifyStatus'),
4303				'description' => Dict::S('UI:BulkModifyStatus+'),
4304			),
4305			'errors' => array(
4306				'label' => Dict::S('UI:BulkModifyErrors'),
4307				'description' => Dict::S('UI:BulkModifyErrors+'),
4308			),
4309		);
4310		$aRows = array();
4311
4312		$oP->add("<div class=\"page_header\">\n");
4313		$oP->add("<h1>".MetaModel::GetClassIcon($sClass)."&nbsp;".Dict::Format('UI:Modify_N_ObjectsOf_Class',
4314				count($aSelectedObj), MetaModel::GetName($sClass))."</h1>\n");
4315		$oP->add("</div>\n");
4316		$oP->set_title(Dict::Format('UI:Modify_N_ObjectsOf_Class', count($aSelectedObj), $sClass));
4317		if (!$bPreview)
4318		{
4319			// Not in preview mode, do the update for real
4320			$sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id');
4321			if (!utils::IsTransactionValid($sTransactionId, false))
4322			{
4323				throw new Exception(Dict::S('UI:Error:ObjectAlreadyUpdated'));
4324			}
4325			utils::RemoveTransaction($sTransactionId);
4326		}
4327		$iPreviousTimeLimit = ini_get('max_execution_time');
4328		$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
4329		foreach($aSelectedObj as $iId)
4330		{
4331			set_time_limit($iLoopTimeLimit);
4332			/** @var \cmdbAbstractObject $oObj */
4333			$oObj = MetaModel::GetObject($sClass, $iId);
4334			$aErrors = $oObj->UpdateObjectFromPostedForm('');
4335			$bResult = (count($aErrors) == 0);
4336			if ($bResult)
4337			{
4338				list($bResult, $aErrors) = $oObj->CheckToWrite();
4339			}
4340			if ($bPreview)
4341			{
4342				$sStatus = $bResult ? Dict::S('UI:BulkModifyStatusOk') : Dict::S('UI:BulkModifyStatusError');
4343			}
4344			else
4345			{
4346				$sStatus = $bResult ? Dict::S('UI:BulkModifyStatusModified') : Dict::S('UI:BulkModifyStatusSkipped');
4347			}
4348			$sCSSClass = $bResult ? HILIGHT_CLASS_NONE : HILIGHT_CLASS_CRITICAL;
4349			$sChecked = $bResult ? 'checked' : '';
4350			$sDisabled = $bResult ? '' : 'disabled';
4351			$aRows[] = array(
4352				'form::select' => "<input type=\"checkbox\" class=\"selectList\" $sChecked $sDisabled\"></input>",
4353				'object' => $oObj->GetHyperlink(),
4354				'status' => $sStatus,
4355				'errors' => '<p>'.($bResult ? '' : implode('</p><p>', $aErrors)).'</p>',
4356				'@class' => $sCSSClass,
4357			);
4358			if ($bResult && (!$bPreview))
4359			{
4360				$oObj->DBUpdate();
4361			}
4362		}
4363		set_time_limit($iPreviousTimeLimit);
4364		$oP->Table($aHeaders, $aRows);
4365		if ($bPreview)
4366		{
4367			$sFormAction = utils::GetAbsoluteUrlAppRoot().'pages/UI.php'; // No parameter in the URL, the only parameter will be the ones passed through the form
4368			// Form to submit:
4369			$oP->add("<form method=\"post\" action=\"$sFormAction\" enctype=\"multipart/form-data\">\n");
4370			$aDefaults = utils::ReadParam('default', array());
4371			$oAppContext = new ApplicationContext();
4372			$oP->add($oAppContext->GetForForm());
4373			foreach($aContextData as $sKey => $value)
4374			{
4375				$oP->add("<input type=\"hidden\" name=\"{$sKey}\" value=\"$value\">\n");
4376			}
4377			$oP->add("<input type=\"hidden\" name=\"operation\" value=\"$sCustomOperation\">\n");
4378			$oP->add("<input type=\"hidden\" name=\"class\" value=\"$sClass\">\n");
4379			$oP->add("<input type=\"hidden\" name=\"preview_mode\" value=\"0\">\n");
4380			$oP->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".utils::GetNewTransactionId()."\">\n");
4381			$oP->add("<button type=\"button\" class=\"action cancel\" onClick=\"window.location.href='$sCancelUrl'\">".Dict::S('UI:Button:Cancel')."</button>&nbsp;&nbsp;&nbsp;&nbsp;\n");
4382			$oP->add("<button type=\"submit\" class=\"action\"><span>".Dict::S('UI:Button:ModifyAll')."</span></button>\n");
4383			foreach($_POST as $sKey => $value)
4384			{
4385				if (preg_match('/attr_(.+)/', $sKey, $aMatches))
4386				{
4387					// Beware: some values (like durations) are passed as arrays
4388					if (is_array($value))
4389					{
4390						foreach($value as $vKey => $vValue)
4391						{
4392							$oP->add("<input type=\"hidden\" name=\"{$sKey}[$vKey]\" value=\"".htmlentities($vValue,
4393									ENT_QUOTES, 'UTF-8')."\">\n");
4394						}
4395					}
4396					else
4397					{
4398						$oP->add("<input type=\"hidden\" name=\"$sKey\" value=\"".htmlentities($value, ENT_QUOTES,
4399								'UTF-8')."\">\n");
4400					}
4401				}
4402			}
4403			$oP->add("</form>\n");
4404		}
4405		else
4406		{
4407			$oP->add("<button type=\"button\" onClick=\"window.location.href='$sCancelUrl'\" class=\"action\"><span>".Dict::S('UI:Button:Done')."</span></button>\n");
4408		}
4409	}
4410
4411	/**
4412	 * Perform all the needed checks to delete one (or more) objects
4413	 *
4414	 * @param \WebPage $oP
4415	 * @param $sClass
4416	 * @param $aObjects
4417	 * @param $bPreview
4418	 * @param $sCustomOperation
4419	 * @param array $aContextData
4420	 *
4421	 * @throws \CoreException
4422	 * @throws \DictExceptionMissingString
4423	 */
4424	public static function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bPreview, $sCustomOperation, $aContextData = array())
4425	{
4426		$oDeletionPlan = new DeletionPlan();
4427
4428		foreach($aObjects as $oObj)
4429		{
4430			if ($bPreview)
4431			{
4432				$oObj->CheckToDelete($oDeletionPlan);
4433			}
4434			else
4435			{
4436				$oObj->DBDeleteTracked(CMDBObject::GetCurrentChange(), null, $oDeletionPlan);
4437			}
4438		}
4439
4440		if ($bPreview)
4441		{
4442			if (count($aObjects) == 1)
4443			{
4444				$oObj = $aObjects[0];
4445				$oP->add("<h1>".Dict::Format('UI:Delete:ConfirmDeletionOf_Name', $oObj->GetName())."</h1>\n");
4446			}
4447			else
4448			{
4449				$oP->add("<h1>".Dict::Format('UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class', count($aObjects),
4450						MetaModel::GetName($sClass))."</h1>\n");
4451			}
4452			// Explain what should be done
4453			//
4454			$aDisplayData = array();
4455			foreach($oDeletionPlan->ListDeletes() as $sTargetClass => $aDeletes)
4456			{
4457				foreach($aDeletes as $iId => $aData)
4458				{
4459					$oToDelete = $aData['to_delete'];
4460					$bAutoDel = (($aData['mode'] == DEL_SILENT) || ($aData['mode'] == DEL_AUTO));
4461					if (array_key_exists('issue', $aData))
4462					{
4463						if ($bAutoDel)
4464						{
4465							if (isset($aData['requested_explicitely']))
4466							{
4467								$sConsequence = Dict::Format('UI:Delete:CannotDeleteBecause', $aData['issue']);
4468							}
4469							else
4470							{
4471								$sConsequence = Dict::Format('UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible',
4472									$aData['issue']);
4473							}
4474						}
4475						else
4476						{
4477							$sConsequence = Dict::Format('UI:Delete:MustBeDeletedManuallyButNotPossible',
4478								$aData['issue']);
4479						}
4480					}
4481					else
4482					{
4483						if ($bAutoDel)
4484						{
4485							if (isset($aData['requested_explicitely']))
4486							{
4487								$sConsequence = ''; // not applicable
4488							}
4489							else
4490							{
4491								$sConsequence = Dict::S('UI:Delete:WillBeDeletedAutomatically');
4492							}
4493						}
4494						else
4495						{
4496							$sConsequence = Dict::S('UI:Delete:MustBeDeletedManually');
4497						}
4498					}
4499					$aDisplayData[] = array(
4500						'class' => MetaModel::GetName(get_class($oToDelete)),
4501						'object' => $oToDelete->GetHyperLink(),
4502						'consequence' => $sConsequence,
4503					);
4504				}
4505			}
4506			foreach($oDeletionPlan->ListUpdates() as $sRemoteClass => $aToUpdate)
4507			{
4508				foreach($aToUpdate as $iId => $aData)
4509				{
4510					$oToUpdate = $aData['to_reset'];
4511					if (array_key_exists('issue', $aData))
4512					{
4513						$sConsequence = Dict::Format('UI:Delete:CannotUpdateBecause_Issue', $aData['issue']);
4514					}
4515					else
4516					{
4517						$sConsequence = Dict::Format('UI:Delete:WillAutomaticallyUpdate_Fields',
4518							$aData['attributes_list']);
4519					}
4520					$aDisplayData[] = array(
4521						'class' => MetaModel::GetName(get_class($oToUpdate)),
4522						'object' => $oToUpdate->GetHyperLink(),
4523						'consequence' => $sConsequence,
4524					);
4525				}
4526			}
4527
4528			$iImpactedIndirectly = $oDeletionPlan->GetTargetCount() - count($aObjects);
4529			if ($iImpactedIndirectly > 0)
4530			{
4531				if (count($aObjects) == 1)
4532				{
4533					$oObj = $aObjects[0];
4534					$oP->p(Dict::Format('UI:Delete:Count_Objects/LinksReferencing_Object', $iImpactedIndirectly,
4535						$oObj->GetName()));
4536				}
4537				else
4538				{
4539					$oP->p(Dict::Format('UI:Delete:Count_Objects/LinksReferencingTheObjects', $iImpactedIndirectly));
4540				}
4541				$oP->p(Dict::S('UI:Delete:ReferencesMustBeDeletedToEnsureIntegrity'));
4542			}
4543
4544			if (($iImpactedIndirectly > 0) || $oDeletionPlan->FoundStopper())
4545			{
4546				$aDisplayConfig = array();
4547				$aDisplayConfig['class'] = array('label' => 'Class', 'description' => '');
4548				$aDisplayConfig['object'] = array('label' => 'Object', 'description' => '');
4549				$aDisplayConfig['consequence'] = array(
4550					'label' => 'Consequence',
4551					'description' => Dict::S('UI:Delete:Consequence+'),
4552				);
4553				$oP->table($aDisplayConfig, $aDisplayData);
4554			}
4555
4556			if ($oDeletionPlan->FoundStopper())
4557			{
4558				if ($oDeletionPlan->FoundSecurityIssue())
4559				{
4560					$oP->p(Dict::S('UI:Delete:SorryDeletionNotAllowed'));
4561				}
4562				elseif ($oDeletionPlan->FoundManualOperation())
4563				{
4564					$oP->p(Dict::S('UI:Delete:PleaseDoTheManualOperations'));
4565				}
4566				else // $bFoundManualOp
4567				{
4568					$oP->p(Dict::S('UI:Delete:PleaseDoTheManualOperations'));
4569				}
4570				$oAppContext = new ApplicationContext();
4571				$oP->add("<form method=\"post\">\n");
4572				$oP->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".utils::ReadParam('transaction_id', '', false,
4573						'transaction_id')
4574					."\">\n");
4575				$oP->add("<input type=\"button\" onclick=\"window.history.back();\" value=\"".Dict::S('UI:Button:Back')."\">\n");
4576				$oP->add("<input DISABLED type=\"submit\" name=\"\" value=\"".Dict::S('UI:Button:Delete')."\">\n");
4577				$oP->add($oAppContext->GetForForm());
4578				$oP->add("</form>\n");
4579			}
4580			else
4581			{
4582				if (count($aObjects) == 1)
4583				{
4584					$oObj = $aObjects[0];
4585					$id = $oObj->GetKey();
4586					$oP->p('<h1>'.Dict::Format('UI:Delect:Confirm_Object', $oObj->GetHyperLink()).'</h1>');
4587				}
4588				else
4589				{
4590					$oP->p('<h1>'.Dict::Format('UI:Delect:Confirm_Count_ObjectsOf_Class', count($aObjects),
4591							MetaModel::GetName($sClass)).'</h1>');
4592				}
4593				foreach($aObjects as $oObj)
4594				{
4595					$aKeys[] = $oObj->GetKey();
4596				}
4597				$oFilter = new DBObjectSearch($sClass);
4598				$oFilter->AddCondition('id', $aKeys, 'IN');
4599				$oSet = new CMDBobjectSet($oFilter);
4600				$oP->add('<div id="0">');
4601				CMDBAbstractObject::DisplaySet($oP, $oSet, array('display_limit' => false, 'menu' => false));
4602				$oP->add("</div>\n");
4603				$oP->add("<form method=\"post\">\n");
4604				foreach($aContextData as $sKey => $value)
4605				{
4606					$oP->add("<input type=\"hidden\" name=\"{$sKey}\" value=\"$value\">\n");
4607				}
4608				$oP->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".utils::GetNewTransactionId()."\">\n");
4609				$oP->add("<input type=\"hidden\" name=\"operation\" value=\"$sCustomOperation\">\n");
4610				$oP->add("<input type=\"hidden\" name=\"filter\" value=\"".htmlentities($oFilter->Serialize(), ENT_QUOTES,
4611						'UTF-8')."\">\n");
4612				$oP->add("<input type=\"hidden\" name=\"class\" value=\"$sClass\">\n");
4613				foreach($aObjects as $oObj)
4614				{
4615					$oP->add("<input type=\"hidden\" name=\"selectObject[]\" value=\"".$oObj->GetKey()."\">\n");
4616				}
4617				$oP->add("<input type=\"button\" onclick=\"window.history.back();\" value=\"".Dict::S('UI:Button:Back')."\">\n");
4618				$oP->add("<input type=\"submit\" name=\"\" value=\"".Dict::S('UI:Button:Delete')."\">\n");
4619				$oAppContext = new ApplicationContext();
4620				$oP->add($oAppContext->GetForForm());
4621				$oP->add("</form>\n");
4622			}
4623		}
4624		else // if ($bPreview)...
4625		{
4626			// Execute the deletion
4627			//
4628			if (count($aObjects) == 1)
4629			{
4630				$oObj = $aObjects[0];
4631				$oP->add("<h1>".Dict::Format('UI:Title:DeletionOf_Object', $oObj->GetName())."</h1>\n");
4632			}
4633			else
4634			{
4635				$oP->add("<h1>".Dict::Format('UI:Title:BulkDeletionOf_Count_ObjectsOf_Class', count($aObjects),
4636						MetaModel::GetName($sClass))."</h1>\n");
4637			}
4638			// Security - do not allow the user to force a forbidden delete by the mean of page arguments...
4639			if ($oDeletionPlan->FoundSecurityIssue())
4640			{
4641				throw new CoreException(Dict::S('UI:Error:NotEnoughRightsToDelete'));
4642			}
4643			if ($oDeletionPlan->FoundManualOperation())
4644			{
4645				throw new CoreException(Dict::S('UI:Error:CannotDeleteBecauseManualOpNeeded'));
4646			}
4647			if ($oDeletionPlan->FoundManualDelete())
4648			{
4649				throw new CoreException(Dict::S('UI:Error:CannotDeleteBecauseOfDepencies'));
4650			}
4651
4652			// Report deletions
4653			//
4654			$aDisplayData = array();
4655			foreach($oDeletionPlan->ListDeletes() as $sTargetClass => $aDeletes)
4656			{
4657				foreach($aDeletes as $iId => $aData)
4658				{
4659					$oToDelete = $aData['to_delete'];
4660
4661					if (isset($aData['requested_explicitely']))
4662					{
4663						$sMessage = Dict::S('UI:Delete:Deleted');
4664					}
4665					else
4666					{
4667						$sMessage = Dict::S('UI:Delete:AutomaticallyDeleted');
4668					}
4669					$aDisplayData[] = array(
4670						'class' => MetaModel::GetName(get_class($oToDelete)),
4671						'object' => $oToDelete->GetName(),
4672						'consequence' => $sMessage,
4673					);
4674				}
4675			}
4676
4677			// Report updates
4678			//
4679			foreach($oDeletionPlan->ListUpdates() as $sTargetClass => $aToUpdate)
4680			{
4681				foreach($aToUpdate as $iId => $aData)
4682				{
4683					$oToUpdate = $aData['to_reset'];
4684					$aDisplayData[] = array(
4685						'class' => MetaModel::GetName(get_class($oToUpdate)),
4686						'object' => $oToUpdate->GetHyperLink(),
4687						'consequence' => Dict::Format('UI:Delete:AutomaticResetOf_Fields', $aData['attributes_list']),
4688					);
4689				}
4690			}
4691
4692			// Report automatic jobs
4693			//
4694			if ($oDeletionPlan->GetTargetCount() > 0)
4695			{
4696				if (count($aObjects) == 1)
4697				{
4698					$oObj = $aObjects[0];
4699					$oP->p(Dict::Format('UI:Delete:CleaningUpRefencesTo_Object', $oObj->GetName()));
4700				}
4701				else
4702				{
4703					$oP->p(Dict::Format('UI:Delete:CleaningUpRefencesTo_Several_ObjectsOf_Class', count($aObjects),
4704						MetaModel::GetName($sClass)));
4705				}
4706				$aDisplayConfig = array();
4707				$aDisplayConfig['class'] = array('label' => 'Class', 'description' => '');
4708				$aDisplayConfig['object'] = array('label' => 'Object', 'description' => '');
4709				$aDisplayConfig['consequence'] = array('label' => 'Done', 'description' => Dict::S('UI:Delete:Done+'));
4710				$oP->table($aDisplayConfig, $aDisplayData);
4711			}
4712		}
4713	}
4714
4715	/**
4716	 * Find redundancy settings that can be viewed and modified in a tab
4717	 * Settings are distributed to the corresponding link set attribute so as to be shown in the relevant tab
4718	 */
4719	protected function FindVisibleRedundancySettings()
4720	{
4721		$aRet = array();
4722		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
4723		{
4724			if ($oAttDef instanceof AttributeRedundancySettings)
4725			{
4726				if ($oAttDef->IsVisible())
4727				{
4728					$aQueryInfo = $oAttDef->GetRelationQueryData();
4729					if (isset($aQueryInfo['sAttribute']))
4730					{
4731						$oUpperAttDef = MetaModel::GetAttributeDef($aQueryInfo['sFromClass'],
4732							$aQueryInfo['sAttribute']);
4733						$oHostAttDef = $oUpperAttDef->GetMirrorLinkAttribute();
4734						if ($oHostAttDef)
4735						{
4736							$sHostAttCode = $oHostAttDef->GetCode();
4737							$aRet[$sHostAttCode][] = $oAttDef;
4738						}
4739					}
4740				}
4741			}
4742		}
4743
4744		return $aRet;
4745	}
4746
4747	/**
4748	 * Generates the javascript code handle the "watchdog" associated with the concurrent access locking mechanism
4749	 *
4750	 * @param Webpage $oPage
4751	 * @param string $sOwnershipToken
4752	 */
4753	protected function GetOwnershipJSHandler($oPage, $sOwnershipToken)
4754	{
4755		$iInterval = max(MIN_WATCHDOG_INTERVAL,
4756				MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay')) * 1000 / 2; // Minimum interval for the watchdog is MIN_WATCHDOG_INTERVAL
4757		$sJSClass = json_encode(get_class($this));
4758		$iKey = (int)$this->GetKey();
4759		$sJSToken = json_encode($sOwnershipToken);
4760		$sJSTitle = json_encode(Dict::S('UI:DisconnectedDlgTitle'));
4761		$sJSOk = json_encode(Dict::S('UI:Button:Ok'));
4762		$oPage->add_ready_script(
4763			<<<EOF
4764		window.setInterval(function() {
4765			if (window.bInSubmit || window.bInCancel) return;
4766
4767			$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'extend_lock', obj_class: $sJSClass, obj_key: $iKey, token: $sJSToken }, function(data) {
4768				if (!data.status)
4769				{
4770					if ($('.lock_owned').length == 0)
4771					{
4772						$('.ui-layout-content').prepend('<div class="header_message message_error lock_owned">'+data.message+'</div>');
4773						$('<div>'+data.popup_message+'</div>').dialog({title: $sJSTitle, modal: true, autoOpen: true, buttons:[ {text: $sJSOk, click: function() { $(this).dialog('close'); } }], close: function() { $(this).remove(); }});
4774					}
4775					$('.wizContainer form button.action:not(.cancel)').prop('disabled', true);
4776				}
4777				else if ((data.operation == 'lost') || (data.operation == 'expired'))
4778				{
4779					if ($('.lock_owned').length == 0)
4780					{
4781						$('.ui-layout-content').prepend('<div class="header_message message_error lock_owned">'+data.message+'</div>');
4782						$('<div>'+data.popup_message+'</div>').dialog({title: $sJSTitle, modal: true, autoOpen: true, buttons:[ {text: $sJSOk, click: function() { $(this).dialog('close'); } }], close: function() { $(this).remove(); }});
4783					}
4784					$('.wizContainer form button.action:not(.cancel)').prop('disabled', true);
4785				}
4786			}, 'json');
4787		}, $iInterval);
4788EOF
4789		);
4790	}
4791}
4792