the class hierarchy -> field name
+ *
+ * For example, having this :
+ *
+ *
+ * +---------------------+ +--------------------+ +--------------+
+ * | Class A | | Class B | | Class C |
+ * +---------------------+ +--------------------+ +--------------+
+ * | foo -------->c_id_friendly_name--------->friendlyname |
+ * +---------------------+ +--------------------+ +--------------+
+ *
+ *
+ * The ExternalField foo points to a magical field that is brought by c_id ExternalKey in class B.
+ *
+ *
In the normal case the foo label would be : B -> C -> friendlyname
+ * But as foo is a friendlyname its label will be the same as the one on A.b_id field
+ * This can be overrided with dict key Class:ClassA/Attribute:foo
+ *
+ * @throws \CoreException
+ * @throws \Exception
+ */
+ public function GetLabel($sDefault = null)
+ {
+ $sLabelDefaultValue = '';
+ $sLabel = parent::GetLabel($sLabelDefaultValue);
+ if ($sLabelDefaultValue !== $sLabel) {
+ return $sLabel;
+ }
+
+ if ($this->IsFriendlyName() && ($this->Get("target_attcode") === "friendlyname")) {
+ // This will be used even if we are pointing to a friendlyname in a distance > 1
+ // For example we can link to a magic friendlyname (like org_id_friendlyname)
+ // If a specific label is needed, use a Dict key !
+ // See N°2174
+ $sKeyAttCode = $this->Get("extkey_attcode");
+ $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode);
+ $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode);
+
+ return $sLabel;
+ }
+
+ $oRemoteAtt = $this->GetExtAttDef();
+ $sLabel = $oRemoteAtt->GetLabel($this->m_sCode);
+ $oKeyAtt = $this->GetKeyAttDef();
+ $sKeyLabel = $oKeyAtt->GetLabel($this->GetKeyAttCode());
+ $sLabel = "{$sKeyLabel}->{$sLabel}";
+
+ return $sLabel;
+ }
+
+ public function GetLabelForSearchField()
+ {
+ $sLabel = parent::GetLabel('');
+ if (strlen($sLabel) == 0) {
+ $sKeyAttCode = $this->Get("extkey_attcode");
+ $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode);
+ $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode);
+
+ $oRemoteAtt = $this->GetExtAttDef();
+ $sLabel .= '->' . $oRemoteAtt->GetLabel($this->m_sCode);
+ }
+
+ return $sLabel;
+ }
+
+ public function GetDescription($sDefault = null)
+ {
+ $sLabel = parent::GetDescription('');
+ if (strlen($sLabel) == 0) {
+ $oRemoteAtt = $this->GetExtAttDef();
+ $sLabel = $oRemoteAtt->GetDescription('');
+ }
+
+ return $sLabel;
+ }
+
+ public function GetHelpOnEdition($sDefault = null)
+ {
+ $sLabel = parent::GetHelpOnEdition('');
+ if (strlen($sLabel) == 0) {
+ $oRemoteAtt = $this->GetExtAttDef();
+ $sLabel = $oRemoteAtt->GetHelpOnEdition('');
+ }
+
+ return $sLabel;
+ }
+
+ public function IsExternalKey($iType = EXTKEY_RELATIVE)
+ {
+ switch ($iType) {
+ case EXTKEY_ABSOLUTE:
+ // see further
+ $oRemoteAtt = $this->GetExtAttDef();
+
+ return $oRemoteAtt->IsExternalKey($iType);
+
+ case EXTKEY_RELATIVE:
+ return false;
+
+ default:
+ throw new CoreException("Unexpected value for argument iType: '$iType'");
+ }
+ }
+
+ /**
+ * @return bool
+ * @throws \CoreException
+ */
+ public function IsFriendlyName()
+ {
+ $oRemoteAtt = $this->GetExtAttDef();
+ if ($oRemoteAtt instanceof AttributeExternalField) {
+ $bRet = $oRemoteAtt->IsFriendlyName();
+ } elseif ($oRemoteAtt instanceof AttributeFriendlyName) {
+ $bRet = true;
+ } else {
+ $bRet = false;
+ }
+
+ return $bRet;
+ }
+
+ public function GetTargetClass($iType = EXTKEY_RELATIVE)
+ {
+ return $this->GetKeyAttDef($iType)->GetTargetClass();
+ }
+
+ public static function IsExternalField()
+ {
+ return true;
+ }
+
+ public function GetKeyAttCode()
+ {
+ return $this->Get("extkey_attcode");
+ }
+
+ public function GetExtAttCode()
+ {
+ return $this->Get("target_attcode");
+ }
+
+ /**
+ * @param int $iType
+ *
+ * @return \AttributeExternalKey
+ * @throws \CoreException
+ * @throws \Exception
+ */
+ public function GetKeyAttDef($iType = EXTKEY_RELATIVE)
+ {
+ switch ($iType) {
+ case EXTKEY_ABSOLUTE:
+ // see further
+ /** @var \AttributeExternalKey $oRemoteAtt */
+ $oRemoteAtt = $this->GetExtAttDef();
+ if ($oRemoteAtt->IsExternalField()) {
+ return $oRemoteAtt->GetKeyAttDef(EXTKEY_ABSOLUTE);
+ } else {
+ if ($oRemoteAtt->IsExternalKey()) {
+ return $oRemoteAtt;
+ }
+ }
+
+ return $this->GetKeyAttDef(EXTKEY_RELATIVE); // which corresponds to the code hereafter !
+
+ case EXTKEY_RELATIVE:
+ /** @var \AttributeExternalKey $oAttDef */
+ $oAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $this->Get("extkey_attcode"));
+
+ return $oAttDef;
+
+ default:
+ throw new CoreException("Unexpected value for argument iType: '$iType'");
+ }
+ }
+
+ public function GetPrerequisiteAttributes($sClass = null)
+ {
+ return array($this->Get("extkey_attcode"));
+ }
+
+
+ /**
+ * @return \AttributeExternalField
+ * @throws \CoreException
+ * @throws \Exception
+ */
+ public function GetExtAttDef()
+ {
+ $oKeyAttDef = $this->GetKeyAttDef();
+ /** @var \AttributeExternalField $oExtAttDef */
+ $oExtAttDef = MetaModel::GetAttributeDef($oKeyAttDef->GetTargetClass(), $this->Get("target_attcode"));
+ if (!is_object($oExtAttDef)) {
+ throw new CoreException("Invalid external field " . $this->GetCode() . " in class " . $this->GetHostClass() . ". The class " . $oKeyAttDef->GetTargetClass() . " has no attribute " . $this->Get("target_attcode"));
+ }
+
+ return $oExtAttDef;
+ }
+
+ /**
+ * @return mixed
+ * @throws \CoreException
+ */
+ public function GetSQLExpr()
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetSQLExpr();
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetDefaultValue();
+ }
+
+ public function IsNullAllowed()
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->IsNullAllowed();
+ }
+
+ public static function IsScalar()
+ {
+ return true;
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetBasicFilterOperators();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetBasicFilterLooseOperator();
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetBasicFilterSQLExpr($sOpCode, $value);
+ }
+
+ public function GetNullValue()
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetNullValue();
+ }
+
+ public function IsNull($proposedValue)
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->IsNull($proposedValue);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->HasAValue($proposedValue);
+ }
+
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->MakeRealValue($proposedValue, $oHostObj);
+ }
+
+ /**
+ * @inheritDoc
+ * @since 3.1.0 N°6271 Delegate to remote attribute to ensure cascading computed values
+ */
+ public function GetSQLValues($value)
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetSQLValues($value);
+ }
+
+ public function ScalarToSQL($value)
+ {
+ // This one could be used in case of filtering only
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->ScalarToSQL($value);
+ }
+
+
+ // Do not overload GetSQLExpression here because this is handled in the joins
+ //public function GetSQLExpressions($sPrefix = '') {return array();}
+
+ // Here, we get the data...
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->FromSQLToValue($aCols, $sPrefix);
+ }
+
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetAsHTML($value, null, $bLocalize);
+ }
+
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetAsXML($value, null, $bLocalize);
+ }
+
+ public function GetAsCSV(
+ $value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ $oExtAttDef = $this->GetExtAttDef();
+
+ return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier, null, $bLocalize, $bConvertToPlainText);
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\LabelField';
+ }
+
+ /**
+ * @param \DBObject $oObject
+ * @param \Combodo\iTop\Form\Field\Field $oFormField
+ *
+ * @return null
+ * @throws \CoreException
+ */
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ // Retrieving AttDef from the remote attribute
+ $oRemoteAttDef = $this->GetExtAttDef();
+
+ if ($oFormField === null) {
+ // ExternalField's FormField are actually based on the FormField from the target attribute.
+ // Except for the AttributeExternalKey because we have no OQL and stuff
+ if ($oRemoteAttDef instanceof AttributeExternalKey) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ } else {
+ $sFormFieldClass = $oRemoteAttDef::GetFormFieldClass();
+ }
+ /** @var \Combodo\iTop\Form\Field\Field $oFormField */
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ switch ($sFormFieldClass) {
+ case '\Combodo\iTop\Form\Field\SelectField':
+ $oFormField->SetChoices($oRemoteAttDef->GetAllowedValues($oObject->ToArgsForQuery()));
+ break;
+ default:
+ break;
+ }
+ }
+ parent::MakeFormField($oObject, $oFormField);
+ if ($oFormField instanceof \Combodo\iTop\Form\Field\TextAreaField) {
+ if (method_exists($oRemoteAttDef, 'GetFormat')) {
+ /** @var \Combodo\iTop\Form\Field\TextAreaField $oFormField */
+ $oFormField->SetFormat($oRemoteAttDef->GetFormat());
+ }
+ }
+
+ // Manually setting for remote ExternalKey, otherwise, the id would be displayed.
+ if ($oRemoteAttDef instanceof AttributeExternalKey) {
+ $oFormField->SetCurrentValue($oObject->Get($this->GetCode() . '_friendlyname'));
+ }
+
+ // Readonly field because we can't update external fields
+ $oFormField->SetReadOnly(true);
+
+ return $oFormField;
+ }
+
+ public function IsPartOfFingerprint()
+ {
+ return false;
+ }
+
+ public function GetFormat()
+ {
+ $oRemoteAttDef = $this->GetExtAttDef();
+ if (method_exists($oRemoteAttDef, 'GetFormat')) {
+ /** @var \Combodo\iTop\Form\Field\TextAreaField $oFormField */
+ return $oRemoteAttDef->GetFormat();
+ }
+ return 'text';
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeExternalKey.php b/sources/Core/AttributeDefinition/AttributeExternalKey.php
new file mode 100644
index 0000000000..3be7a2989e
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeExternalKey.php
@@ -0,0 +1,335 @@
+GetFinalAttDef();
+ $sTargetClass = $oRemoteAtt->GetTargetClass();
+ if (MetaModel::IsHierarchicalClass($sTargetClass)) {
+ return self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY;
+ }
+
+ return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY;
+ } catch (CoreException $e) {
+ }
+
+ return self::SEARCH_WIDGET_TYPE_RAW;
+ }
+
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array("targetclass", "is_null_allowed", "on_target_delete"));
+ }
+
+ public function GetEditClass()
+ {
+ return "ExtKey";
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "INT(11)" . ($bFullSpec ? " DEFAULT 0" : "");
+ }
+
+ public function RequiresIndex()
+ {
+ return true;
+ }
+
+ public function IsExternalKey($iType = EXTKEY_RELATIVE)
+ {
+ return true;
+ }
+
+ public function GetTargetClass($iType = EXTKEY_RELATIVE)
+ {
+ return $this->Get("targetclass");
+ }
+
+ public function GetKeyAttDef($iType = EXTKEY_RELATIVE)
+ {
+ return $this;
+ }
+
+ public function GetKeyAttCode()
+ {
+ return $this->GetCode();
+ }
+
+ public function GetDisplayStyle()
+ {
+ return $this->GetOptional('display_style', 'select');
+ }
+
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return 0;
+ }
+
+ public function IsNullAllowed()
+ {
+ if (MetaModel::GetConfig()->Get('disable_mandatory_ext_keys')) {
+ return true;
+ }
+
+ return $this->Get("is_null_allowed");
+ }
+
+
+ public function GetBasicFilterOperators()
+ {
+ return parent::GetBasicFilterOperators();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return parent::GetBasicFilterLooseOperator();
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ return parent::GetBasicFilterSQLExpr($sOpCode, $value);
+ }
+
+ // overloaded here so that an ext key always have the answer to
+ // "what are your possible values?"
+ public function GetValuesDef()
+ {
+ $oValSetDef = $this->Get("allowed_values");
+ if (!$oValSetDef) {
+ // Let's propose every existing value
+ $oValSetDef = new ValueSetObjects('SELECT ' . $this->GetTargetClass());
+ }
+
+ return $oValSetDef;
+ }
+
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ //throw new Exception("GetAllowedValues on ext key has been deprecated");
+ try {
+ return parent::GetAllowedValues($aArgs, $sContains);
+ } catch (Exception $e) {
+ // Some required arguments could not be found, enlarge to any existing value
+ $oValSetDef = new ValueSetObjects('SELECT ' . $this->GetTargetClass());
+
+ return $oValSetDef->GetValues($aArgs, $sContains);
+ }
+ }
+
+ public function GetAllowedValuesForSelect($aArgs = array(), $sContains = '')
+ {
+ //$this->GetValuesDef();
+ $oValSetDef = new ValueSetObjects('SELECT ' . $this->GetTargetClass());
+ return $oValSetDef->GetValuesForAutocomplete($aArgs, $sContains);
+ }
+
+
+ public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null)
+ {
+ $oValSetDef = $this->GetValuesDef();
+ $oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue);
+
+ return $oSet;
+ }
+
+ public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null)
+ {
+ return DBObjectSearch::FromOQL($this->GetValuesDef()->GetFilterExpression());
+ }
+
+ public function GetDeletionPropagationOption()
+ {
+ return $this->Get("on_target_delete");
+ }
+
+ public function GetNullValue()
+ {
+ return 0;
+ }
+
+ public function IsNull($proposedValue)
+ {
+ return ($proposedValue == 0);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ return ((int)$proposedValue) !== 0;
+ }
+
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (is_null($proposedValue)) {
+ return 0;
+ }
+ if ($proposedValue === '') {
+ return 0;
+ }
+ if (MetaModel::IsValidObject($proposedValue)) {
+ return $proposedValue->GetKey();
+ }
+
+ return (int)$proposedValue;
+ }
+
+ /** @inheritdoc @since 3.1 */
+ public function WriteExternalValues(DBObject $oHostObject): void
+ {
+ $sTargetKey = $oHostObject->Get($this->GetCode());
+ $oFilter = DBSearch::FromOQL('SELECT `' . TemporaryObjectDescriptor::class . '` WHERE item_class=:class AND item_id=:id');
+ $oSet = new DBObjectSet($oFilter, [], ['class' => $this->GetTargetClass(), 'id' => $sTargetKey]);
+ while ($oTemporaryObjectDescriptor = $oSet->Fetch()) {
+ $oTemporaryObjectDescriptor->Set('host_class', get_class($oHostObject));
+ $oTemporaryObjectDescriptor->Set('host_id', $oHostObject->GetKey());
+ $oTemporaryObjectDescriptor->Set('host_att_code', $this->GetCode());
+ $oTemporaryObjectDescriptor->DBUpdate();
+ }
+ }
+
+ public function GetMaximumComboLength()
+ {
+ return $this->GetOptional('max_combo_length', MetaModel::GetConfig()->Get('max_combo_length'));
+ }
+
+ public function GetMinAutoCompleteChars()
+ {
+ return $this->GetOptional('min_autocomplete_chars', MetaModel::GetConfig()->Get('min_autocomplete_chars'));
+ }
+
+ /**
+ * @return int
+ * @since 3.0.0
+ */
+ public function GetMaxAutoCompleteResults(): int
+ {
+ return MetaModel::GetConfig()->Get('max_autocomplete_results');
+ }
+
+ public function AllowTargetCreation()
+ {
+ return $this->GetOptional('allow_target_creation', MetaModel::GetConfig()->Get('allow_target_creation'));
+ }
+
+ /**
+ * Find the corresponding "link" attribute on the target class, if any
+ *
+ * @return null | AttributeDefinition
+ * @throws \CoreException
+ */
+ public function GetMirrorLinkAttribute()
+ {
+ $oRet = null;
+ $sRemoteClass = $this->GetTargetClass();
+ foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) {
+ if (!$oRemoteAttDef->IsLinkSet()) {
+ continue;
+ }
+ if (!is_subclass_of($this->GetHostClass(),
+ $oRemoteAttDef->GetLinkedClass()) && $oRemoteAttDef->GetLinkedClass() != $this->GetHostClass()) {
+ continue;
+ }
+ if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetCode()) {
+ continue;
+ }
+ $oRet = $oRemoteAttDef;
+ break;
+ }
+
+ return $oRet;
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\SelectObjectField';
+ }
+
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ /** @var \Combodo\iTop\Form\Field\Field $oFormField */
+ if ($oFormField === null) {
+ // Later : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ }
+
+ // Setting params
+ $oFormField->SetMaximumComboLength($this->GetMaximumComboLength());
+ $oFormField->SetMinAutoCompleteChars($this->GetMinAutoCompleteChars());
+ $oFormField->SetMaxAutoCompleteResults($this->GetMaxAutoCompleteResults());
+ $oFormField->SetHierarchical(MetaModel::IsHierarchicalClass($this->GetTargetClass()));
+ // Setting choices regarding the field dependencies
+ $aFieldDependencies = $this->GetPrerequisiteAttributes();
+ if (!empty($aFieldDependencies)) {
+ $oTmpAttDef = $this;
+ $oTmpField = $oFormField;
+ $oFormField->SetOnFinalizeCallback(function () use ($oTmpField, $oTmpAttDef, $oObject) {
+ /** @var $oTmpField \Combodo\iTop\Form\Field\Field */
+ /** @var $oTmpAttDef \AttributeDefinition */
+ /** @var $oObject \DBObject */
+
+ // We set search object only if it has not already been set (overrided)
+ if ($oTmpField->GetSearch() === null) {
+ $oSearch = DBSearch::FromOQL($oTmpAttDef->GetValuesDef()->GetFilterExpression());
+ $oSearch->SetInternalParams(array('this' => $oObject));
+ $oTmpField->SetSearch($oSearch);
+ }
+ });
+ } else {
+ $oSearch = DBSearch::FromOQL($this->GetValuesDef()->GetFilterExpression());
+ $oSearch->SetInternalParams(array('this' => $oObject));
+ $oFormField->SetSearch($oSearch);
+ }
+
+ parent::MakeFormField($oObject, $oFormField);
+
+ return $oFormField;
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (!is_null($oHostObject)) {
+ return $oHostObject->GetAsHTML($this->GetCode(), $oHostObject);
+ }
+
+ return DBObject::MakeHyperLink($this->GetTargetClass(), $sValue);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeFinalClass.php b/sources/Core/AttributeDefinition/AttributeFinalClass.php
new file mode 100644
index 0000000000..c104e4f139
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeFinalClass.php
@@ -0,0 +1,197 @@
+m_sCode = $sCode;
+ $aParams["allowed_values"] = null;
+ parent::__construct($sCode, $aParams);
+
+ $this->m_sValue = $this->Get("default_value");
+ }
+
+ public function IsWritable()
+ {
+ return false;
+ }
+
+ public function IsMagic()
+ {
+ return true;
+ }
+
+ public function RequiresIndex()
+ {
+ return true;
+ }
+
+ public function SetFixedValue($sValue)
+ {
+ $this->m_sValue = $sValue;
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return $this->m_sValue;
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (empty($sValue)) {
+ return '';
+ }
+ if ($bLocalize) {
+ return MetaModel::GetName($sValue);
+ } else {
+ return $sValue;
+ }
+ }
+
+ /**
+ * An enum can be localized
+ *
+ * @param string $sProposedValue
+ * @param bool $bLocalizedValue
+ * @param string $sSepItem
+ * @param string $sSepAttribute
+ * @param string $sSepValue
+ * @param string $sAttributeQualifier
+ *
+ * @return mixed|null|string
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function MakeValueFromString(
+ $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
+ $sAttributeQualifier = null
+ )
+ {
+ if ($bLocalizedValue) {
+ // Lookup for the value matching the input
+ //
+ $sFoundValue = null;
+ $aRawValues = self::GetAllowedValues();
+ if (!is_null($aRawValues)) {
+ foreach ($aRawValues as $sKey => $sValue) {
+ if ($sProposedValue == $sValue) {
+ $sFoundValue = $sKey;
+ break;
+ }
+ }
+ }
+ if (is_null($sFoundValue)) {
+ return null;
+ }
+
+ return $this->MakeRealValue($sFoundValue, null);
+ } else {
+ return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue,
+ $sAttributeQualifier);
+ }
+ }
+
+
+ // Because this is sometimes used to get a localized/string version of an attribute...
+ public function GetEditValue($sValue, $oHostObj = null)
+ {
+ if (empty($sValue)) {
+ return '';
+ }
+
+ return MetaModel::GetName($sValue);
+ }
+
+ public function GetForJSON($value)
+ {
+ // JSON values are NOT localized
+ return $value;
+ }
+
+ /**
+ * @param $value
+ * @param string $sSeparator
+ * @param string $sTextQualifier
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ * @param bool $bConvertToPlainText
+ *
+ * @return string
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ */
+ public function GetAsCSV(
+ $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ if ($bLocalize && $value != '') {
+ $sRawValue = MetaModel::GetName($value);
+ } else {
+ $sRawValue = $value;
+ }
+
+ return parent::GetAsCSV($sRawValue, $sSeparator, $sTextQualifier, null, false, $bConvertToPlainText);
+ }
+
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if (empty($value)) {
+ return '';
+ }
+ if ($bLocalize) {
+ $sRawValue = MetaModel::GetName($value);
+ } else {
+ $sRawValue = $value;
+ }
+
+ return Str::pure2xml($sRawValue);
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return '=';
+ }
+
+ public function GetValueLabel($sValue)
+ {
+ if (empty($sValue)) {
+ return '';
+ }
+
+ return MetaModel::GetName($sValue);
+ }
+
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ $aRawValues = MetaModel::EnumChildClasses($this->GetHostClass(), ENUM_CHILD_CLASSES_ALL);
+ $aLocalizedValues = array();
+ foreach ($aRawValues as $sClass) {
+ $aLocalizedValues[$sClass] = MetaModel::GetName($sClass);
+ }
+
+ return $aLocalizedValues;
+ }
+
+ /**
+ * @return bool
+ * @since 2.7.0 N°2272 OQL perf finalclass in all intermediary tables
+ */
+ public function CopyOnAllTables()
+ {
+ $sClass = self::GetHostClass();
+ if (MetaModel::IsLeafClass($sClass)) {
+ // Leaf class, no finalclass
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeFriendlyName.php b/sources/Core/AttributeDefinition/AttributeFriendlyName.php
new file mode 100644
index 0000000000..e4cfb7a396
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeFriendlyName.php
@@ -0,0 +1,199 @@
+m_sCode = $sCode;
+ $aParams = array();
+ $aParams["default_value"] = '';
+ parent::__construct($sCode, $aParams);
+
+ $this->m_sValue = $this->Get("default_value");
+ }
+
+ public function GetEditClass()
+ {
+ return "";
+ }
+
+ public function GetValuesDef()
+ {
+ return null;
+ }
+
+ public function GetPrerequisiteAttributes($sClass = null)
+ {
+ // Code duplicated with AttributeObsolescenceFlag
+ $aAttributes = $this->GetOptional("depends_on", array());
+ $oExpression = $this->GetOQLExpression();
+ foreach ($oExpression->ListRequiredFields() as $sAttCode) {
+ if (!in_array($sAttCode, $aAttributes)) {
+ $aAttributes[] = $sAttCode;
+ }
+ }
+
+ return $aAttributes;
+ }
+
+ public static function IsScalar()
+ {
+ return true;
+ }
+
+ public function IsNullAllowed()
+ {
+ return false;
+ }
+
+ public function GetSQLExpressions($sPrefix = '')
+ {
+ if ($sPrefix == '') {
+ $sPrefix = $this->GetCode(); // Warning AttributeComputedFieldVoid does not have any sql property
+ }
+
+ return array('' => $sPrefix);
+ }
+
+ public static function IsBasedOnOQLExpression()
+ {
+ return true;
+ }
+
+ public function GetOQLExpression()
+ {
+ return MetaModel::GetNameExpression($this->GetHostClass());
+ }
+
+ public function GetLabel($sDefault = null)
+ {
+ $sLabel = parent::GetLabel('');
+ if (strlen($sLabel) == 0) {
+ $sLabel = Dict::S('Core:FriendlyName-Label');
+ }
+
+ return $sLabel;
+ }
+
+ public function GetDescription($sDefault = null)
+ {
+ $sLabel = parent::GetDescription('');
+ if (strlen($sLabel) == 0) {
+ $sLabel = Dict::S('Core:FriendlyName-Description');
+ }
+
+ return $sLabel;
+ }
+
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ $sValue = $aCols[$sPrefix];
+
+ return $sValue;
+ }
+
+ public function IsWritable()
+ {
+ return false;
+ }
+
+ public function IsMagic()
+ {
+ return true;
+ }
+
+ public static function IsBasedOnDBColumns()
+ {
+ return false;
+ }
+
+ public function SetFixedValue($sValue)
+ {
+ $this->m_sValue = $sValue;
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return $this->m_sValue;
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ return Str::pure2html((string)$sValue);
+ }
+
+ public function GetAsCSV(
+ $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ $sFrom = array("\r\n", $sTextQualifier);
+ $sTo = array("\n", $sTextQualifier . $sTextQualifier);
+ $sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
+
+ return $sTextQualifier . $sEscaped . $sTextQualifier;
+ }
+
+ static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\StringField';
+ }
+
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ if ($oFormField === null) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ }
+ $oFormField->SetReadOnly(true);
+ parent::MakeFormField($oObject, $oFormField);
+
+ return $oFormField;
+ }
+
+ // Do not display friendly names in the history of change
+ public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null)
+ {
+ return '';
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array("=" => "equals", "!=" => "differs from");
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return "Contains";
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ $sQValue = CMDBSource::Quote($value);
+ switch ($sOpCode) {
+ case '=':
+ case '!=':
+ return $this->GetSQLExpr() . " $sOpCode $sQValue";
+ case 'Contains':
+ return $this->GetSQLExpr() . " LIKE " . CMDBSource::Quote("%$value%");
+ case 'NotLike':
+ return $this->GetSQLExpr() . " NOT LIKE $sQValue";
+ case 'Like':
+ default:
+ return $this->GetSQLExpr() . " LIKE $sQValue";
+ }
+ }
+
+ public function IsPartOfFingerprint()
+ {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeHTML.php b/sources/Core/AttributeDefinition/AttributeHTML.php
new file mode 100644
index 0000000000..953e7c9a50
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeHTML.php
@@ -0,0 +1,51 @@
+ ?), containing HTML code, to an attribute
+ *
+ * @package iTopORM
+ */
+class AttributeHTML extends AttributeLongText
+{
+ /**
+ * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
+ *
+ * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited
+ * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9
+ *
+ * @param string $sCode
+ * @param array $aParams
+ *
+ * @throws \Exception
+ * @noinspection SenselessProxyMethodInspection
+ */
+ public function __construct($sCode, $aParams)
+ {
+ parent::__construct($sCode, $aParams);
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ $aColumns = array();
+ $aColumns[$this->Get('sql')] = $this->GetSQLCol();
+ if ($this->GetOptional('format', null) != null) {
+ // Add the extra column only if the property 'format' is specified for the attribute
+ $aColumns[$this->Get('sql') . '_format'] = "ENUM('text','html')";
+ if ($bFullSpec) {
+ $aColumns[$this->Get('sql') . '_format'] .= " DEFAULT 'html'"; // default 'html' is for migrating old records
+ }
+ }
+
+ return $aColumns;
+ }
+
+ /**
+ * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup)
+ *
+ * @return string
+ */
+ public function GetFormat()
+ {
+ return $this->GetOptional('format', 'html'); // Defaults to HTML
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeHierarchicalKey.php b/sources/Core/AttributeDefinition/AttributeHierarchicalKey.php
new file mode 100644
index 0000000000..fe4d5fe6a0
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeHierarchicalKey.php
@@ -0,0 +1,186 @@
+m_sTargetClass)) {
+ $this->m_sTargetClass = $sHostClass;
+ }
+ parent::SetHostClass($sHostClass);
+ }
+
+ public static function IsHierarchicalKey()
+ {
+ return true;
+ }
+
+ public function GetTargetClass($iType = EXTKEY_RELATIVE)
+ {
+ return $this->m_sTargetClass;
+ }
+
+ public function GetKeyAttDef($iType = EXTKEY_RELATIVE)
+ {
+ return $this;
+ }
+
+ public function GetKeyAttCode()
+ {
+ return $this->GetCode();
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return parent::GetBasicFilterOperators();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return parent::GetBasicFilterLooseOperator();
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ $aColumns = array();
+ $aColumns[$this->GetCode()] = 'INT(11)' . ($bFullSpec ? ' DEFAULT 0' : '');
+ $aColumns[$this->GetSQLLeft()] = 'INT(11)' . ($bFullSpec ? ' DEFAULT 0' : '');
+ $aColumns[$this->GetSQLRight()] = 'INT(11)' . ($bFullSpec ? ' DEFAULT 0' : '');
+
+ return $aColumns;
+ }
+
+ public function GetSQLRight()
+ {
+ return $this->GetCode() . '_right';
+ }
+
+ public function GetSQLLeft()
+ {
+ return $this->GetCode() . '_left';
+ }
+
+ public function GetSQLValues($value)
+ {
+ if (!is_array($value)) {
+ $aValues[$this->GetCode()] = $value;
+ } else {
+ $aValues = array();
+ $aValues[$this->GetCode()] = $value[$this->GetCode()];
+ $aValues[$this->GetSQLRight()] = $value[$this->GetSQLRight()];
+ $aValues[$this->GetSQLLeft()] = $value[$this->GetSQLLeft()];
+ }
+
+ return $aValues;
+ }
+
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ $oFilter = $this->GetHierachicalFilter($aArgs, $sContains);
+ if ($oFilter) {
+ $oValSetDef = $this->GetValuesDef();
+ $oValSetDef->SetCondition($oFilter);
+
+ return $oValSetDef->GetValues($aArgs, $sContains);
+ } else {
+ return parent::GetAllowedValues($aArgs, $sContains);
+ }
+ }
+
+ public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null)
+ {
+ $oValSetDef = $this->GetValuesDef();
+ $oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue);
+ if ($oFilter) {
+ $oValSetDef->SetCondition($oFilter);
+ }
+ $oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue);
+
+ return $oSet;
+ }
+
+ public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null)
+ {
+ $oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue);
+ if ($oFilter) {
+ return $oFilter;
+ }
+
+ return parent::GetAllowedValuesAsFilter($aArgs, $sContains, $iAdditionalValue);
+ }
+
+ private function GetHierachicalFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null)
+ {
+ if (array_key_exists('this', $aArgs)) {
+ // Hierarchical keys have one more constraint: the "parent value" cannot be
+ // "under" themselves
+ $iRootId = $aArgs['this']->GetKey();
+ if ($iRootId > 0) // ignore objects that do no exist in the database...
+ {
+ $sClass = $this->m_sTargetClass;
+
+ return DBObjectSearch::FromOQL("SELECT $sClass AS node JOIN $sClass AS root ON node." . $this->GetCode() . " NOT BELOW root.id WHERE root.id = $iRootId");
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Find the corresponding "link" attribute on the target class, if any
+ *
+ * @return null | AttributeDefinition
+ */
+ public function GetMirrorLinkAttribute()
+ {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeIPAddress.php b/sources/Core/AttributeDefinition/AttributeIPAddress.php
new file mode 100644
index 0000000000..e797f51085
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeIPAddress.php
@@ -0,0 +1,39 @@
+GetCode() . '`)');
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeImage.php b/sources/Core/AttributeDefinition/AttributeImage.php
new file mode 100644
index 0000000000..dff7f02044
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeImage.php
@@ -0,0 +1,194 @@
+IsEmpty())
+ && ($oDoc->GetMimeType() === 'image/svg+xml')) {
+ $sCleanSvg = HTMLSanitizer::Sanitize($oDoc->GetData(), 'svg_sanitizer');
+ $oDoc = new ormDocument($sCleanSvg, $oDoc->GetMimeType(), $oDoc->GetFileName());
+ }
+
+ // The validation of the MIME Type is done by CheckFormat below
+ return $oDoc;
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return new ormDocument('', '', '');
+ }
+
+ /**
+ * Check that the supplied ormDocument actually contains an image
+ * {@inheritDoc}
+ *
+ * @see AttributeDefinition::CheckFormat()
+ */
+ public function CheckFormat($value)
+ {
+ if ($value instanceof ormDocument && !$value->IsEmpty()) {
+ return ($value->GetMainMimeType() == 'image');
+ }
+
+ return true;
+ }
+
+ /**
+ * @param \ormDocument $value
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string
+ *
+ * @see edit_image.js for JS generated markup in form edition
+ */
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ $sRet = '';
+ $bIsCustomImage = false;
+
+ $iMaxWidth = $this->Get('display_max_width');
+ $sMaxWidthPx = $iMaxWidth . 'px';
+ $iMaxHeight = $this->Get('display_max_height');
+ $sMaxHeightPx = $iMaxHeight . 'px';
+
+ $sDefaultImageUrl = $this->Get('default_image');
+ if ($sDefaultImageUrl !== null) {
+ $sRet = $this->GetHtmlForImageUrl($sDefaultImageUrl, $sMaxWidthPx, $sMaxHeightPx);
+ }
+
+ $sCustomImageUrl = $this->GetAttributeImageFileUrl($value, $oHostObject);
+ if ($sCustomImageUrl !== null) {
+ $bIsCustomImage = true;
+ $sRet = $this->GetHtmlForImageUrl($sCustomImageUrl, $sMaxWidthPx, $sMaxHeightPx);
+ }
+
+ $sCssClasses = 'ibo-input-image--image-view attribute-image';
+ $sCssClasses .= ' ' . (($bIsCustomImage) ? 'attribute-image-custom' : 'attribute-image-default');
+
+ // Important: If you change this, mind updating edit_image.js as well
+ return '
' . $sRet . '
';
+ }
+
+ /**
+ * @param string $sUrl
+ * @param int $iMaxWidthPx
+ * @param int $iMaxHeightPx
+ *
+ * @return string
+ *
+ * @since 2.6.0 new private method
+ * @since 2.7.0 change visibility to protected
+ */
+ protected function GetHtmlForImageUrl($sUrl, $iMaxWidthPx, $iMaxHeightPx)
+ {
+ return '
';
+ }
+
+ /**
+ * @param \ormDocument $value
+ * @param \DBObject $oHostObject
+ *
+ * @return null|string
+ *
+ * @since 2.6.0 new private method
+ * @since 2.7.0 change visibility to protected
+ */
+ protected function GetAttributeImageFileUrl($value, $oHostObject)
+ {
+ if (!is_object($value)) {
+ return null;
+ }
+ if ($value->IsEmpty()) {
+ return null;
+ }
+
+ $bExistingImageModified = ($oHostObject->IsModified() && (array_key_exists($this->GetCode(), $oHostObject->ListChanges())));
+ if ($oHostObject->IsNew() || ($bExistingImageModified)) {
+ // If the object is modified (or not yet stored in the database) we must serve the content of the image directly inline
+ // otherwise (if we just give an URL) the browser will be given the wrong content... and may cache it
+ return 'data:' . $value->GetMimeType() . ';base64,' . base64_encode($value->GetData());
+ }
+
+ return $value->GetDisplayURL(get_class($oHostObject), $oHostObject->GetKey(), $this->GetCode());
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\ImageField';
+ }
+
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ if ($oFormField === null) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ }
+
+ parent::MakeFormField($oObject, $oFormField);
+
+ // Generating urls
+ $value = $oObject->Get($this->GetCode());
+ if (is_object($value) && !$value->IsEmpty()) {
+ $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode()));
+ $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode()));
+ } else {
+ $oDefaultImage = $this->Get('default_image');
+ if (is_object($oDefaultImage) && !$oDefaultImage->IsEmpty()) {
+ $oFormField->SetDownloadUrl($oDefaultImage);
+ $oFormField->SetDisplayUrl($oDefaultImage);
+ }
+ }
+
+ return $oFormField;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeInteger.php b/sources/Core/AttributeDefinition/AttributeInteger.php
new file mode 100644
index 0000000000..270879a826
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeInteger.php
@@ -0,0 +1,138 @@
+GetSQLColSpec() : '');
+ }
+
+ public function GetValidationPattern()
+ {
+ return "^[0-9]+$";
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array(
+ "!=" => "differs from",
+ "=" => "equals",
+ ">" => "greater (strict) than",
+ ">=" => "greater than",
+ "<" => "less (strict) than",
+ "<=" => "less than",
+ "in" => "in"
+ );
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ // Unless we implement an "equals approximately..." or "same order of magnitude"
+ return "=";
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ $sQValue = CMDBSource::Quote($value);
+ switch ($sOpCode) {
+ case '!=':
+ return $this->GetSQLExpr() . " != $sQValue";
+ break;
+ case '>':
+ return $this->GetSQLExpr() . " > $sQValue";
+ break;
+ case '>=':
+ return $this->GetSQLExpr() . " >= $sQValue";
+ break;
+ case '<':
+ return $this->GetSQLExpr() . " < $sQValue";
+ break;
+ case '<=':
+ return $this->GetSQLExpr() . " <= $sQValue";
+ break;
+ case 'in':
+ if (!is_array($value)) {
+ throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')");
+ }
+
+ return $this->GetSQLExpr() . " IN ('" . implode("', '", $value) . "')";
+ break;
+
+ case '=':
+ default:
+ return $this->GetSQLExpr() . " = \"$value\"";
+ }
+ }
+
+ public function GetNullValue()
+ {
+ return null;
+ }
+
+ public function IsNull($proposedValue)
+ {
+ return is_null($proposedValue);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ return utils::IsNotNullOrEmptyString($proposedValue);
+ }
+
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (is_null($proposedValue)) {
+ return null;
+ }
+ if ($proposedValue === '') {
+ return null;
+ } // 0 is transformed into '' !
+
+ return (int)$proposedValue;
+ }
+
+ public function ScalarToSQL($value)
+ {
+ assert(is_numeric($value) || is_null($value));
+
+ return $value; // supposed to be an int
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeLinkedSet.php b/sources/Core/AttributeDefinition/AttributeLinkedSet.php
new file mode 100644
index 0000000000..6921e5116c
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeLinkedSet.php
@@ -0,0 +1,910 @@
+aCSSClasses[] = 'attribute-set';
+ }
+
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(),
+ array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max"));
+ }
+
+ public function GetEditClass()
+ {
+ return "LinkedSet";
+ }
+
+ /** @inheritDoc */
+ public static function IsBulkModifyCompatible(): bool
+ {
+ return false;
+ }
+
+ public function IsWritable()
+ {
+ return true;
+ }
+
+ public static function IsLinkSet()
+ {
+ return true;
+ }
+
+ public function IsIndirect()
+ {
+ return false;
+ }
+
+ public function GetValuesDef()
+ {
+ $oValSetDef = $this->Get("allowed_values");
+ if (!$oValSetDef) {
+ // Let's propose every existing value
+ $oValSetDef = new ValueSetObjects('SELECT ' . \Combodo\iTop\Service\Links\LinkSetModel::GetTargetClass($this));
+ }
+
+ return $oValSetDef;
+ }
+
+ public function GetEditValue($value, $oHostObj = null)
+ {
+ /** @var ormLinkSet $value * */
+ if ($value->Count() === 0) {
+ return '';
+ }
+
+ /** Return linked objects key as string **/
+ $aValues = $value->GetValues();
+
+ return implode(' ', $aValues);
+ }
+
+ public function GetPrerequisiteAttributes($sClass = null)
+ {
+ return $this->Get("depends_on");
+ }
+
+ /**
+ * @param \DBObject|null $oHostObject
+ *
+ * @return \ormLinkSet
+ *
+ * @throws \Exception
+ * @throws \CoreException
+ * @throws \CoreWarning
+ */
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ if ($oHostObject === null) {
+ return null;
+ }
+
+ $sLinkClass = $this->GetLinkedClass();
+ $sExtKeyToMe = $this->GetExtKeyToMe();
+
+ // The class to target is not the current class, because if this is a derived class,
+ // it may differ from the target class, then things start to become confusing
+ /** @var \AttributeExternalKey $oRemoteExtKeyAtt */
+ $oRemoteExtKeyAtt = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToMe);
+ $sMyClass = $oRemoteExtKeyAtt->GetTargetClass();
+
+ $oMyselfSearch = new DBObjectSearch($sMyClass);
+ if ($oHostObject !== null) {
+ $oMyselfSearch->AddCondition('id', $oHostObject->GetKey(), '=');
+ }
+
+ $oLinkSearch = new DBObjectSearch($sLinkClass);
+ $oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe);
+ if ($this->IsIndirect()) {
+ // Join the remote class so that the archive flag will be taken into account
+ /** @var \AttributeLinkedSetIndirect $this */
+ $sExtKeyToRemote = $this->GetExtKeyToRemote();
+ /** @var \AttributeExternalKey $oExtKeyToRemote */
+ $oExtKeyToRemote = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToRemote);
+ $sRemoteClass = $oExtKeyToRemote->GetTargetClass();
+ if (MetaModel::IsArchivable($sRemoteClass)) {
+ $oRemoteSearch = new DBObjectSearch($sRemoteClass);
+ /** @var \AttributeLinkedSetIndirect $this */
+ $oLinkSearch->AddCondition_PointingTo($oRemoteSearch, $this->GetExtKeyToRemote());
+ }
+ }
+ $oLinks = new DBObjectSet($oLinkSearch);
+ $oLinkSet = new ormLinkSet($this->GetHostClass(), $this->GetCode(), $oLinks);
+
+ return $oLinkSet;
+ }
+
+ public function GetTrackingLevel()
+ {
+ return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_default'));
+ }
+
+ /**
+ * @return string see LINKSET_EDITMODE_* constants
+ */
+ public function GetEditMode()
+ {
+ return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS);
+ }
+
+ /**
+ * @return int see LINKSET_EDITWHEN_* constants
+ * @since 3.1.1 3.2.0 N°6385
+ */
+ public function GetEditWhen(): int
+ {
+ return $this->GetOptional('edit_when', LINKSET_EDITWHEN_ALWAYS);
+ }
+
+ /**
+ * @return string see LINKSET_DISPLAY_STYLE_* constants
+ * @since 3.1.0 N°3190
+ */
+ public function GetDisplayStyle()
+ {
+ $sDisplayStyle = $this->GetOptional('display_style', LINKSET_DISPLAY_STYLE_TAB);
+ if ($sDisplayStyle === '') {
+ $sDisplayStyle = LINKSET_DISPLAY_STYLE_TAB;
+ }
+
+ return $sDisplayStyle;
+ }
+
+ /**
+ * Indicates if the current Attribute has constraints (php constraints or datamodel constraints)
+ * @return bool true if Attribute has constraints
+ * @since 3.1.0 N°6228
+ */
+ public function HasPHPConstraint(): bool
+ {
+ return $this->GetOptional('with_php_constraint', false);
+ }
+
+ /**
+ * @return bool true if Attribute has computation (DB_LINKS_CHANGED event propagation, `with_php_computation` attribute xml property), false otherwise
+ * @since 3.1.1 3.2.0 N°6228
+ */
+ public function HasPHPComputation(): bool
+ {
+ return $this->GetOptional('with_php_computation', false);
+ }
+
+ public function GetLinkedClass()
+ {
+ return $this->Get('linked_class');
+ }
+
+ public function GetExtKeyToMe()
+ {
+ return $this->Get('ext_key_to_me');
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return '';
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ return '';
+ }
+
+ /** @inheritDoc * */
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if ($this->GetDisplayStyle() === LINKSET_DISPLAY_STYLE_TAB) {
+ return $this->GetAsHTMLForTab($sValue, $oHostObject, $bLocalize);
+ } else {
+ return $this->GetAsHTMLForProperty($sValue, $oHostObject, $bLocalize);
+ }
+ }
+
+ public function GetAsHTMLForTab($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (is_object($sValue) && ($sValue instanceof ormLinkSet)) {
+ $sValue->Rewind();
+ $aItems = array();
+ while ($oObj = $sValue->Fetch()) {
+ // Show only relevant information (hide the external key to the current object)
+ $aAttributes = array();
+ foreach (MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef) {
+ if ($sAttCode == $this->GetExtKeyToMe()) {
+ continue;
+ }
+ if ($oAttDef->IsExternalField()) {
+ continue;
+ }
+ $sAttValue = $oObj->GetAsHTML($sAttCode);
+ if (strlen($sAttValue) > 0) {
+ $aAttributes[] = $sAttValue;
+ }
+ }
+ $sAttributes = implode(', ', $aAttributes);
+ $aItems[] = $sAttributes;
+ }
+
+ return implode('
', $aItems);
+ }
+
+ return null;
+ }
+
+ public function GetAsHTMLForProperty($sValue, $oHostObject = null, $bLocalize = true): string
+ {
+ try {
+
+ /** @var ormLinkSet $sValue */
+ if (is_null($sValue) || $sValue->Count() === 0) {
+ return '';
+ }
+
+ $oLinkSetBlock = new \Combodo\iTop\Application\UI\Links\Set\BlockLinkSetDisplayAsProperty($this->GetCode(), $this, $sValue);
+
+ return \Combodo\iTop\Renderer\Console\ConsoleBlockRenderer::RenderBlockTemplates($oLinkSetBlock);
+ } catch (Exception $e) {
+ $sMessage = "Error while displaying attribute {$this->GetCode()}";
+ IssueLog::Error($sMessage, IssueLog::CHANNEL_DEFAULT, [
+ 'host_object_class' => $this->GetHostClass(),
+ 'host_object_key' => $oHostObject->GetKey(),
+ 'attribute' => $this->GetCode(),
+ ]);
+
+ return $sMessage;
+ }
+ }
+
+ /**
+ * @param string $sValue
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string
+ *
+ * @throws \CoreException
+ */
+ public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (is_object($sValue) && ($sValue instanceof ormLinkSet)) {
+ $sValue->Rewind();
+ $sRes = "\n";
+ while ($oObj = $sValue->Fetch()) {
+ $sObjClass = get_class($oObj);
+ $sRes .= "<$sObjClass id=\"" . $oObj->GetKey() . "\">\n";
+ // Show only relevant information (hide the external key to the current object)
+ foreach (MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) {
+ if ($sAttCode == 'finalclass') {
+ if ($sObjClass == $this->GetLinkedClass()) {
+ // Simplify the output if the exact class could be determined implicitely
+ continue;
+ }
+ }
+ if ($sAttCode == $this->GetExtKeyToMe()) {
+ continue;
+ }
+ if ($oAttDef->IsExternalField()) {
+ /** @var \AttributeExternalField $oAttDef */
+ if ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe()) {
+ continue;
+ }
+ /** @var AttributeExternalField $oAttDef */
+ if ($oAttDef->IsFriendlyName()) {
+ continue;
+ }
+ }
+ if ($oAttDef instanceof AttributeFriendlyName) {
+ continue;
+ }
+ if (!$oAttDef->IsScalar()) {
+ continue;
+ }
+ $sAttValue = $oObj->GetAsXML($sAttCode, $bLocalize);
+ $sRes .= "<$sAttCode>$sAttValue$sAttCode>\n";
+ }
+ $sRes .= "$sObjClass>\n";
+ }
+ $sRes .= "\n";
+ } else {
+ $sRes = '';
+ }
+
+ return $sRes;
+ }
+
+ /**
+ * @param $sValue
+ * @param string $sSeparator
+ * @param string $sTextQualifier
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ * @param bool $bConvertToPlainText
+ *
+ * @return mixed|string
+ * @throws \CoreException
+ */
+ public function GetAsCSV(
+ $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator');
+ $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator');
+ $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator');
+ $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier');
+
+ if (is_object($sValue) && ($sValue instanceof ormLinkSet)) {
+ $sValue->Rewind();
+ $aItems = array();
+ while ($oObj = $sValue->Fetch()) {
+ $sObjClass = get_class($oObj);
+ // Show only relevant information (hide the external key to the current object)
+ $aAttributes = array();
+ foreach (MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) {
+ if ($sAttCode == 'finalclass') {
+ if ($sObjClass == $this->GetLinkedClass()) {
+ // Simplify the output if the exact class could be determined implicitely
+ continue;
+ }
+ }
+ if ($sAttCode == $this->GetExtKeyToMe()) {
+ continue;
+ }
+ if ($oAttDef->IsExternalField()) {
+ continue;
+ }
+ if (!$oAttDef->IsBasedOnDBColumns()) {
+ continue;
+ }
+ if (!$oAttDef->IsScalar()) {
+ continue;
+ }
+ $sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '', $bLocalize);
+ if (strlen($sAttValue) > 0) {
+ $sAttributeData = str_replace($sAttributeQualifier, $sAttributeQualifier . $sAttributeQualifier,
+ $sAttCode . $sSepValue . $sAttValue);
+ $aAttributes[] = $sAttributeQualifier . $sAttributeData . $sAttributeQualifier;
+ }
+ }
+ $sAttributes = implode($sSepAttribute, $aAttributes);
+ $aItems[] = $sAttributes;
+ }
+ $sRes = implode($sSepItem, $aItems);
+ } else {
+ $sRes = '';
+ }
+ $sRes = str_replace($sTextQualifier, $sTextQualifier . $sTextQualifier, $sRes);
+ $sRes = $sTextQualifier . $sRes . $sTextQualifier;
+
+ return $sRes;
+ }
+
+ /**
+ * List the available verbs for 'GetForTemplate'
+ */
+ public function EnumTemplateVerbs()
+ {
+ return array(
+ '' => 'Plain text (unlocalized) representation',
+ 'html' => 'HTML representation (unordered list)',
+ );
+ }
+
+ /**
+ * Get various representations of the value, for insertion into a template (e.g. in Notifications)
+ *
+ * @param mixed $value The current value of the field
+ * @param string $sVerb The verb specifying the representation of the value
+ * @param DBObject $oHostObject The object
+ * @param bool $bLocalize Whether or not to localize the value
+ *
+ * @return string
+ * @throws \Exception
+ */
+ public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
+ {
+ $sRemoteName = $this->IsIndirect() ?
+ /** @var \AttributeLinkedSetIndirect $this */
+ $this->GetExtKeyToRemote() . '_friendlyname' : 'friendlyname';
+
+ $oLinkSet = clone $value; // Workaround/Safety net for Trac #887
+ $iLimit = MetaModel::GetConfig()->Get('max_linkset_output');
+ $iCount = 0;
+ $aNames = array();
+ foreach ($oLinkSet as $oItem) {
+ if (($iLimit > 0) && ($iCount == $iLimit)) {
+ $iTotal = $oLinkSet->Count();
+ $aNames[] = '... ' . Dict::Format('UI:TruncatedResults', $iCount, $iTotal);
+ break;
+ }
+ $aNames[] = $oItem->Get($sRemoteName);
+ $iCount++;
+ }
+
+ switch ($sVerb) {
+ case '':
+ return implode("\n", $aNames);
+
+ case 'html':
+ return '- ' . implode("
- ", $aNames) . '
';
+
+ default:
+ throw new Exception("Unknown verb '$sVerb' for attribute " . $this->GetCode() . ' in class ' . get_class($oHostObject));
+ }
+ }
+
+ public function DuplicatesAllowed()
+ {
+ return false;
+ } // No duplicates for 1:n links, never
+
+ public function GetImportColumns()
+ {
+ $aColumns = array();
+ $aColumns[$this->GetCode()] = 'MEDIUMTEXT' . CMDBSource::GetSqlStringColumnDefinition();
+
+ return $aColumns;
+ }
+
+ /**
+ * @param string $sProposedValue
+ * @param bool $bLocalizedValue
+ * @param string $sSepItem
+ * @param string $sSepAttribute
+ * @param string $sSepValue
+ * @param string $sAttributeQualifier
+ *
+ * @return \DBObjectSet|mixed
+ * @throws \CSVParserException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \MissingQueryArgument
+ * @throws \MySQLException
+ * @throws \MySQLHasGoneAwayException
+ * @throws \Exception
+ */
+ public function MakeValueFromString(
+ $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
+ $sAttributeQualifier = null
+ )
+ {
+ if (is_null($sSepItem) || empty($sSepItem)) {
+ $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator');
+ }
+ if (is_null($sSepAttribute) || empty($sSepAttribute)) {
+ $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator');
+ }
+ if (is_null($sSepValue) || empty($sSepValue)) {
+ $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator');
+ }
+ if (is_null($sAttributeQualifier) || empty($sAttributeQualifier)) {
+ $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier');
+ }
+
+ $sTargetClass = $this->Get('linked_class');
+
+ $sInput = str_replace($sSepItem, "\n", $sProposedValue);
+ $oCSVParser = new CSVParser($sInput, $sSepAttribute, $sAttributeQualifier);
+
+ $aInput = $oCSVParser->ToArray(0 /* do not skip lines */);
+
+ $aLinks = array();
+ foreach ($aInput as $aRow) {
+ // 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value
+ $aExtKeys = array();
+ $aValues = array();
+ foreach ($aRow as $sCell) {
+ $iSepPos = strpos($sCell, $sSepValue);
+ if ($iSepPos === false) {
+ // Houston...
+ throw new CoreException('Wrong format for link attribute specification', array('value' => $sCell));
+ }
+
+ $sAttCode = trim(substr($sCell, 0, $iSepPos));
+ $sValue = substr($sCell, $iSepPos + strlen($sSepValue));
+
+ if (preg_match('/^(.+)->(.+)$/', $sAttCode, $aMatches)) {
+ $sKeyAttCode = $aMatches[1];
+ $sRemoteAttCode = $aMatches[2];
+ $aExtKeys[$sKeyAttCode][$sRemoteAttCode] = $sValue;
+ if (!MetaModel::IsValidAttCode($sTargetClass, $sKeyAttCode)) {
+ throw new CoreException('Wrong attribute code for link attribute specification',
+ array('class' => $sTargetClass, 'attcode' => $sKeyAttCode));
+ }
+ /** @var \AttributeExternalKey $oKeyAttDef */
+ $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
+ $sRemoteClass = $oKeyAttDef->GetTargetClass();
+ if (!MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode)) {
+ throw new CoreException('Wrong attribute code for link attribute specification',
+ array('class' => $sRemoteClass, 'attcode' => $sRemoteAttCode));
+ }
+ } else {
+ if (!MetaModel::IsValidAttCode($sTargetClass, $sAttCode)) {
+ throw new CoreException('Wrong attribute code for link attribute specification',
+ array('class' => $sTargetClass, 'attcode' => $sAttCode));
+ }
+ $oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttCode);
+ $aValues[$sAttCode] = $oAttDef->MakeValueFromString($sValue, $bLocalizedValue, $sSepItem,
+ $sSepAttribute, $sSepValue, $sAttributeQualifier);
+ }
+ }
+
+ // 2nd - Instanciate the object and set the value
+ if (isset($aValues['finalclass'])) {
+ $sLinkClass = $aValues['finalclass'];
+ if (!is_subclass_of($sLinkClass, $sTargetClass)) {
+ throw new CoreException('Wrong class for link attribute specification',
+ array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass));
+ }
+ } elseif (MetaModel::IsAbstract($sTargetClass)) {
+ throw new CoreException('Missing finalclass for link attribute specification');
+ } else {
+ $sLinkClass = $sTargetClass;
+ }
+
+ $oLink = MetaModel::NewObject($sLinkClass);
+ foreach ($aValues as $sAttCode => $sValue) {
+ $oLink->Set($sAttCode, $sValue);
+ }
+
+ // 3rd - Set external keys from search conditions
+ foreach ($aExtKeys as $sKeyAttCode => $aReconciliation) {
+ $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
+ $sKeyClass = $oKeyAttDef->GetTargetClass();
+ $oExtKeyFilter = new DBObjectSearch($sKeyClass);
+ $aReconciliationDesc = array();
+ foreach ($aReconciliation as $sRemoteAttCode => $sValue) {
+ $oExtKeyFilter->AddCondition($sRemoteAttCode, $sValue, '=');
+ $aReconciliationDesc[] = "$sRemoteAttCode=$sValue";
+ }
+ $oExtKeySet = new DBObjectSet($oExtKeyFilter);
+ switch ($oExtKeySet->Count()) {
+ case 0:
+ $sReconciliationDesc = implode(', ', $aReconciliationDesc);
+ throw new CoreException("Found no match",
+ array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc));
+ break;
+ case 1:
+ $oRemoteObj = $oExtKeySet->Fetch();
+ $oLink->Set($sKeyAttCode, $oRemoteObj->GetKey());
+ break;
+ default:
+ $sReconciliationDesc = implode(', ', $aReconciliationDesc);
+ throw new CoreException("Found several matches",
+ array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc));
+ // Found several matches, ambiguous
+ }
+ }
+
+ // Check (roughly) if such a link is valid
+ $aErrors = array();
+ foreach (MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) {
+ if ($oAttDef->IsExternalKey()) {
+ /** @var \AttributeExternalKey $oAttDef */
+ if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(),
+ $oAttDef->GetTargetClass()))) {
+ continue; // Don't check the key to self
+ }
+ }
+
+ if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) {
+ $aErrors[] = $sAttCode;
+ }
+ }
+ if (count($aErrors) > 0) {
+ throw new CoreException("Missing value for mandatory attribute(s): " . implode(', ', $aErrors));
+ }
+
+ $aLinks[] = $oLink;
+ }
+ $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks);
+
+ return $oSet;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param \ormLinkSet $value
+ */
+ public function GetForJSON($value)
+ {
+ $aRet = array();
+ if (is_object($value) && ($value instanceof ormLinkSet)) {
+ $value->Rewind();
+ while ($oObj = $value->Fetch()) {
+ $sObjClass = get_class($oObj);
+ // Show only relevant information (hide the external key to the current object)
+ $aAttributes = array();
+ foreach (MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) {
+ if ($sAttCode == 'finalclass') {
+ if ($sObjClass == $this->GetLinkedClass()) {
+ // Simplify the output if the exact class could be determined implicitely
+ continue;
+ }
+ }
+ if ($sAttCode == $this->GetExtKeyToMe()) {
+ continue;
+ }
+ if ($oAttDef->IsExternalField()) {
+ continue;
+ }
+ if (!$oAttDef->IsBasedOnDBColumns()) {
+ continue;
+ }
+ if (!$oAttDef->IsScalar()) {
+ continue;
+ }
+ $attValue = $oObj->Get($sAttCode);
+ $aAttributes[$sAttCode] = $oAttDef->GetForJSON($attValue);
+ }
+ $aRet[] = $aAttributes;
+ }
+ }
+
+ return $aRet;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return \DBObjectSet
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \Exception
+ */
+ public function FromJSONToValue($json)
+ {
+ $sTargetClass = $this->Get('linked_class');
+
+ $aLinks = array();
+ foreach ($json as $aValues) {
+ if (isset($aValues['finalclass'])) {
+ $sLinkClass = $aValues['finalclass'];
+ if (!is_subclass_of($sLinkClass, $sTargetClass)) {
+ throw new CoreException('Wrong class for link attribute specification',
+ array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass));
+ }
+ } elseif (MetaModel::IsAbstract($sTargetClass)) {
+ throw new CoreException('Missing finalclass for link attribute specification');
+ } else {
+ $sLinkClass = $sTargetClass;
+ }
+
+ $oLink = MetaModel::NewObject($sLinkClass);
+ foreach ($aValues as $sAttCode => $sValue) {
+ $oLink->Set($sAttCode, $sValue);
+ }
+
+ // Check (roughly) if such a link is valid
+ $aErrors = array();
+ foreach (MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) {
+ if ($oAttDef->IsExternalKey()) {
+ /** @var AttributeExternalKey $oAttDef */
+ if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(),
+ $oAttDef->GetTargetClass()))) {
+ continue; // Don't check the key to self
+ }
+ }
+
+ if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) {
+ $aErrors[] = $sAttCode;
+ }
+ }
+ if (count($aErrors) > 0) {
+ throw new CoreException("Missing value for mandatory attribute(s): " . implode(', ', $aErrors));
+ }
+
+ $aLinks[] = $oLink;
+ }
+ $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks);
+
+ return $oSet;
+ }
+
+ /**
+ * @param $proposedValue
+ * @param $oHostObj
+ *
+ * @return mixed
+ * @throws \Exception
+ */
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if ($proposedValue === null) {
+ $sLinkedClass = $this->GetLinkedClass();
+ $aLinkedObjectsArray = array();
+ $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray);
+
+ return new ormLinkSet(
+ get_class($oHostObj),
+ $this->GetCode(),
+ $oSet
+ );
+ }
+
+ return $proposedValue;
+ }
+
+ /**
+ * @param ormLinkSet $val1
+ * @param ormLinkSet $val2
+ *
+ * @return bool
+ */
+ public function Equals($val1, $val2)
+ {
+ if ($val1 === $val2) {
+ $bAreEquivalent = true;
+ } else {
+ $bAreEquivalent = ($val2->HasDelta() === false);
+ }
+
+ return $bAreEquivalent;
+ }
+
+ /**
+ * Find the corresponding "link" attribute on the target class, if any
+ *
+ * @return null | AttributeDefinition
+ * @throws \Exception
+ */
+ public function GetMirrorLinkAttribute()
+ {
+ $oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe());
+
+ return $oRemoteAtt;
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\LinkedSetField';
+ }
+
+ /**
+ * @param \DBObject $oObject
+ * @param \Combodo\iTop\Form\Field\LinkedSetField $oFormField
+ *
+ * @return \Combodo\iTop\Form\Field\LinkedSetField
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ * @throws \Exception
+ */
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ if ($oFormField === null) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ }
+
+ // Setting target class
+ if (!$this->IsIndirect()) {
+ $sTargetClass = $this->GetLinkedClass();
+ } else {
+ /** @var \AttributeExternalKey $oRemoteAttDef */
+ /** @var \AttributeLinkedSetIndirect $this */
+ $oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
+ $sTargetClass = $oRemoteAttDef->GetTargetClass();
+
+ /** @var \AttributeLinkedSetIndirect $this */
+ $oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote());
+ }
+ $oFormField->SetTargetClass($sTargetClass);
+ $oFormField->SetLinkedClass($this->GetLinkedClass());
+ $oFormField->SetIndirect($this->IsIndirect());
+ // Setting attcodes to display
+ $aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list'));
+ // - Adding friendlyname attribute to the list is not already in it
+ $sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass);
+ if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay)) {
+ $aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay);
+ }
+ // - Adding attribute properties
+ $aAttributesToDisplay = array();
+ foreach ($aAttCodesToDisplay as $sAttCodeToDisplay) {
+ $oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay);
+ $aAttributesToDisplay[$sAttCodeToDisplay] = [
+ 'att_code' => $sAttCodeToDisplay,
+ 'label' => $oAttDefToDisplay->GetLabel(),
+ ];
+ }
+ $oFormField->SetAttributesToDisplay($aAttributesToDisplay);
+
+ // Append lnk attributes (filtered from zlist)
+ if ($this->IsIndirect()) {
+ $aLnkAttDefToDisplay = MetaModel::GetZListAttDefsFilteredForIndirectLinkClass($this->m_sHostClass, $this->m_sCode);
+ $aLnkAttributesToDisplay = array();
+ foreach ($aLnkAttDefToDisplay as $oLnkAttDefToDisplay) {
+ $aLnkAttributesToDisplay[$oLnkAttDefToDisplay->GetCode()] = [
+ 'att_code' => $oLnkAttDefToDisplay->GetCode(),
+ 'label' => $oLnkAttDefToDisplay->GetLabel(),
+ 'mandatory' => !$oLnkAttDefToDisplay->IsNullAllowed(),
+ ];
+ }
+ $oFormField->SetLnkAttributesToDisplay($aLnkAttributesToDisplay);
+ }
+
+ parent::MakeFormField($oObject, $oFormField);
+
+ return $oFormField;
+ }
+
+ public function IsPartOfFingerprint()
+ {
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ * @param \ormLinkSet $proposedValue
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ // Protection against wrong value type
+ if (false === ($proposedValue instanceof ormLinkSet)) {
+ return parent::HasAValue($proposedValue);
+ }
+
+ // We test if there is at least 1 item in the linkset (new or existing), not if an item is being added to it.
+ return $proposedValue->Count() > 0;
+ }
+
+ /**
+ * SearchSpecificLabel.
+ *
+ * @param string $sDictEntrySuffix
+ * @param string $sDefault
+ * @param bool $bUserLanguageOnly
+ * @param ...$aArgs
+ * @return string
+ * @since 3.1.0
+ */
+ public function SearchSpecificLabel(string $sDictEntrySuffix, string $sDefault, bool $bUserLanguageOnly, ...$aArgs): string
+ {
+ try {
+ $sNextClass = $this->m_sHostClass;
+
+ do {
+ $sKey = "Class:{$sNextClass}/Attribute:{$this->m_sCode}/{$sDictEntrySuffix}";
+ if (Dict::S($sKey, null, $bUserLanguageOnly) !== $sKey) {
+ return Dict::Format($sKey, ...$aArgs);
+ }
+ $sNextClass = MetaModel::GetParentClass($sNextClass);
+ } while ($sNextClass !== null);
+
+ if (Dict::S($sDictEntrySuffix, null, $bUserLanguageOnly) !== $sKey) {
+ return Dict::Format($sDictEntrySuffix, ...$aArgs);
+ } else {
+ return $sDefault;
+ }
+ } catch (Exception $e) {
+ ExceptionLog::LogException($e);
+ return $sDefault;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeLinkedSetIndirect.php b/sources/Core/AttributeDefinition/AttributeLinkedSetIndirect.php
new file mode 100644
index 0000000000..90d838647a
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeLinkedSetIndirect.php
@@ -0,0 +1,96 @@
+Get('ext_key_to_remote');
+ }
+
+ public function GetEditClass()
+ {
+ return "LinkedSet";
+ }
+
+ public function DuplicatesAllowed()
+ {
+ return $this->GetOptional("duplicates", false);
+ } // The same object may be linked several times... or not...
+
+ public function GetTrackingLevel()
+ {
+ return $this->GetOptional('tracking_level',
+ MetaModel::GetConfig()->Get('tracking_level_linked_set_indirect_default'));
+ }
+
+ /**
+ * Find the corresponding "link" attribute on the target class, if any
+ *
+ * @return null | AttributeDefinition
+ * @throws \CoreException
+ */
+ public function GetMirrorLinkAttribute()
+ {
+ $oRet = null;
+ /** @var \AttributeExternalKey $oExtKeyToRemote */
+ $oExtKeyToRemote = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
+ $sRemoteClass = $oExtKeyToRemote->GetTargetClass();
+ foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) {
+ if (!$oRemoteAttDef instanceof AttributeLinkedSetIndirect) {
+ continue;
+ }
+ if ($oRemoteAttDef->GetLinkedClass() != $this->GetLinkedClass()) {
+ continue;
+ }
+ if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetExtKeyToRemote()) {
+ continue;
+ }
+ if ($oRemoteAttDef->GetExtKeyToRemote() != $this->GetExtKeyToMe()) {
+ continue;
+ }
+ $oRet = $oRemoteAttDef;
+ break;
+ }
+
+ return $oRet;
+ }
+
+ /** @inheritDoc */
+ public static function IsBulkModifyCompatible(): bool
+ {
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeLongText.php b/sources/Core/AttributeDefinition/AttributeLongText.php
new file mode 100644
index 0000000000..73a9668b4d
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeLongText.php
@@ -0,0 +1,45 @@
+GetFormat() === 'html')
+ ? CMDBChangeOpSetAttributeHTML::class
+ : CMDBChangeOpSetAttributeLongText::class;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeMetaEnum.php b/sources/Core/AttributeDefinition/AttributeMetaEnum.php
new file mode 100644
index 0000000000..7ddbbd88e0
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeMetaEnum.php
@@ -0,0 +1,137 @@
+GetHostClass();
+ }
+ $aMappingData = $this->GetMapRule($sClass);
+ if ($aMappingData == null) {
+ $aRet = array();
+ } else {
+ $aRet = array($aMappingData['attcode']);
+ }
+
+ return $aRet;
+ }
+
+ /**
+ * Overload the standard so as to leave the data unsorted
+ *
+ * @param array $aArgs
+ * @param string $sContains
+ *
+ * @return array|null
+ */
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ $oValSetDef = $this->GetValuesDef();
+ if (!$oValSetDef) {
+ return null;
+ }
+ $aRawValues = $oValSetDef->GetValueList();
+
+ if (is_null($aRawValues)) {
+ return null;
+ }
+ $aLocalizedValues = array();
+ foreach ($aRawValues as $sKey => $sValue) {
+ $aLocalizedValues[$sKey] = $this->GetValueLabel($sKey);
+ }
+
+ return $aLocalizedValues;
+ }
+
+ /**
+ * Returns the meta value for the given object.
+ * See also MetaModel::RebuildMetaEnums() that must be maintained when MapValue changes
+ *
+ * @param $oObject
+ *
+ * @return mixed
+ * @throws Exception
+ */
+ public function MapValue($oObject)
+ {
+ $aMappingData = $this->GetMapRule(get_class($oObject));
+ if ($aMappingData == null) {
+ $sRet = $this->GetDefaultValue();
+ } else {
+ $sAttCode = $aMappingData['attcode'];
+ $value = $oObject->Get($sAttCode);
+ if (array_key_exists($value, $aMappingData['values'])) {
+ $sRet = $aMappingData['values'][$value];
+ } elseif ($this->GetDefaultValue() != '') {
+ $sRet = $this->GetDefaultValue();
+ } else {
+ throw new Exception('AttributeMetaEnum::MapValue(): mapping not found for value "' . $value . '" in ' . get_class($oObject) . ', on attribute ' . MetaModel::GetAttributeOrigin($this->GetHostClass(),
+ $this->GetCode()) . '::' . $this->GetCode());
+ }
+ }
+
+ return $sRet;
+ }
+
+ public function GetMapRule($sClass)
+ {
+ $aMappings = $this->Get('mapping');
+ if (array_key_exists($sClass, $aMappings)) {
+ $aMappingData = $aMappings[$sClass];
+ } else {
+ $sParent = MetaModel::GetParentClass($sClass);
+ if (is_null($sParent)) {
+ $aMappingData = null;
+ } else {
+ $aMappingData = $this->GetMapRule($sParent);
+ }
+ }
+
+ return $aMappingData;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeOQL.php b/sources/Core/AttributeDefinition/AttributeOQL.php
new file mode 100644
index 0000000000..918df131a2
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeOQL.php
@@ -0,0 +1,33 @@
+Get("is_null_allowed");
+ }
+
+
+ public function GetBasicFilterOperators()
+ {
+ return parent::GetBasicFilterOperators();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return parent::GetBasicFilterLooseOperator();
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ return parent::GetBasicFilterSQLExpr($sOpCode, $value);
+ }
+
+ public function GetNullValue()
+ {
+ return 0;
+ }
+
+ public function IsNull($proposedValue)
+ {
+ return ($proposedValue == 0);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ return ((int)$proposedValue) !== 0;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param int|DBObject $proposedValue Object key or valid ({@see MetaModel::IsValidObject()}) datamodel object
+ */
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (is_null($proposedValue)) {
+ return 0;
+ }
+ if ($proposedValue === '') {
+ return 0;
+ }
+ if (MetaModel::IsValidObject($proposedValue)) {
+ return $proposedValue->GetKey();
+ }
+
+ return (int)$proposedValue;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeObsolescenceDate.php b/sources/Core/AttributeDefinition/AttributeObsolescenceDate.php
new file mode 100644
index 0000000000..b2bf12e0ec
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeObsolescenceDate.php
@@ -0,0 +1,35 @@
+ null,
+ "sql" => $sCode,
+ "default_value" => "",
+ "is_null_allowed" => false,
+ "depends_on" => array()
+ ));
+ }
+
+ public function IsWritable()
+ {
+ return false;
+ }
+
+ public function IsMagic()
+ {
+ return true;
+ }
+
+ public static function IsBasedOnDBColumns()
+ {
+ return false;
+ }
+
+ /**
+ * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via
+ * GetOQLExpression)
+ *
+ * @return bool
+ */
+ public static function IsBasedOnOQLExpression()
+ {
+ return true;
+ }
+
+ public function GetOQLExpression()
+ {
+ return MetaModel::GetObsolescenceExpression($this->GetHostClass());
+ }
+
+ public function GetSQLExpressions($sPrefix = '')
+ {
+ return array();
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ return array();
+ } // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation)
+
+ public function GetSQLValues($value)
+ {
+ return array();
+ } // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update)
+
+ public function GetEditClass()
+ {
+ return "";
+ }
+
+ public function GetValuesDef()
+ {
+ return null;
+ }
+
+ public function GetPrerequisiteAttributes($sClass = null)
+ {
+ // Code duplicated with AttributeFriendlyName
+ $aAttributes = $this->GetOptional("depends_on", array());
+ $oExpression = $this->GetOQLExpression();
+ foreach ($oExpression->ListRequiredFields() as $sClass => $sAttCode) {
+ if (!in_array($sAttCode, $aAttributes)) {
+ $aAttributes[] = $sAttCode;
+ }
+ }
+ return $aAttributes;
+ }
+
+ public function IsDirectField()
+ {
+ return true;
+ }
+
+ public static function IsScalar()
+ {
+ return true;
+ }
+
+ public function GetSQLExpr()
+ {
+ return null;
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return $this->MakeRealValue(false, $oHostObject);
+ }
+
+ public function IsNullAllowed()
+ {
+ return false;
+ }
+
+ public function GetLabel($sDefault = null)
+ {
+ $sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label', $sDefault);
+
+ return parent::GetLabel($sDefault);
+ }
+
+ public function GetDescription($sDefault = null)
+ {
+ $sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label+', $sDefault);
+
+ return parent::GetDescription($sDefault);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeOneWayPassword.php b/sources/Core/AttributeDefinition/AttributeOneWayPassword.php
new file mode 100644
index 0000000000..2b6064308f
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeOneWayPassword.php
@@ -0,0 +1,233 @@
+GetOptional("is_null_allowed", false);
+ }
+
+ // Facilitate things: allow the user to Set the value from a string or from an ormPassword (already encrypted)
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ $oPassword = $proposedValue;
+ if (is_object($oPassword)) {
+ $oPassword = clone $proposedValue;
+ } else {
+ $oPassword = new ormPassword('', '');
+ $oPassword->SetPassword($proposedValue);
+ }
+
+ return $oPassword;
+ }
+
+ public function GetSQLExpressions($sPrefix = '')
+ {
+ if ($sPrefix == '') {
+ $sPrefix = $this->GetCode(); // Warning: AttributeOneWayPassword does not have any sql property so code = sql !
+ }
+ $aColumns = array();
+ // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
+ $aColumns[''] = $sPrefix . '_hash';
+ $aColumns['_salt'] = $sPrefix . '_salt';
+
+ return $aColumns;
+ }
+
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ if (!array_key_exists($sPrefix, $aCols)) {
+ $sAvailable = implode(', ', array_keys($aCols));
+ throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
+ }
+ $hashed = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : '';
+
+ if (!array_key_exists($sPrefix . '_salt', $aCols)) {
+ $sAvailable = implode(', ', array_keys($aCols));
+ throw new MissingColumnException("Missing column '" . $sPrefix . "_salt' from {$sAvailable}");
+ }
+ $sSalt = isset($aCols[$sPrefix . '_salt']) ? $aCols[$sPrefix . '_salt'] : '';
+
+ $value = new ormPassword($hashed, $sSalt);
+
+ return $value;
+ }
+
+ public function GetSQLValues($value)
+ {
+ // #@# Optimization: do not load blobs anytime
+ // As per mySQL doc, selecting blob columns will prevent mySQL from
+ // using memory in case a temporary table has to be created
+ // (temporary tables created on disk)
+ // We will have to remove the blobs from the list of attributes when doing the select
+ // then the use of Get() should finalize the load
+ if ($value instanceof ormPassword) {
+ $aValues = array();
+ $aValues[$this->GetCode() . '_hash'] = $value->GetHash();
+ $aValues[$this->GetCode() . '_salt'] = $value->GetSalt();
+ } else {
+ $aValues = array();
+ $aValues[$this->GetCode() . '_hash'] = '';
+ $aValues[$this->GetCode() . '_salt'] = '';
+ }
+
+ return $aValues;
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ $aColumns = array();
+ $aColumns[$this->GetCode() . '_hash'] = 'TINYBLOB';
+ $aColumns[$this->GetCode() . '_salt'] = 'TINYBLOB';
+
+ return $aColumns;
+ }
+
+ public function GetImportColumns()
+ {
+ $aColumns = array();
+ $aColumns[$this->GetCode()] = 'TINYTEXT' . CMDBSource::GetSqlStringColumnDefinition();
+
+ return $aColumns;
+ }
+
+ public function FromImportToValue($aCols, $sPrefix = '')
+ {
+ if (!isset($aCols[$sPrefix])) {
+ $sAvailable = implode(', ', array_keys($aCols));
+ throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
+ }
+ $sClearPwd = $aCols[$sPrefix];
+
+ $oPassword = new ormPassword('', '');
+ $oPassword->SetPassword($sClearPwd);
+
+ return $oPassword;
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return '=';
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ return 'true';
+ }
+
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if (is_object($value)) {
+ return $value->GetAsHTML();
+ }
+
+ return '';
+ }
+
+ public function GetAsCSV(
+ $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ return ''; // Not exportable in CSV
+ }
+
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ return ''; // Not exportable in XML
+ }
+
+ public function GetValueLabel($sValue, $oHostObj = null)
+ {
+ // Don't display anything in "group by" reports
+ return '*****';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ // Protection against wrong value type
+ if (false === ($proposedValue instanceof ormPassword)) {
+ // On object creation, the attribute value is "" instead of an ormPassword...
+ if (is_string($proposedValue)) {
+ return utils::IsNotNullOrEmptyString($proposedValue);
+ }
+
+ return parent::HasAValue($proposedValue);
+ }
+
+ return $proposedValue->IsEmpty() === false;
+ }
+
+ protected function GetChangeRecordAdditionalData(CMDBChangeOp $oMyChangeOp, DBObject $oObject, $original, $value): void
+ {
+ if (is_null($original)) {
+ $original = '';
+ }
+ $oMyChangeOp->Set("prev_pwd", $original);
+ }
+
+ protected function GetChangeRecordClassName(): string
+ {
+ return CMDBChangeOpSetAttributeOneWayPassword::class;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributePassword.php b/sources/Core/AttributeDefinition/AttributePassword.php
new file mode 100644
index 0000000000..1d45481697
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributePassword.php
@@ -0,0 +1,65 @@
+GetSQLColSpec() : '');
+ }
+
+ public function GetMaxSize()
+ {
+ return 64;
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (utils::IsNullOrEmptyString($sValue)) {
+ return '';
+ } else {
+ return '******';
+ }
+ }
+
+ public function IsPartOfFingerprint()
+ {
+ return false;
+ } // Cannot reliably compare two encrypted passwords since the same password will be encrypted in diffferent manners depending on the random 'salt'
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributePercentage.php b/sources/Core/AttributeDefinition/AttributePercentage.php
new file mode 100644
index 0000000000..c069a51a3f
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributePercentage.php
@@ -0,0 +1,53 @@
+ 100) {
+ $iValue = 100;
+ } else {
+ if ($iValue < 0) {
+ $iValue = 0;
+ }
+ }
+ if ($iValue > 90) {
+ $sColor = "#cc3300";
+ } else {
+ if ($iValue > 50) {
+ $sColor = "#cccc00";
+ } else {
+ $sColor = "#33cc00";
+ }
+ }
+ $iPercentWidth = ($iWidth * $iValue) / 100;
+
+ return " $sValue %";
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributePhoneNumber.php b/sources/Core/AttributeDefinition/AttributePhoneNumber.php
new file mode 100644
index 0000000000..963e742175
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributePhoneNumber.php
@@ -0,0 +1,51 @@
+GetOptional('validation_pattern',
+ '^' . utils::GetConfig()->Get('phone_number_validation_pattern') . '$');
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\PhoneField';
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (empty($sValue)) {
+ return '';
+ }
+
+ $sUrlDecorationClass = utils::GetConfig()->Get('phone_number_decoration_class');
+ $sUrlPattern = utils::GetConfig()->Get('phone_number_url_pattern');
+ $sUrl = sprintf($sUrlPattern, $sValue);
+
+ return '' . parent::GetAsHTML($sValue) . '';
+ }
+
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributePropertySet.php b/sources/Core/AttributeDefinition/AttributePropertySet.php
new file mode 100644
index 0000000000..cedad14e33
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributePropertySet.php
@@ -0,0 +1,108 @@
+ (string)$proposedValue);
+ }
+
+ return $proposedValue;
+ }
+
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if (!is_array($value)) {
+ throw new CoreException('Expecting an array', array('found' => get_class($value)));
+ }
+ if (count($value) == 0) {
+ return "";
+ }
+
+ $sRes = "";
+ $sRes .= "";
+ foreach ($value as $sProperty => $sValue) {
+ if ($sProperty == 'auth_pwd') {
+ $sValue = '*****';
+ }
+ $sRes .= "";
+ $sCell = str_replace("\n", "
\n", Str::pure2html(@(string)$sValue));
+ $sRes .= "$sProperty | $sCell | ";
+ $sRes .= "
";
+ }
+ $sRes .= "";
+ $sRes .= "
";
+
+ return $sRes;
+ }
+
+ public function GetAsCSV(
+ $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ if (!is_array($value) || count($value) == 0) {
+ return "";
+ }
+
+ $aRes = array();
+ foreach ($value as $sProperty => $sValue) {
+ if ($sProperty == 'auth_pwd') {
+ $sValue = '*****';
+ }
+ $sFrom = array(',', '=');
+ $sTo = array('\,', '\=');
+ $aRes[] = $sProperty . '=' . str_replace($sFrom, $sTo, (string)$sValue);
+ }
+ $sRaw = implode(',', $aRes);
+
+ $sFrom = array("\r\n", $sTextQualifier);
+ $sTo = array("\n", $sTextQualifier . $sTextQualifier);
+ $sEscaped = str_replace($sFrom, $sTo, $sRaw);
+
+ return $sTextQualifier . $sEscaped . $sTextQualifier;
+ }
+
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if (!is_array($value) || count($value) == 0) {
+ return "";
+ }
+
+ $sRes = "";
+ foreach ($value as $sProperty => $sValue) {
+ if ($sProperty == 'auth_pwd') {
+ $sValue = '*****';
+ }
+ $sRes .= "";
+ $sRes .= Str::pure2xml((string)$sValue);
+ $sRes .= "";
+ }
+
+ return $sRes;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeQueryAttCodeSet.php b/sources/Core/AttributeDefinition/AttributeQueryAttCodeSet.php
new file mode 100644
index 0000000000..8a63ad2415
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeQueryAttCodeSet.php
@@ -0,0 +1,169 @@
+aCSSClasses[] = 'attribute-query-attcode-set';
+ }
+
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array('query_field'));
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "TEXT" . CMDBSource::GetSqlStringColumnDefinition();
+ }
+
+ public function GetMaxSize()
+ {
+ return 65535;
+ }
+
+ /**
+ * Get a class array indexed by alias
+ * @param $oHostObj
+ *
+ * @return array
+ */
+ private function GetClassList($oHostObj)
+ {
+ try {
+ $sQueryField = $this->Get('query_field');
+ $sQuery = $oHostObj->Get($sQueryField);
+ if (empty($sQuery)) {
+ return array();
+ }
+ $oFilter = DBSearch::FromOQL($sQuery);
+ return $oFilter->GetSelectedClasses();
+
+ } catch (OQLException $e) {
+ IssueLog::Warning($e->getMessage());
+ }
+ return array();
+ }
+
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ if (isset($aArgs['this'])) {
+ $oHostObj = $aArgs['this'];
+ $aClasses = $this->GetClassList($oHostObj);
+
+ $aAllowedAttributes = array();
+ $aAllAttributes = array();
+
+ if ((count($aClasses) == 1) && (array_keys($aClasses)[0] == array_values($aClasses)[0])) {
+ $sClass = reset($aClasses);
+ $aAttributes = MetaModel::GetAttributesList($sClass);
+ foreach ($aAttributes as $sAttCode) {
+ $aAllowedAttributes[$sAttCode] = "$sAttCode (" . MetaModel::GetLabel($sClass, $sAttCode) . ')';
+ }
+ } else {
+ if (!empty($aClasses)) {
+ ksort($aClasses);
+ foreach ($aClasses as $sAlias => $sClass) {
+ $aAttributes = MetaModel::GetAttributesList($sClass);
+ foreach ($aAttributes as $sAttCode) {
+ $aAllAttributes[] = array('alias' => $sAlias, 'class' => $sClass, 'att_code' => $sAttCode);
+ }
+ }
+ }
+ foreach ($aAllAttributes as $aFullAttCode) {
+ $sAttCode = $aFullAttCode['alias'] . '.' . $aFullAttCode['att_code'];
+ $sClass = $aFullAttCode['class'];
+ $sLabel = "$sAttCode (" . MetaModel::GetLabel($sClass, $aFullAttCode['att_code']) . ')';
+ $aAllowedAttributes[$sAttCode] = $sLabel;
+ }
+ }
+ return $aAllowedAttributes;
+ }
+
+ return null;
+ }
+
+ /**
+ * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
+ *
+ * @param $proposedValue
+ * @param \DBObject $oHostObj
+ *
+ * @param bool $bIgnoreErrors
+ *
+ * @return mixed
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \OQLException
+ * @throws \Exception
+ */
+ public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false)
+ {
+ $oSet = new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
+ $aArgs = array();
+ if (!empty($oHostObj)) {
+ $aArgs['this'] = $oHostObj;
+ }
+ $aAllowedAttributes = $this->GetAllowedValues($aArgs);
+ $aInvalidAttCodes = array();
+ if (is_string($proposedValue) && !empty($proposedValue)) {
+ $proposedValue = trim($proposedValue);
+ $aProposedValues = $this->FromStringToArray($proposedValue);
+ $aValues = array();
+ foreach ($aProposedValues as $sValue) {
+ $sAttCode = trim($sValue);
+ if (empty($aAllowedAttributes) || isset($aAllowedAttributes[$sAttCode])) {
+ $aValues[$sAttCode] = $sAttCode;
+ } else {
+ $aInvalidAttCodes[] = $sAttCode;
+ }
+ }
+ $oSet->SetValues($aValues);
+ } elseif ($proposedValue instanceof ormSet) {
+ $oSet = $proposedValue;
+ }
+ if (!empty($aInvalidAttCodes) && !$bIgnoreErrors) {
+ throw new CoreUnexpectedValue("The attribute(s) " . implode(', ', $aInvalidAttCodes) . " are invalid");
+ }
+
+ return $oSet;
+ }
+
+ /**
+ * @param $value
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string|null
+ *
+ * @throws \Exception
+ */
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+
+ if ($value instanceof ormSet) {
+ $value = $value->GetValues();
+ }
+ if (is_array($value)) {
+ if (!empty($oHostObject) && $bLocalize) {
+ $aArgs['this'] = $oHostObject;
+ $aAllowedAttributes = $this->GetAllowedValues($aArgs);
+
+ $aLocalizedValues = array();
+ foreach ($value as $sAttCode) {
+ if (isset($aAllowedAttributes[$sAttCode])) {
+ $sLabelForHtmlAttribute = utils::HtmlEntities($aAllowedAttributes[$sAttCode]);
+ $aLocalizedValues[] = '' . $sAttCode . '';
+ }
+ }
+ $value = $aLocalizedValues;
+ }
+ $value = implode('', $value);
+ }
+
+ return '' . $value . '';
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeRedundancySettings.php b/sources/Core/AttributeDefinition/AttributeRedundancySettings.php
new file mode 100644
index 0000000000..3cbe036db6
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeRedundancySettings.php
@@ -0,0 +1,445 @@
+GetSQLColSpec() : '');
+ }
+
+
+ public function GetValidationPattern()
+ {
+ return "^[0-9]{1,3}|[0-9]{1,2}%|disabled$";
+ }
+
+ public function GetMaxSize()
+ {
+ return 20;
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ $sRet = 'disabled';
+ if ($this->Get('enabled')) {
+ if ($this->Get('min_up_type') == 'count') {
+ $sRet = (string)$this->Get('min_up');
+ } else // percent
+ {
+ $sRet = $this->Get('min_up') . '%';
+ }
+ }
+
+ return $sRet;
+ }
+
+ public function IsNullAllowed()
+ {
+ return false;
+ }
+
+ public function GetNullValue()
+ {
+ return '';
+ }
+
+ public function IsNull($proposedValue)
+ {
+ return ($proposedValue == '');
+ }
+
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (is_null($proposedValue)) {
+ return '';
+ }
+
+ return (string)$proposedValue;
+ }
+
+ public function ScalarToSQL($value)
+ {
+ if (!is_string($value)) {
+ throw new CoreException('Expected the attribute value to be a string', array(
+ 'found_type' => gettype($value),
+ 'value' => $value,
+ 'class' => $this->GetHostClass(),
+ 'attribute' => $this->GetCode()
+ ));
+ }
+
+ return $value;
+ }
+
+ public function GetRelationQueryData()
+ {
+ foreach (MetaModel::EnumRelationQueries($this->GetHostClass(), $this->Get('relation_code'),
+ false) as $sDummy => $aQueryInfo) {
+ if ($aQueryInfo['sFromClass'] == $this->Get('from_class')) {
+ if ($aQueryInfo['sNeighbour'] == $this->Get('neighbour_id')) {
+ return $aQueryInfo;
+ }
+ }
+ }
+
+ return array();
+ }
+
+ /**
+ * Find the user option label
+ *
+ * @param string $sUserOption possible values : disabled|cout|percent
+ * @param string $sDefault
+ *
+ * @return string
+ * @throws \Exception
+ */
+ public function GetUserOptionFormat($sUserOption, $sDefault = null)
+ {
+ $sLabel = $this->SearchLabel('/Attribute:' . $this->m_sCode . '/' . $sUserOption, null, true /*user lang*/);
+ if (is_null($sLabel)) {
+ // If no default value is specified, let's define the most relevant one for developping purposes
+ if (is_null($sDefault)) {
+ $sDefault = str_replace('_', ' ', $this->m_sCode . ':' . $sUserOption . '(%1$s)');
+ }
+ // Browse the hierarchy again, accepting default (english) translations
+ $sLabel = $this->SearchLabel('/Attribute:' . $this->m_sCode . '/' . $sUserOption, $sDefault, false);
+ }
+
+ return $sLabel;
+ }
+
+ /**
+ * Override to display the value in the GUI
+ *
+ * @param string $sValue
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ */
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ $sCurrentOption = $this->GetCurrentOption($sValue);
+ $sClass = $oHostObject ? get_class($oHostObject) : $this->m_sHostClass;
+
+ return sprintf($this->GetUserOptionFormat($sCurrentOption), $this->GetMinUpValue($sValue),
+ MetaModel::GetName($sClass));
+ }
+
+ public function GetAsCSV(
+ $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ $sFrom = array("\r\n", $sTextQualifier);
+ $sTo = array("\n", $sTextQualifier . $sTextQualifier);
+ $sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
+
+ return $sTextQualifier . $sEscaped . $sTextQualifier;
+ }
+
+ /**
+ * Helper to interpret the value, given the current settings and string representation of the attribute
+ */
+ public function IsEnabled($sValue)
+ {
+ if ($this->get('enabled_mode') == 'fixed') {
+ $bRet = $this->get('enabled');
+ } else {
+ $bRet = ($sValue != 'disabled');
+ }
+
+ return $bRet;
+ }
+
+ /**
+ * Helper to interpret the value, given the current settings and string representation of the attribute
+ */
+ public function GetMinUpType($sValue)
+ {
+ if ($this->get('min_up_mode') == 'fixed') {
+ $sRet = $this->get('min_up_type');
+ } else {
+ $sRet = 'count';
+ if (substr(trim($sValue), -1, 1) == '%') {
+ $sRet = 'percent';
+ }
+ }
+
+ return $sRet;
+ }
+
+ /**
+ * Helper to interpret the value, given the current settings and string representation of the attribute
+ */
+ public function GetMinUpValue($sValue)
+ {
+ if ($this->get('min_up_mode') == 'fixed') {
+ $iRet = (int)$this->Get('min_up');
+ } else {
+ $sRefValue = $sValue;
+ if (substr(trim($sValue), -1, 1) == '%') {
+ $sRefValue = substr(trim($sValue), 0, -1);
+ }
+ $iRet = (int)trim($sRefValue);
+ }
+
+ return $iRet;
+ }
+
+ /**
+ * Helper to determine if the redundancy can be viewed/edited by the end-user
+ */
+ public function IsVisible()
+ {
+ $bRet = false;
+ if ($this->Get('enabled_mode') == 'fixed') {
+ $bRet = $this->Get('enabled');
+ } elseif ($this->Get('enabled_mode') == 'user') {
+ $bRet = true;
+ }
+
+ return $bRet;
+ }
+
+ public function IsWritable()
+ {
+ if (($this->Get('enabled_mode') == 'fixed') && ($this->Get('min_up_mode') == 'fixed')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns an HTML form that can be read by ReadValueFromPostedForm
+ */
+ public function GetDisplayForm($sCurrentValue, $oPage, $bEditMode = false, $sFormPrefix = '')
+ {
+ $sRet = '';
+ $aUserOptions = $this->GetUserOptions($sCurrentValue);
+ if (count($aUserOptions) < 2) {
+ $bEditOption = false;
+ } else {
+ $bEditOption = $bEditMode;
+ }
+ $sCurrentOption = $this->GetCurrentOption($sCurrentValue);
+ foreach ($aUserOptions as $sUserOption) {
+ $bSelected = ($sUserOption == $sCurrentOption);
+ $sRet .= '';
+ $sRet .= $this->GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditOption, $sUserOption,
+ $bSelected);
+ $sRet .= '
';
+ }
+
+ return $sRet;
+ }
+
+ const USER_OPTION_DISABLED = 'disabled';
+ const USER_OPTION_ENABLED_COUNT = 'count';
+ const USER_OPTION_ENABLED_PERCENT = 'percent';
+
+ /**
+ * Depending on the xxx_mode parameters, build the list of options that are allowed to the end-user
+ */
+ protected function GetUserOptions($sValue)
+ {
+ $aRet = array();
+ if ($this->Get('enabled_mode') == 'user') {
+ $aRet[] = self::USER_OPTION_DISABLED;
+ }
+
+ if ($this->Get('min_up_mode') == 'user') {
+ $aRet[] = self::USER_OPTION_ENABLED_COUNT;
+ $aRet[] = self::USER_OPTION_ENABLED_PERCENT;
+ } else {
+ if ($this->GetMinUpType($sValue) == 'count') {
+ $aRet[] = self::USER_OPTION_ENABLED_COUNT;
+ } else {
+ $aRet[] = self::USER_OPTION_ENABLED_PERCENT;
+ }
+ }
+
+ return $aRet;
+ }
+
+ /**
+ * Convert the string representation into one of the existing options
+ */
+ protected function GetCurrentOption($sValue)
+ {
+ $sRet = self::USER_OPTION_DISABLED;
+ if ($this->IsEnabled($sValue)) {
+ if ($this->GetMinUpType($sValue) == 'count') {
+ $sRet = self::USER_OPTION_ENABLED_COUNT;
+ } else {
+ $sRet = self::USER_OPTION_ENABLED_PERCENT;
+ }
+ }
+
+ return $sRet;
+ }
+
+ /**
+ * Display an option (form, or current value)
+ *
+ * @param string $sCurrentValue
+ * @param \Combodo\iTop\Application\WebPage\WebPage $oPage
+ * @param string $sFormPrefix
+ * @param bool $bEditMode
+ * @param string $sUserOption
+ * @param bool $bSelected
+ *
+ * @return string
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ * @throws \Exception
+ */
+ protected function GetDisplayOption(
+ $sCurrentValue, $oPage, $sFormPrefix, $bEditMode, $sUserOption, $bSelected = true
+ )
+ {
+ $sRet = '';
+
+ $iCurrentValue = $this->GetMinUpValue($sCurrentValue);
+ if ($bEditMode) {
+ $sValue = null;
+ $sHtmlNamesPrefix = 'rddcy_' . $this->Get('relation_code') . '_' . $this->Get('from_class') . '_' . $this->Get('neighbour_id');
+ switch ($sUserOption) {
+ case self::USER_OPTION_DISABLED:
+ $sValue = ''; // Empty placeholder
+ break;
+
+ case self::USER_OPTION_ENABLED_COUNT:
+ if ($bEditMode) {
+ $sName = $sHtmlNamesPrefix . '_min_up_count';
+ $sEditValue = $bSelected ? $iCurrentValue : '';
+ $sValue = '';
+ // To fix an issue on Firefox: focus set to the option (because the input is within the label for the option)
+ $oPage->add_ready_script("\$('[name=\"$sName\"]').on('click', function(){var me=this; setTimeout(function(){\$(me).trigger('focus');}, 100);});");
+ } else {
+ $sValue = $iCurrentValue;
+ }
+ break;
+
+ case self::USER_OPTION_ENABLED_PERCENT:
+ if ($bEditMode) {
+ $sName = $sHtmlNamesPrefix . '_min_up_percent';
+ $sEditValue = $bSelected ? $iCurrentValue : '';
+ $sValue = '';
+ // To fix an issue on Firefox: focus set to the option (because the input is within the label for the option)
+ $oPage->add_ready_script("\$('[name=\"$sName\"]').on('click', function(){var me=this; setTimeout(function(){\$(me).trigger('focus');}, 100);});");
+ } else {
+ $sValue = $iCurrentValue;
+ }
+ break;
+ }
+ $sLabel = sprintf($this->GetUserOptionFormat($sUserOption), $sValue,
+ MetaModel::GetName($this->GetHostClass()));
+
+ $sOptionName = $sHtmlNamesPrefix . '_user_option';
+ $sOptionId = $sOptionName . '_' . $sUserOption;
+ $sChecked = $bSelected ? 'checked' : '';
+ $sRet = ' ';
+ } else {
+ // Read-only: display only the currently selected option
+ if ($bSelected) {
+ $sRet = sprintf($this->GetUserOptionFormat($sUserOption), $iCurrentValue,
+ MetaModel::GetName($this->GetHostClass()));
+ }
+ }
+
+ return $sRet;
+ }
+
+ /**
+ * Makes the string representation out of the values given by the form defined in GetDisplayForm
+ */
+ public function ReadValueFromPostedForm($sFormPrefix)
+ {
+ $sHtmlNamesPrefix = 'rddcy_' . $this->Get('relation_code') . '_' . $this->Get('from_class') . '_' . $this->Get('neighbour_id');
+
+ $iMinUpCount = (int)utils::ReadPostedParam($sHtmlNamesPrefix . '_min_up_count', null, 'raw_data');
+ $iMinUpPercent = (int)utils::ReadPostedParam($sHtmlNamesPrefix . '_min_up_percent', null, 'raw_data');
+ $sSelectedOption = utils::ReadPostedParam($sHtmlNamesPrefix . '_user_option', null, 'raw_data');
+ switch ($sSelectedOption) {
+ case self::USER_OPTION_ENABLED_COUNT:
+ $sRet = $iMinUpCount;
+ break;
+
+ case self::USER_OPTION_ENABLED_PERCENT:
+ $sRet = $iMinUpPercent . '%';
+ break;
+
+ case self::USER_OPTION_DISABLED:
+ default:
+ $sRet = 'disabled';
+ break;
+ }
+
+ return $sRet;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeSet.php b/sources/Core/AttributeDefinition/AttributeSet.php
new file mode 100644
index 0000000000..ca5e6c49e6
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeSet.php
@@ -0,0 +1,453 @@
+aCSSClasses[] = 'attribute-set';
+ $this->bDisplayLink = true;
+ }
+
+ /**
+ * @param bool $bDisplayLink
+ */
+ public function setDisplayLink($bDisplayLink)
+ {
+ $this->bDisplayLink = $bDisplayLink;
+ }
+
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array('is_null_allowed', 'max_items'));
+ }
+
+ /**
+ * Allowed different values for the set values are mandatory for this attribute to be modified
+ *
+ * @param array $aArgs
+ * @param string $sContains
+ *
+ * @return array|null
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function GetPossibleValues($aArgs = array(), $sContains = '')
+ {
+ return $this->GetAllowedValues($aArgs, $sContains);
+ }
+
+ /**
+ * @param \ormSet $oValue
+ *
+ * @param $aArgs
+ *
+ * @return string JSON to be used in the itop.set_widget JQuery widget
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function GetJsonForWidget($oValue, $aArgs = array())
+ {
+ $aJson = array();
+
+ // possible_values
+ $aAllowedValues = $this->GetPossibleValues($aArgs);
+ $aSetKeyValData = array();
+ foreach ($aAllowedValues as $sCode => $sLabel) {
+ $aSetKeyValData[] = [
+ 'code' => $sCode,
+ 'label' => $sLabel,
+ ];
+ }
+ $aJson['possible_values'] = $aSetKeyValData;
+ $aRemoved = array();
+ if (is_null($oValue)) {
+ $aJson['partial_values'] = array();
+ $aJson['orig_value'] = array();
+ } else {
+ $aPartialValues = $oValue->GetModified();
+ foreach ($aPartialValues as $key => $value) {
+ if (!isset($aAllowedValues[$value])) {
+ unset($aPartialValues[$key]);
+ }
+ }
+ $aJson['partial_values'] = array_values($aPartialValues);
+ $aOrigValues = array_merge($oValue->GetValues(), $oValue->GetModified());
+ foreach ($aOrigValues as $key => $value) {
+ if (!isset($aAllowedValues[$value])) {
+ // Remove unwanted values
+ $aRemoved[] = $value;
+ unset($aOrigValues[$key]);
+ }
+ }
+ $aJson['orig_value'] = array_values($aOrigValues);
+ }
+ $aJson['added'] = array();
+ $aJson['removed'] = $aRemoved;
+
+ $iMaxTags = $this->GetMaxItems();
+ $aJson['max_items_allowed'] = $iMaxTags;
+
+ return json_encode($aJson);
+ }
+
+ public function RequiresIndex()
+ {
+ return true;
+ }
+
+ public function RequiresFullTextIndex()
+ {
+ return true;
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return null;
+ }
+
+ public function IsNullAllowed()
+ {
+ return $this->Get("is_null_allowed");
+ }
+
+ public function GetEditClass()
+ {
+ return "Set";
+ }
+
+ public function GetEditValue($value, $oHostObj = null)
+ {
+ if (is_string($value)) {
+ return $value;
+ }
+ if ($value instanceof ormSet) {
+ $value = $value->GetValues();
+ }
+ if (is_array($value)) {
+ return implode(', ', $value);
+ }
+ return '';
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ $iLen = $this->GetMaxSize();
+ return "VARCHAR($iLen)"
+ . CMDBSource::GetSqlStringColumnDefinition()
+ . ($bFullSpec ? $this->GetSQLColSpec() : '');
+ }
+
+ public function GetMaxSize()
+ {
+ return 255;
+ }
+
+ public function FromStringToArray($proposedValue, $sDefaultSepItem = ',')
+ {
+ $aValues = array();
+ if (!empty($proposedValue)) {
+ $sSepItem = MetaModel::GetConfig()->Get('tag_set_item_separator');
+ // convert also , separated strings
+ if ($sSepItem !== $sDefaultSepItem) {
+ $proposedValue = str_replace($sDefaultSepItem, $sSepItem, $proposedValue);
+ }
+ foreach (explode($sSepItem, $proposedValue) as $sCode) {
+ $sValue = trim($sCode);
+ if ($sValue !== '') {
+ $aValues[] = $sValue;
+ }
+ }
+ }
+ return $aValues;
+ }
+
+ /**
+ * @param array $aCols
+ * @param string $sPrefix
+ *
+ * @return mixed
+ * @throws \Exception
+ */
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ $sValue = $aCols["$sPrefix"];
+
+ return $this->MakeRealValue($sValue, null, true);
+ }
+
+ /**
+ * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
+ *
+ * @param $proposedValue
+ * @param \DBObject $oHostObj
+ *
+ * @param bool $bIgnoreErrors
+ *
+ * @return mixed
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ */
+ public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false)
+ {
+ $oSet = new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
+ $aAllowedValues = $this->GetPossibleValues();
+ if (is_string($proposedValue) && !empty($proposedValue)) {
+ $proposedValue = trim("$proposedValue");
+ $aValues = $this->FromStringToArray($proposedValue);
+ foreach ($aValues as $i => $sValue) {
+ if (!isset($aAllowedValues[$sValue])) {
+ unset($aValues[$i]);
+ }
+ }
+ $oSet->SetValues($aValues);
+ } elseif ($proposedValue instanceof ormSet) {
+ $oSet = $proposedValue;
+ }
+
+ return $oSet;
+ }
+
+ /**
+ * Get the value from a given string (plain text, CSV import)
+ *
+ * @param string $sProposedValue
+ * @param bool $bLocalizedValue
+ * @param string $sSepItem
+ * @param string $sSepAttribute
+ * @param string $sSepValue
+ * @param string $sAttributeQualifier
+ *
+ * @return mixed null if no match could be found
+ * @throws \Exception
+ */
+ public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null)
+ {
+ return $this->MakeRealValue($sProposedValue, null);
+ }
+
+ /**
+ * @return null|\ormSet
+ * @throws \CoreException
+ * @throws \Exception
+ */
+ public function GetNullValue()
+ {
+ return new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
+ }
+
+ public function IsNull($proposedValue)
+ {
+ if (empty($proposedValue)) {
+ return true;
+ }
+
+ /** @var \ormSet $proposedValue */
+ return $proposedValue->Count() == 0;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ if (false === ($proposedValue instanceof ormSet)) {
+ return parent::HasAValue($proposedValue);
+ }
+
+ return $proposedValue->Count() > 0;
+ }
+
+ /**
+ * To be overloaded for localized enums
+ *
+ * @param $sValue
+ *
+ * @return string label corresponding to the given value (in plain text)
+ * @throws \Exception
+ */
+ public function GetValueLabel($sValue)
+ {
+ if ($sValue instanceof ormSet) {
+ $sValue = $sValue->GetValues();
+ }
+ if (is_array($sValue)) {
+ return implode(', ', $sValue);
+ }
+ return $sValue;
+ }
+
+ /**
+ * @param string $sValue
+ * @param null $oHostObj
+ *
+ * @return string
+ * @throws \Exception
+ */
+ public function GetAsPlainText($sValue, $oHostObj = null)
+ {
+ return $this->GetValueLabel($sValue);
+ }
+
+ /**
+ * @param string $value
+ *
+ * @return string
+ */
+ public function ScalarToSQL($value)
+ {
+ if (empty($value)) {
+ return '';
+ }
+ if ($value instanceof ormSet) {
+ $value = $value->GetValues();
+ }
+ if (is_array($value)) {
+ $sSepItem = MetaModel::GetConfig()->Get('tag_set_item_separator');
+ $sRes = implode($sSepItem, $value);
+ if (!empty($sRes)) {
+ $value = "{$sSepItem}{$sRes}{$sSepItem}";
+ } else {
+ $value = '';
+ }
+ }
+ return $value;
+ }
+
+ /**
+ * @param $value
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string|null
+ *
+ * @throws \Exception
+ */
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if ($value instanceof ormSet) {
+ $aValues = $value->GetValues();
+ return $this->GenerateViewHtmlForValues($aValues);
+ }
+ if (is_array($value)) {
+ return implode(', ', $value);
+ }
+ return $value;
+ }
+
+ /**
+ * HTML representation of a list of values (read-only)
+ * accept a list of strings
+ *
+ * @param array $aValues
+ * @param string $sCssClass
+ * @param bool $bWithLink if true will generate a link, otherwise just a "a" tag without href
+ *
+ * @return string
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function GenerateViewHtmlForValues($aValues, $sCssClass = '', $bWithLink = true)
+ {
+ if (empty($aValues)) {
+ return '';
+ }
+ $sHtml = '';
+ foreach ($aValues as $sValue) {
+ $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode());
+ $sAttCode = $this->GetCode();
+ $sLabel = utils::EscapeHtml($this->GetValueLabel($sValue));
+ $sDescription = utils::EscapeHtml($this->GetValueDescription($sValue));
+ $oFilter = DBSearch::FromOQL("SELECT $sClass WHERE $sAttCode MATCHES '$sValue'");
+ $oAppContext = new ApplicationContext();
+ $sContext = $oAppContext->GetForLink(true);
+ $sUIPage = cmdbAbstractObject::ComputeStandardUIPage($oFilter->GetClass());
+ $sFilter = rawurlencode($oFilter->serialize());
+ $sLink = '';
+ if ($bWithLink && $this->bDisplayLink) {
+ $sUrl = utils::GetAbsoluteUrlAppRoot() . "pages/$sUIPage?operation=search&filter=" . $sFilter . $sContext;
+ $sLink = ' href="' . $sUrl . '"';
+ }
+
+ // Prepare tooltip
+ if (empty($sDescription)) {
+ $sTooltipContent = $sLabel;
+ $sTooltipHtmlEnabled = 'false';
+ } else {
+ $sTooltipContent = <<$sLabel
+$sDescription
+HTML;
+ $sTooltipHtmlEnabled = 'true';
+ }
+ $sTooltipContent = utils::EscapeHtml($sTooltipContent);
+
+ $sHtml .= '' . $sLabel . '';
+ }
+ $sHtml .= '';
+
+ return $sHtml;
+ }
+
+ /**
+ * @param $value
+ * @param string $sSeparator
+ * @param string $sTextQualifier
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ * @param bool $bConvertToPlainText
+ *
+ * @return mixed|string
+ */
+ public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false)
+ {
+ $sSepItem = MetaModel::GetConfig()->Get('tag_set_item_separator');
+ if (is_object($value) && ($value instanceof ormSet)) {
+ if ($bLocalize) {
+ $aValues = $value->GetLabels();
+ } else {
+ $aValues = $value->GetValues();
+ }
+ $sRes = implode($sSepItem, $aValues);
+ } else {
+ $sRes = '';
+ }
+
+ return "{$sTextQualifier}{$sRes}{$sTextQualifier}";
+ }
+
+ public function GetMaxItems()
+ {
+ return $this->Get('max_items');
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\SetField';
+ }
+
+ public function RecordAttChange(DBObject $oObject, $original, $value): void
+ {
+ /** @var \ormSet $original */
+ /** @var \ormSet $value */
+ parent::RecordAttChange($oObject,
+ implode(' ', $original->GetValues()),
+ implode(' ', $value->GetValues())
+ );
+ }
+
+ protected function GetChangeRecordClassName(): string
+ {
+ return CMDBChangeOpSetAttributeTagSet::class;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeStopWatch.php b/sources/Core/AttributeDefinition/AttributeStopWatch.php
new file mode 100644
index 0000000000..c67a8e377a
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeStopWatch.php
@@ -0,0 +1,808 @@
+ array of 'option' => value
+ return array_merge(parent::ListExpectedParams(),
+ array("states", "goal_computing", "working_time_computing", "thresholds"));
+ }
+
+ public function GetEditClass()
+ {
+ return "StopWatch";
+ }
+
+ public static function IsBasedOnDBColumns()
+ {
+ return true;
+ }
+
+ public static function IsScalar()
+ {
+ return true;
+ }
+
+ public function IsWritable()
+ {
+ return true;
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return $this->NewStopWatch();
+ }
+
+ /**
+ * @param \ormStopWatch $value
+ * @param \DBObject $oHostObj
+ *
+ * @return string
+ */
+ public function GetEditValue($value, $oHostObj = null)
+ {
+ return $value->GetTimeSpent();
+ }
+
+ public function GetStates()
+ {
+ return $this->Get('states');
+ }
+
+ public function AlwaysLoadInTables()
+ {
+ // Each and every stop watch is accessed for computing the highlight code (DBObject::GetHighlightCode())
+ return true;
+ }
+
+ /**
+ * Construct a brand new (but configured) stop watch
+ */
+ public function NewStopWatch()
+ {
+ $oSW = new ormStopWatch();
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $oSW->DefineThreshold($iThreshold);
+ }
+
+ return $oSW;
+ }
+
+ // Facilitate things: allow the user to Set the value from a string
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (!$proposedValue instanceof ormStopWatch) {
+ return $this->NewStopWatch();
+ }
+
+ return $proposedValue;
+ }
+
+ public function GetSQLExpressions($sPrefix = '')
+ {
+ if ($sPrefix == '') {
+ $sPrefix = $this->GetCode(); // Warning: a stopwatch does not have any 'sql' property, so its SQL column is equal to its attribute code !!
+ }
+ $aColumns = array();
+ // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
+ $aColumns[''] = $sPrefix . '_timespent';
+ $aColumns['_started'] = $sPrefix . '_started';
+ $aColumns['_laststart'] = $sPrefix . '_laststart';
+ $aColumns['_stopped'] = $sPrefix . '_stopped';
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = '_' . $iThreshold;
+ $aColumns[$sThPrefix . '_deadline'] = $sPrefix . $sThPrefix . '_deadline';
+ $aColumns[$sThPrefix . '_passed'] = $sPrefix . $sThPrefix . '_passed';
+ $aColumns[$sThPrefix . '_triggered'] = $sPrefix . $sThPrefix . '_triggered';
+ $aColumns[$sThPrefix . '_overrun'] = $sPrefix . $sThPrefix . '_overrun';
+ }
+
+ return $aColumns;
+ }
+
+ public static function DateToSeconds($sDate)
+ {
+ if (is_null($sDate)) {
+ return null;
+ }
+ $oDateTime = new DateTime($sDate);
+ $iSeconds = $oDateTime->format('U');
+
+ return $iSeconds;
+ }
+
+ public static function SecondsToDate($iSeconds)
+ {
+ if (is_null($iSeconds)) {
+ return null;
+ }
+
+ return date("Y-m-d H:i:s", $iSeconds);
+ }
+
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ $aExpectedCols = array($sPrefix, $sPrefix . '_started', $sPrefix . '_laststart', $sPrefix . '_stopped');
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = '_' . $iThreshold;
+ $aExpectedCols[] = $sPrefix . $sThPrefix . '_deadline';
+ $aExpectedCols[] = $sPrefix . $sThPrefix . '_passed';
+ $aExpectedCols[] = $sPrefix . $sThPrefix . '_triggered';
+ $aExpectedCols[] = $sPrefix . $sThPrefix . '_overrun';
+ }
+ foreach ($aExpectedCols as $sExpectedCol) {
+ if (!array_key_exists($sExpectedCol, $aCols)) {
+ $sAvailable = implode(', ', array_keys($aCols));
+ throw new MissingColumnException("Missing column '$sExpectedCol' from {$sAvailable}");
+ }
+ }
+
+ $value = new ormStopWatch(
+ $aCols[$sPrefix],
+ self::DateToSeconds($aCols[$sPrefix . '_started']),
+ self::DateToSeconds($aCols[$sPrefix . '_laststart']),
+ self::DateToSeconds($aCols[$sPrefix . '_stopped'])
+ );
+
+ foreach ($this->ListThresholds() as $iThreshold => $aDefinition) {
+ $sThPrefix = '_' . $iThreshold;
+ $value->DefineThreshold(
+ $iThreshold,
+ self::DateToSeconds($aCols[$sPrefix . $sThPrefix . '_deadline']),
+ (bool)($aCols[$sPrefix . $sThPrefix . '_passed'] == 1),
+ (bool)($aCols[$sPrefix . $sThPrefix . '_triggered'] == 1),
+ $aCols[$sPrefix . $sThPrefix . '_overrun'],
+ array_key_exists('highlight', $aDefinition) ? $aDefinition['highlight'] : null
+ );
+ }
+
+ return $value;
+ }
+
+ public function GetSQLValues($value)
+ {
+ if ($value instanceof ormStopWatch) {
+ $aValues = array();
+ $aValues[$this->GetCode() . '_timespent'] = $value->GetTimeSpent();
+ $aValues[$this->GetCode() . '_started'] = self::SecondsToDate($value->GetStartDate());
+ $aValues[$this->GetCode() . '_laststart'] = self::SecondsToDate($value->GetLastStartDate());
+ $aValues[$this->GetCode() . '_stopped'] = self::SecondsToDate($value->GetStopDate());
+
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sPrefix = $this->GetCode() . '_' . $iThreshold;
+ $aValues[$sPrefix . '_deadline'] = self::SecondsToDate($value->GetThresholdDate($iThreshold));
+ $aValues[$sPrefix . '_passed'] = $value->IsThresholdPassed($iThreshold) ? '1' : '0';
+ $aValues[$sPrefix . '_triggered'] = $value->IsThresholdTriggered($iThreshold) ? '1' : '0';
+ $aValues[$sPrefix . '_overrun'] = $value->GetOverrun($iThreshold);
+ }
+ } else {
+ $aValues = array();
+ $aValues[$this->GetCode() . '_timespent'] = '';
+ $aValues[$this->GetCode() . '_started'] = '';
+ $aValues[$this->GetCode() . '_laststart'] = '';
+ $aValues[$this->GetCode() . '_stopped'] = '';
+ }
+
+ return $aValues;
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ $aColumns = array();
+ $aColumns[$this->GetCode() . '_timespent'] = 'INT(11) UNSIGNED';
+ $aColumns[$this->GetCode() . '_started'] = 'DATETIME';
+ $aColumns[$this->GetCode() . '_laststart'] = 'DATETIME';
+ $aColumns[$this->GetCode() . '_stopped'] = 'DATETIME';
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sPrefix = $this->GetCode() . '_' . $iThreshold;
+ $aColumns[$sPrefix . '_deadline'] = 'DATETIME';
+ $aColumns[$sPrefix . '_passed'] = 'TINYINT(1) UNSIGNED';
+ $aColumns[$sPrefix . '_triggered'] = 'TINYINT(1)';
+ $aColumns[$sPrefix . '_overrun'] = 'INT(11) UNSIGNED';
+ }
+
+ return $aColumns;
+ }
+
+ public function GetMagicFields()
+ {
+ $aRes = [
+ $this->GetCode() . '_started',
+ $this->GetCode() . '_laststart',
+ $this->GetCode() . '_stopped',
+ ];
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sPrefix = $this->GetCode() . '_' . $iThreshold;
+ $aRes[] = $sPrefix . '_deadline';
+ $aRes[] = $sPrefix . '_passed';
+ $aRes[] = $sPrefix . '_triggered';
+ $aRes[] = $sPrefix . '_overrun';
+ }
+
+ return $aRes;
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return '=';
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ return 'true';
+ }
+
+ /**
+ * @param \ormStopWatch $value
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string
+ */
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if (is_object($value)) {
+ return $value->GetAsHTML($this, $oHostObject);
+ }
+
+ return '';
+ }
+
+ /**
+ * @param ormStopWatch $value
+ * @param string $sSeparator
+ * @param string $sTextQualifier
+ * @param null $oHostObject
+ * @param bool $bLocalize
+ * @param bool $bConvertToPlainText
+ *
+ * @return string
+ */
+ public function GetAsCSV(
+ $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ return $value->GetTimeSpent();
+ }
+
+ /**
+ * @param \ormStopWatch $value
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return mixed
+ */
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ return $value->GetTimeSpent();
+ }
+
+ public function ListThresholds()
+ {
+ return $this->Get('thresholds');
+ }
+
+ public function Fingerprint($value)
+ {
+ $sFingerprint = '';
+ if (is_object($value)) {
+ $sFingerprint = $value->GetAsHTML($this);
+ }
+
+ return $sFingerprint;
+ }
+
+ /**
+ * To expose internal values: Declare an attribute AttributeSubItem
+ * and implement the GetSubItemXXXX verbs
+ *
+ * @param string $sItemCode
+ *
+ * @return array
+ * @throws \CoreException
+ */
+ public function GetSubItemSQLExpression($sItemCode)
+ {
+ $sPrefix = $this->GetCode();
+ switch ($sItemCode) {
+ case 'timespent':
+ return array('' => $sPrefix . '_timespent');
+ case 'started':
+ return array('' => $sPrefix . '_started');
+ case 'laststart':
+ return array('' => $sPrefix . '_laststart');
+ case 'stopped':
+ return array('' => $sPrefix . '_stopped');
+ }
+
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ return array('' => $sPrefix . '_' . $iThreshold . '_deadline');
+ case 'passed':
+ return array('' => $sPrefix . '_' . $iThreshold . '_passed');
+ case 'triggered':
+ return array('' => $sPrefix . '_' . $iThreshold . '_triggered');
+ case 'overrun':
+ return array('' => $sPrefix . '_' . $iThreshold . '_overrun');
+ }
+ }
+ }
+ throw new CoreException("Unknown item code '$sItemCode' for attribute " . $this->GetHostClass() . '::' . $this->GetCode());
+ }
+
+ /**
+ * @param string $sItemCode
+ * @param \ormStopWatch $value
+ * @param \DBObject $oHostObject
+ *
+ * @return mixed
+ * @throws \CoreException
+ */
+ public function GetSubItemValue($sItemCode, $value, $oHostObject = null)
+ {
+ $oStopWatch = $value;
+ switch ($sItemCode) {
+ case 'timespent':
+ return $oStopWatch->GetTimeSpent();
+ case 'started':
+ return $oStopWatch->GetStartDate();
+ case 'laststart':
+ return $oStopWatch->GetLastStartDate();
+ case 'stopped':
+ return $oStopWatch->GetStopDate();
+ }
+
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ return $oStopWatch->GetThresholdDate($iThreshold);
+ case 'passed':
+ return $oStopWatch->IsThresholdPassed($iThreshold);
+ case 'triggered':
+ return $oStopWatch->IsThresholdTriggered($iThreshold);
+ case 'overrun':
+ return $oStopWatch->GetOverrun($iThreshold);
+ }
+ }
+ }
+
+ throw new CoreException("Unknown item code '$sItemCode' for attribute " . $this->GetHostClass() . '::' . $this->GetCode());
+ }
+
+
+ public function GetSubItemSearchType($sItemCode)
+ {
+ switch ($sItemCode) {
+ case 'timespent':
+ return static::SEARCH_WIDGET_TYPE_NUMERIC; //seconds
+ case 'started':
+ case 'laststart':
+ case 'stopped':
+ return static::SEARCH_WIDGET_TYPE_DATE_TIME; //timestamp
+ }
+
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ return static::SEARCH_WIDGET_TYPE_DATE_TIME; //timestamp
+ case 'passed':
+ case 'triggered':
+ return static::SEARCH_WIDGET_TYPE_ENUM; //booleans, used in conjuction with GetSubItemAllowedValues and IsSubItemNullAllowed
+ case 'overrun':
+ return static::SEARCH_WIDGET_TYPE_NUMERIC; //seconds
+ }
+ }
+ }
+
+ return static::SEARCH_WIDGET_TYPE_RAW;
+ }
+
+ public function GetSubItemAllowedValues($sItemCode, $aArgs = array(), $sContains = '')
+ {
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'passed':
+ case 'triggered':
+ return array(
+ 0 => $this->GetBooleanLabel(0),
+ 1 => $this->GetBooleanLabel(1),
+ );
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public function IsSubItemNullAllowed($sItemCode, $bDefaultValue)
+ {
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'passed':
+ case 'triggered':
+ return false;
+ }
+ }
+ }
+
+ return $bDefaultValue;
+ }
+
+ protected function GetBooleanLabel($bValue)
+ {
+ $sDictKey = $bValue ? 'yes' : 'no';
+
+ return Dict::S('BooleanLabel:' . $sDictKey, 'def:' . $sDictKey);
+ }
+
+ public function GetSubItemAsHTMLForHistory($sItemCode, $sValue)
+ {
+ $sHtml = null;
+ switch ($sItemCode) {
+ case 'timespent':
+ $sHtml = (int)$sValue ? Str::pure2html(AttributeDuration::FormatDuration($sValue)) : null;
+ break;
+ case 'started':
+ case 'laststart':
+ case 'stopped':
+ $sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(), (int)$sValue) : null;
+ break;
+
+ default:
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ $sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(),
+ (int)$sValue) : null;
+ break;
+ case 'passed':
+ case 'triggered':
+ $sHtml = $this->GetBooleanLabel((int)$sValue);
+ break;
+ case 'overrun':
+ $sHtml = (int)$sValue > 0 ? Str::pure2html(AttributeDuration::FormatDuration((int)$sValue)) : '';
+ }
+ }
+ }
+ }
+
+ return $sHtml;
+ }
+
+ public function GetSubItemAsPlainText($sItemCode, $value)
+ {
+ $sRet = $value;
+
+ switch ($sItemCode) {
+ case 'timespent':
+ $sRet = AttributeDuration::FormatDuration($value);
+ break;
+ case 'started':
+ case 'laststart':
+ case 'stopped':
+ if (is_null($value)) {
+ $sRet = ''; // Undefined
+ } else {
+ $oDateTime = new DateTime();
+ $oDateTime->setTimestamp($value);
+ $oDateTimeFormat = AttributeDateTime::GetFormat();
+ $sRet = $oDateTimeFormat->Format($oDateTime);
+ }
+ break;
+
+ default:
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ if ($value) {
+ if (is_int($value)) {
+ $sDate = date(AttributeDateTime::GetInternalFormat(), $value);
+ $sRet = AttributeDeadline::FormatDeadline($sDate);
+ } else {
+ $sRet = $value;
+ }
+ } else {
+ $sRet = '';
+ }
+ break;
+ case 'passed':
+ case 'triggered':
+ $sRet = $this->GetBooleanLabel($value);
+ break;
+ case 'overrun':
+ $sRet = AttributeDuration::FormatDuration($value);
+ break;
+ }
+ }
+ }
+ }
+
+ return $sRet;
+ }
+
+ public function GetSubItemAsHTML($sItemCode, $value)
+ {
+ $sHtml = $value;
+
+ switch ($sItemCode) {
+ case 'timespent':
+ $sHtml = Str::pure2html(AttributeDuration::FormatDuration($value));
+ break;
+ case 'started':
+ case 'laststart':
+ case 'stopped':
+ if (is_null($value)) {
+ $sHtml = ''; // Undefined
+ } else {
+ $oDateTime = new DateTime();
+ $oDateTime->setTimestamp($value);
+ $oDateTimeFormat = AttributeDateTime::GetFormat();
+ $sHtml = Str::pure2html($oDateTimeFormat->Format($oDateTime));
+ }
+ break;
+
+ default:
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ if ($value) {
+ $sDate = date(AttributeDateTime::GetInternalFormat(), $value);
+ $sHtml = Str::pure2html(AttributeDeadline::FormatDeadline($sDate));
+ } else {
+ $sHtml = '';
+ }
+ break;
+ case 'passed':
+ case 'triggered':
+ $sHtml = $this->GetBooleanLabel($value);
+ break;
+ case 'overrun':
+ $sHtml = Str::pure2html(AttributeDuration::FormatDuration($value));
+ break;
+ }
+ }
+ }
+ }
+
+ return $sHtml;
+ }
+
+ public function GetSubItemAsCSV(
+ $sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"', $bConvertToPlainText = false
+ )
+ {
+ $sFrom = array("\r\n", $sTextQualifier);
+ $sTo = array("\n", $sTextQualifier . $sTextQualifier);
+ $sEscaped = str_replace($sFrom, $sTo, (string)$value);
+ $sRet = $sTextQualifier . $sEscaped . $sTextQualifier;
+
+ switch ($sItemCode) {
+ case 'timespent':
+ $sRet = $sTextQualifier . AttributeDuration::FormatDuration($value) . $sTextQualifier;
+ break;
+ case 'started':
+ case 'laststart':
+ case 'stopped':
+ if ($value !== null) {
+ $oDateTime = new DateTime();
+ $oDateTime->setTimestamp($value);
+ $oDateTimeFormat = AttributeDateTime::GetFormat();
+ $sRet = $sTextQualifier . $oDateTimeFormat->Format($oDateTime) . $sTextQualifier;
+ }
+ break;
+
+ default:
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ if ($value != '') {
+ $oDateTime = new DateTime();
+ $oDateTime->setTimestamp($value);
+ $oDateTimeFormat = AttributeDateTime::GetFormat();
+ $sRet = $sTextQualifier . $oDateTimeFormat->Format($oDateTime) . $sTextQualifier;
+ }
+ break;
+
+ case 'passed':
+ case 'triggered':
+ $sRet = $sTextQualifier . $this->GetBooleanLabel($value) . $sTextQualifier;
+ break;
+
+ case 'overrun':
+ $sRet = $sTextQualifier . AttributeDuration::FormatDuration($value) . $sTextQualifier;
+ break;
+ }
+ }
+ }
+ }
+
+ return $sRet;
+ }
+
+ public function GetSubItemAsXML($sItemCode, $value)
+ {
+ $sRet = Str::pure2xml((string)$value);
+
+ switch ($sItemCode) {
+ case 'timespent':
+ case 'started':
+ case 'laststart':
+ case 'stopped':
+ break;
+
+ default:
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ case 'overrun':
+ break;
+
+ case 'triggered':
+ case 'passed':
+ $sRet = $this->GetBooleanLabel($value);
+ break;
+ }
+ }
+ }
+ }
+
+ return $sRet;
+ }
+
+ /**
+ * Implemented for the HTML spreadsheet format!
+ *
+ * @param string $sItemCode
+ * @param \ormStopWatch $value
+ *
+ * @return false|string
+ */
+ public function GetSubItemAsEditValue($sItemCode, $value)
+ {
+ $sRet = $value;
+
+ switch ($sItemCode) {
+ case 'timespent':
+ break;
+
+ case 'started':
+ case 'laststart':
+ case 'stopped':
+ if (is_null($value)) {
+ $sRet = ''; // Undefined
+ } else {
+ $sRet = date((string)AttributeDateTime::GetFormat(), $value);
+ }
+ break;
+
+ default:
+ foreach ($this->ListThresholds() as $iThreshold => $aFoo) {
+ $sThPrefix = $iThreshold . '_';
+ if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) {
+ // The current threshold is concerned
+ $sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+ switch ($sThresholdCode) {
+ case 'deadline':
+ if ($value) {
+ $sRet = date((string)AttributeDateTime::GetFormat(), $value);
+ } else {
+ $sRet = '';
+ }
+ break;
+ case 'passed':
+ case 'triggered':
+ $sRet = $this->GetBooleanLabel($value);
+ break;
+ case 'overrun':
+ break;
+ }
+ }
+ }
+ }
+
+ return $sRet;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ // A stopwatch always has a value
+ return true;
+ }
+
+ public function RecordAttChange(DBObject $oObject, $original, $value): void
+ {
+ // Stop watches - record changes for sub items only (they are visible, the rest is not visible)
+ //
+ foreach ($this->ListSubItems() as $sSubItemAttCode => $oSubItemAttDef) {
+ $item_value = $this->GetSubItemValue($oSubItemAttDef->Get('item_code'), $value, $oObject);
+ $item_original = $this->GetSubItemValue($oSubItemAttDef->Get('item_code'), $original, $oObject);
+
+ if ($item_value != $item_original) {
+ $oMyChangeOp = MetaModel::NewObject(CMDBChangeOpSetAttributeScalar::class);
+ $oMyChangeOp->Set("objclass", get_class($oObject));
+ $oMyChangeOp->Set("objkey", $oObject->GetKey());
+ $oMyChangeOp->Set("attcode", $sSubItemAttCode);
+
+ $oMyChangeOp->Set("oldvalue", $item_original);
+ $oMyChangeOp->Set("newvalue", $item_value);
+
+ $oMyChangeOp->DBInsertNoReload();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeString.php b/sources/Core/AttributeDefinition/AttributeString.php
new file mode 100644
index 0000000000..89fdf0794b
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeString.php
@@ -0,0 +1,187 @@
+GetSQLColSpec() : '');
+ }
+
+ public function GetValidationPattern()
+ {
+ $sPattern = $this->GetOptional('validation_pattern', '');
+ if (empty($sPattern)) {
+ return parent::GetValidationPattern();
+ } else {
+ return $sPattern;
+ }
+ }
+
+ public function CheckFormat($value)
+ {
+ $sRegExp = $this->GetValidationPattern();
+ if (empty($sRegExp)) {
+ return true;
+ } else {
+ $sRegExp = str_replace('/', '\\/', $sRegExp);
+
+ return preg_match("/$sRegExp/", $value);
+ }
+ }
+
+ public function GetMaxSize()
+ {
+ return 255;
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array(
+ "=" => "equals",
+ "!=" => "differs from",
+ "Like" => "equals (no case)",
+ "NotLike" => "differs from (no case)",
+ "Contains" => "contains",
+ "Begins with" => "begins with",
+ "Finishes with" => "finishes with"
+ );
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return "Contains";
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ $sQValue = CMDBSource::Quote($value);
+ switch ($sOpCode) {
+ case '=':
+ case '!=':
+ return $this->GetSQLExpr() . " $sOpCode $sQValue";
+ case 'Begins with':
+ return $this->GetSQLExpr() . " LIKE " . CMDBSource::Quote("$value%");
+ case 'Finishes with':
+ return $this->GetSQLExpr() . " LIKE " . CMDBSource::Quote("%$value");
+ case 'Contains':
+ return $this->GetSQLExpr() . " LIKE " . CMDBSource::Quote("%$value%");
+ case 'NotLike':
+ return $this->GetSQLExpr() . " NOT LIKE $sQValue";
+ case 'Like':
+ default:
+ return $this->GetSQLExpr() . " LIKE $sQValue";
+ }
+ }
+
+ public function GetNullValue()
+ {
+ return '';
+ }
+
+ public function IsNull($proposedValue)
+ {
+ return ($proposedValue == '');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ return utils::IsNotNullOrEmptyString($proposedValue);
+ }
+
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (is_null($proposedValue)) {
+ return '';
+ }
+
+ return (string)$proposedValue;
+ }
+
+ public function ScalarToSQL($value)
+ {
+ if (!is_string($value) && !is_null($value)) {
+ throw new CoreWarning('Expected the attribute value to be a string', array(
+ 'found_type' => gettype($value),
+ 'value' => $value,
+ 'class' => $this->GetHostClass(),
+ 'attribute' => $this->GetCode()
+ ));
+ }
+
+ return $value;
+ }
+
+ public function GetAsCSV(
+ $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ $sFrom = array("\r\n", $sTextQualifier);
+ $sTo = array("\n", $sTextQualifier . $sTextQualifier);
+ $sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
+
+ return $sTextQualifier . $sEscaped . $sTextQualifier;
+ }
+
+ public function GetDisplayStyle()
+ {
+ return $this->GetOptional('display_style', 'select');
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\StringField';
+ }
+
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ if ($oFormField === null) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ }
+ parent::MakeFormField($oObject, $oFormField);
+
+ return $oFormField;
+ }
+
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeSubItem.php b/sources/Core/AttributeDefinition/AttributeSubItem.php
new file mode 100644
index 0000000000..4b5d021c8a
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeSubItem.php
@@ -0,0 +1,262 @@
+GetTargetAttDef();
+
+ return $oParent->GetSubItemSearchType($this->Get('item_code'));
+ }
+
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ /** @var AttributeStopWatch $oParent */
+ $oParent = $this->GetTargetAttDef();
+
+ return $oParent->GetSubItemAllowedValues($this->Get('item_code'), $aArgs, $sContains);
+ }
+
+ public function IsNullAllowed()
+ {
+ /** @var AttributeStopWatch $oParent */
+ $oParent = $this->GetTargetAttDef();
+
+ $bDefaultValue = parent::IsNullAllowed();
+
+ return $oParent->IsSubItemNullAllowed($this->Get('item_code'), $bDefaultValue);
+ }
+
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array('target_attcode', 'item_code'));
+ }
+
+ public function GetParentAttCode()
+ {
+ return $this->Get("target_attcode");
+ }
+
+ /**
+ * Helper : get the attribute definition to which the execution will be forwarded
+ */
+ public function GetTargetAttDef()
+ {
+ $sClass = $this->GetHostClass();
+ $oParentAttDef = MetaModel::GetAttributeDef($sClass, $this->Get('target_attcode'));
+
+ return $oParentAttDef;
+ }
+
+ public function GetEditClass()
+ {
+ return "";
+ }
+
+ public function GetValuesDef()
+ {
+ return null;
+ }
+
+ public static function IsBasedOnDBColumns()
+ {
+ return true;
+ }
+
+ public static function IsScalar()
+ {
+ return true;
+ }
+
+ public function IsWritable()
+ {
+ return false;
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return null;
+ }
+
+// public function IsNullAllowed() {return false;}
+
+ public static function LoadInObject()
+ {
+ return false;
+ } // if this verb returns false, then GetValues must be implemented
+
+ /**
+ * Used by DBOBject::Get()
+ *
+ * @param \DBObject $oHostObject
+ *
+ * @return \AttributeSubItem
+ * @throws \CoreException
+ */
+ public function GetValue($oHostObject)
+ {
+ /** @var \AttributeStopWatch $oParent */
+ $oParent = $this->GetTargetAttDef();
+ $parentValue = $oHostObject->GetStrict($oParent->GetCode());
+ $res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject);
+
+ return $res;
+ }
+
+ //
+// protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
+
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ return array();
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return "=";
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ $sQValue = CMDBSource::Quote($value);
+ switch ($sOpCode) {
+ case '!=':
+ return $this->GetSQLExpr() . " != $sQValue";
+ break;
+ case '=':
+ default:
+ return $this->GetSQLExpr() . " = $sQValue";
+ }
+ }
+
+ public function GetSQLExpressions($sPrefix = '')
+ {
+ $oParent = $this->GetTargetAttDef();
+ $res = $oParent->GetSubItemSQLExpression($this->Get('item_code'));
+
+ return $res;
+ }
+
+ public function GetAsPlainText($value, $oHostObject = null, $bLocalize = true)
+ {
+ $oParent = $this->GetTargetAttDef();
+ $res = $oParent->GetSubItemAsPlainText($this->Get('item_code'), $value);
+
+ return $res;
+ }
+
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ $oParent = $this->GetTargetAttDef();
+ $res = $oParent->GetSubItemAsHTML($this->Get('item_code'), $value);
+
+ return $res;
+ }
+
+ public function GetAsHTMLForHistory($value, $oHostObject = null, $bLocalize = true)
+ {
+ $oParent = $this->GetTargetAttDef();
+ $res = $oParent->GetSubItemAsHTMLForHistory($this->Get('item_code'), $value);
+
+ return $res;
+ }
+
+ public function GetAsCSV(
+ $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ $oParent = $this->GetTargetAttDef();
+ $res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator, $sTextQualifier,
+ $bConvertToPlainText);
+
+ return $res;
+ }
+
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ $oParent = $this->GetTargetAttDef();
+ $res = $oParent->GetSubItemAsXML($this->Get('item_code'), $value);
+
+ return $res;
+ }
+
+ /**
+ * As of now, this function must be implemented to have the value in spreadsheet format
+ */
+ public function GetEditValue($value, $oHostObj = null)
+ {
+ $oParent = $this->GetTargetAttDef();
+ $res = $oParent->GetSubItemAsEditValue($this->Get('item_code'), $value);
+
+ return $res;
+ }
+
+ public function IsPartOfFingerprint()
+ {
+ return false;
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\LabelField';
+ }
+
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ if ($oFormField === null) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ }
+ parent::MakeFormField($oObject, $oFormField);
+
+ // Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition
+ $sAttCode = $this->GetCode();
+ $oFormField->SetCurrentValue(html_entity_decode($oObject->GetAsHTML($sAttCode), ENT_QUOTES, 'UTF-8'));
+ $oFormField->SetReadOnly(true);
+
+ return $oFormField;
+ }
+
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeTable.php b/sources/Core/AttributeDefinition/AttributeTable.php
new file mode 100644
index 0000000000..09620a48a9
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeTable.php
@@ -0,0 +1,164 @@
+ 0;
+ }
+
+
+ public function GetEditValue($sValue, $oHostObj = null)
+ {
+ return '';
+ }
+
+ // Facilitate things: allow the user to Set the value from a string
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (is_null($proposedValue)) {
+ return array();
+ } else {
+ if (!is_array($proposedValue)) {
+ return array(0 => array(0 => $proposedValue));
+ }
+ }
+
+ return $proposedValue;
+ }
+
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ try {
+ $value = @unserialize($aCols[$sPrefix . '']);
+ if ($value === false) {
+ $value = @json_decode($aCols[$sPrefix . ''], true);
+ if (is_null($value)) {
+ $value = false;
+ }
+ }
+ if ($value === false) {
+ $value = $this->MakeRealValue($aCols[$sPrefix . ''], null);
+ }
+ } catch (Exception $e) {
+ $value = $this->MakeRealValue($aCols[$sPrefix . ''], null);
+ }
+
+ return $value;
+ }
+
+ public function GetSQLValues($value)
+ {
+ $aValues = array();
+ try {
+ $sSerializedValue = serialize($value);
+ } catch (Exception $e) {
+ $sSerializedValue = json_encode($value);
+ }
+ $aValues[$this->Get("sql")] = $sSerializedValue;
+
+ return $aValues;
+ }
+
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if (!is_array($value)) {
+ throw new CoreException('Expecting an array', array('found' => get_class($value)));
+ }
+ if (count($value) == 0) {
+ return "";
+ }
+
+ $sRes = "";
+ $sRes .= "";
+ foreach ($value as $iRow => $aRawData) {
+ $sRes .= "";
+ foreach ($aRawData as $iCol => $cell) {
+ // Note: avoid the warning in case the cell is made of an array
+ $sCell = @Str::pure2html((string)$cell);
+ $sCell = str_replace("\n", "
\n", $sCell);
+ $sRes .= "$sCell | ";
+ }
+ $sRes .= "
";
+ }
+ $sRes .= "";
+ $sRes .= "
";
+
+ return $sRes;
+ }
+
+ public function GetAsCSV(
+ $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ // Not implemented
+ return '';
+ }
+
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if (!is_array($value) || count($value) == 0) {
+ return "";
+ }
+
+ $sRes = "";
+ foreach ($value as $iRow => $aRawData) {
+ $sRes .= "";
+ foreach ($aRawData as $iCol => $cell) {
+ $sCell = Str::pure2xml((string)$cell);
+ $sRes .= "| $sCell | ";
+ }
+ $sRes .= "
";
+ }
+
+ return $sRes;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeTagSet.php b/sources/Core/AttributeDefinition/AttributeTagSet.php
new file mode 100644
index 0000000000..c58a5ad23f
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeTagSet.php
@@ -0,0 +1,641 @@
+aCSSClasses[] = 'attribute-tag-set';
+ }
+
+ public function GetEditClass()
+ {
+ return 'TagSet';
+ }
+
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array('tag_code_max_len'));
+ }
+
+ /**
+ * @param \ormTagSet $oValue
+ *
+ * @param $aArgs
+ *
+ * @return string JSON to be used in the itop.tagset_widget JQuery widget
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function GetJsonForWidget($oValue, $aArgs = array())
+ {
+ $aJson = array();
+
+ // possible_values
+ $aTagSetObjectData = $this->GetAllowedValues($aArgs);
+ $aTagSetKeyValData = array();
+ foreach ($aTagSetObjectData as $sTagCode => $sTagLabel) {
+ $aTagSetKeyValData[] = [
+ 'code' => $sTagCode,
+ 'label' => $sTagLabel,
+ ];
+ }
+ $aJson['possible_values'] = $aTagSetKeyValData;
+
+ if (is_null($oValue)) {
+ $aJson['partial_values'] = array();
+ $aJson['orig_value'] = array();
+ $aJson['added'] = array();
+ $aJson['removed'] = array();
+ } else {
+ $aJson['orig_value'] = array_merge($oValue->GetValues(), $oValue->GetModified());
+ $aJson['added'] = $oValue->GetAdded();
+ $aJson['removed'] = $oValue->GetRemoved();
+
+ if ($oValue->DisplayPartial()) {
+ // For bulk updates
+ $aJson['partial_values'] = $oValue->GetModified();
+ } else {
+ // For simple updates
+ $aJson['partial_values'] = array();
+ }
+ }
+
+
+ $iMaxTags = $this->GetMaxItems();
+ $aJson['max_items_allowed'] = $iMaxTags;
+
+ return json_encode($aJson);
+ }
+
+ public function FromStringToArray($proposedValue, $sDefaultSepItem = ',')
+ {
+ $aValues = array();
+ if (!empty($proposedValue)) {
+ foreach (explode(' ', $proposedValue) as $sCode) {
+ $sValue = trim($sCode);
+ $aValues[] = $sValue;
+ }
+ }
+ return $aValues;
+ }
+
+ /**
+ * Extract all existing tags from a string and ignore bad tags
+ *
+ * @param $sValue
+ * @param bool $bNoLimit : don't apply the maximum tag limit
+ *
+ * @return \ormTagSet
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ */
+ public function GetExistingTagsFromString($sValue, $bNoLimit = false)
+ {
+ $aTagCodes = $this->FromStringToArray("$sValue");
+ $sAttCode = $this->GetCode();
+ $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode);
+ if ($bNoLimit) {
+ $oTagSet = new ormTagSet($sClass, $sAttCode, 0);
+ } else {
+ $oTagSet = new ormTagSet($sClass, $sAttCode, $this->GetMaxItems());
+ }
+ $aGoodTags = array();
+ foreach ($aTagCodes as $sTagCode) {
+ if ($sTagCode === '') {
+ continue;
+ }
+ if ($oTagSet->IsValidTag($sTagCode)) {
+ $aGoodTags[] = $sTagCode;
+ if (!$bNoLimit && (count($aGoodTags) === $this->GetMaxItems())) {
+ // extra and bad tags are ignored
+ break;
+ }
+ }
+ }
+ $oTagSet->SetValues($aGoodTags);
+
+ return $oTagSet;
+ }
+
+ public function GetTagCodeMaxLength()
+ {
+ return $this->Get('tag_code_max_len');
+ }
+
+ public function GetEditValue($value, $oHostObj = null)
+ {
+ if (empty($value)) {
+ return '';
+ }
+ if ($value instanceof ormTagSet) {
+ $aValues = $value->GetValues();
+
+ return implode(' ', $aValues);
+ }
+
+ return '';
+ }
+
+ public function GetMaxSize()
+ {
+ return max(255, ($this->GetMaxItems() * $this->GetTagCodeMaxLength()) + 1);
+ }
+
+ public function Equals($val1, $val2)
+ {
+ if (($val1 instanceof ormTagSet) && ($val2 instanceof ormTagSet)) {
+ return $val1->Equals($val2);
+ }
+
+ return ($val1 == $val2);
+ }
+
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ $sAttCode = $this->GetCode();
+ $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode);
+ $aAllowedTags = TagSetFieldData::GetAllowedValues($sClass, $sAttCode);
+ $aAllowedValues = array();
+ foreach ($aAllowedTags as $oAllowedTag) {
+ $aAllowedValues[$oAllowedTag->Get('code')] = $oAllowedTag->Get('label');
+ }
+
+ return $aAllowedValues;
+ }
+
+ /**
+ * @param array $aCols
+ * @param string $sPrefix
+ *
+ * @return mixed
+ * @throws \CoreException
+ * @throws \Exception
+ */
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ $sValue = $aCols["$sPrefix"];
+
+ return $this->GetExistingTagsFromString($sValue);
+ }
+
+ /**
+ * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
+ *
+ * @param $proposedValue
+ * @param $oHostObj
+ *
+ * @param bool $bIgnoreErrors
+ *
+ * @return mixed
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ */
+ public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false)
+ {
+ $oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
+ if (is_string($proposedValue) && !empty($proposedValue)) {
+ $sJsonFromWidget = json_decode($proposedValue, true);
+ if (is_null($sJsonFromWidget)) {
+ $proposedValue = trim("$proposedValue");
+ $aTagCodes = $this->FromStringToArray($proposedValue);
+ $oTagSet->SetValues($aTagCodes);
+ }
+ } elseif ($proposedValue instanceof ormTagSet) {
+ $oTagSet = $proposedValue;
+ }
+
+ return $oTagSet;
+ }
+
+ /**
+ * Get the value from a given string (plain text, CSV import)
+ *
+ * @param string $sProposedValue
+ * @param bool $bLocalizedValue
+ * @param string $sSepItem
+ * @param string $sSepAttribute
+ * @param string $sSepValue
+ * @param string $sAttributeQualifier
+ *
+ * @return mixed null if no match could be found
+ * @throws \Exception
+ */
+ public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null)
+ {
+ if (is_null($sSepItem) || empty($sSepItem)) {
+ $sSepItem = MetaModel::GetConfig()->Get('tag_set_item_separator');
+ }
+ if (!empty($sProposedValue)) {
+ $oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()),
+ $this->GetCode(), $this->GetMaxItems());
+ $aLabels = explode($sSepItem, $sProposedValue);
+ $aCodes = array();
+ foreach ($aLabels as $sTagLabel) {
+ if (!empty($sTagLabel)) {
+ $aCodes[] = ($bLocalizedValue) ? $oTagSet->GetTagFromLabel($sTagLabel) : $sTagLabel;
+ }
+ }
+ $sProposedValue = implode(' ', $aCodes);
+ }
+
+ return $this->MakeRealValue($sProposedValue, null);
+ }
+
+ public function GetNullValue()
+ {
+ return new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ $oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems());
+ $oTagSet->SetValues([]);
+ return $oTagSet;
+ }
+
+ public function IsNull($proposedValue)
+ {
+ if (is_null($proposedValue)) {
+ return true;
+ }
+
+ /** @var \ormTagSet $proposedValue */
+ return count($proposedValue->GetValues()) == 0;
+ }
+
+ /**
+ * To be overloaded for localized enums
+ *
+ * @param $sValue
+ *
+ * @return string label corresponding to the given value (in plain text)
+ * @throws \CoreWarning
+ * @throws \Exception
+ */
+ public function GetValueLabel($sValue)
+ {
+ if (empty($sValue)) {
+ return '';
+ }
+ if (is_string($sValue)) {
+ $sValue = $this->GetExistingTagsFromString($sValue);
+ }
+ if ($sValue instanceof ormTagSet) {
+ $aValues = $sValue->GetLabels();
+
+ return implode(', ', $aValues);
+ }
+ throw new CoreWarning('Expected the attribute value to be a TagSet', array(
+ 'found_type' => gettype($sValue),
+ 'value' => $sValue,
+ 'class' => $this->GetHostClass(),
+ 'attribute' => $this->GetCode()
+ ));
+ }
+
+ /**
+ * @param $value
+ *
+ * @return string
+ * @throws \CoreWarning
+ */
+ public function ScalarToSQL($value)
+ {
+ if (empty($value)) {
+ return '';
+ }
+ if ($value instanceof ormTagSet) {
+ $aValues = $value->GetValues();
+
+ return implode(' ', $aValues);
+ }
+ throw new CoreWarning('Expected the attribute value to be a TagSet', array(
+ 'found_type' => gettype($value),
+ 'value' => $value,
+ 'class' => $this->GetHostClass(),
+ 'attribute' => $this->GetCode()
+ ));
+ }
+
+ /**
+ * @param $value
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string|null
+ *
+ * @throws \CoreException
+ * @throws \Exception
+ */
+ public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if ($value instanceof ormTagSet) {
+ if ($bLocalize) {
+ $aValues = $value->GetTags();
+ } else {
+ $aValues = $value->GetValues();
+ }
+ if (empty($aValues)) {
+ return '';
+ }
+
+ return $this->GenerateViewHtmlForValues($aValues);
+ }
+ if (is_string($value)) {
+ try {
+ $oValue = $this->MakeRealValue($value, $oHostObject);
+
+ return $this->GetAsHTML($oValue, $oHostObject, $bLocalize);
+ } catch (Exception $e) {
+ // unknown tags are present display the code instead
+ }
+ $aTagCodes = $this->FromStringToArray($value);
+ $aValues = array();
+ $oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()),
+ $this->GetCode(), $this->GetMaxItems());
+ foreach ($aTagCodes as $sTagCode) {
+ try {
+ $oTagSet->Add($sTagCode);
+ } catch (Exception $e) {
+ $aValues[] = $sTagCode;
+ }
+ }
+ $sHTML = '';
+ if (!empty($aValues)) {
+ $sHTML .= $this->GenerateViewHtmlForValues($aValues, 'attribute-set-item-undefined');
+ }
+ $aValues = $oTagSet->GetTags();
+ if (!empty($aValues)) {
+ $sHTML .= $this->GenerateViewHtmlForValues($aValues);
+ }
+
+ return $sHTML;
+ }
+
+ return parent::GetAsHTML($value, $oHostObject, $bLocalize);
+ }
+
+ // Do not display friendly names in the history of change
+ public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null)
+ {
+ $sResult = Dict::Format('Change:AttName_Changed', $this->GetLabel()) . ", ";
+
+ $aNewValues = $this->FromStringToArray($sNewValue);
+ $aOldValues = $this->FromStringToArray($sOldValue);
+
+ $aDelta['removed'] = array_diff($aOldValues, $aNewValues);
+ $aDelta['added'] = array_diff($aNewValues, $aOldValues);
+
+ $aAllowedTags = TagSetFieldData::GetAllowedValues(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode());
+
+ if (!empty($aDelta['removed'])) {
+ $aRemoved = array();
+ foreach ($aDelta['removed'] as $idx => $sTagCode) {
+ if (empty($sTagCode)) {
+ continue;
+ }
+ $sTagLabel = $sTagCode;
+ foreach ($aAllowedTags as $oTag) {
+ if ($sTagCode === $oTag->Get('code')) {
+ $sTagLabel = $oTag->Get('label');
+ }
+ }
+ $aRemoved[] = $sTagLabel;
+ }
+
+ $sRemoved = $this->GenerateViewHtmlForValues($aRemoved, 'history-removed');
+ if (!empty($sRemoved)) {
+ $sResult .= Dict::Format('Change:LinkSet:Removed', $sRemoved);
+ }
+ }
+
+ if (!empty($aDelta['added'])) {
+ if (!empty($sRemoved)) {
+ $sResult .= ', ';
+ }
+
+ $aAdded = array();
+ foreach ($aDelta['added'] as $idx => $sTagCode) {
+ if (empty($sTagCode)) {
+ continue;
+ }
+ $sTagLabel = $sTagCode;
+ foreach ($aAllowedTags as $oTag) {
+ if ($sTagCode === $oTag->Get('code')) {
+ $sTagLabel = $oTag->Get('label');
+ }
+ }
+ $aAdded[] = $sTagLabel;
+ }
+
+ $sAdded = $this->GenerateViewHtmlForValues($aAdded, 'history-added');
+ if (!empty($sAdded)) {
+ $sResult .= Dict::Format('Change:LinkSet:Added', $sAdded);
+ }
+ }
+
+ return $sResult;
+ }
+
+ /**
+ * HTML representation of a list of tags (read-only)
+ * accept a list of strings or a list of TagSetFieldData
+ *
+ * @param array $aValues
+ * @param string $sCssClass
+ * @param bool $bWithLink if true will generate a link, otherwise just a "a" tag without href
+ *
+ * @return string
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function GenerateViewHtmlForValues($aValues, $sCssClass = '', $bWithLink = true)
+ {
+ if (empty($aValues)) {
+ return '';
+ }
+ $sHtml = '';
+ foreach ($aValues as $oTag) {
+ if ($oTag instanceof TagSetFieldData) {
+ $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode());
+ $sAttCode = $this->GetCode();
+ $sTagCode = $oTag->Get('code');
+ $sTagLabel = $oTag->Get('label');
+ $sTagDescription = $oTag->Get('description');
+ $oFilter = DBSearch::FromOQL("SELECT $sClass WHERE $sAttCode MATCHES '$sTagCode'");
+ $oAppContext = new ApplicationContext();
+ $sContext = $oAppContext->GetForLink(true);
+ $sUIPage = cmdbAbstractObject::ComputeStandardUIPage($oFilter->GetClass());
+ $sFilter = rawurlencode($oFilter->serialize());
+
+ $sLink = '';
+ if ($bWithLink && $this->bDisplayLink) {
+ $sUrl = utils::GetAbsoluteUrlAppRoot() . "pages/$sUIPage?operation=search&filter=" . $sFilter . $sContext;
+ $sLink = ' href="' . $sUrl . '"';
+ }
+
+ $sLabelForHtml = utils::EscapeHtml($sTagLabel);
+ $sDescriptionForHtml = utils::EscapeHtml($sTagDescription);
+ if (empty($sTagDescription)) {
+ $sTooltipContent = $sTagLabel;
+ $sTooltipHtmlEnabled = 'false';
+ } else {
+ $sTagLabelEscaped = utils::EscapeHtml($sTagLabel);
+ $sTooltipContent = <<$sTagLabelEscaped
+$sTagDescription
+HTML;
+ $sTooltipHtmlEnabled = 'true';
+ }
+ $sTooltipContent = utils::HtmlEntities($sTooltipContent);
+
+ $sHtml .= '' . $sLabelForHtml . '';
+ } else {
+ $sHtml .= '' . utils::EscapeHtml($oTag) . '';
+ }
+ }
+ $sHtml .= '';
+
+ return $sHtml;
+ }
+
+ /**
+ * @param $value
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string
+ *
+ */
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ if (is_object($value) && ($value instanceof ormTagSet)) {
+ $sRes = "\n";
+ if ($bLocalize) {
+ $aValues = $value->GetLabels();
+ } else {
+ $aValues = $value->GetValues();
+ }
+ if (!empty($aValues)) {
+ $sRes .= '' . implode('', $aValues) . '';
+ }
+ $sRes .= "\n";
+ } else {
+ $sRes = '';
+ }
+
+ return $sRes;
+ }
+
+ /**
+ * List the available verbs for 'GetForTemplate'
+ */
+ public function EnumTemplateVerbs()
+ {
+ return array(
+ '' => 'Plain text representation',
+ 'html' => 'HTML representation (unordered list)',
+ );
+ }
+
+ /**
+ * Get various representations of the value, for insertion into a template (e.g. in Notifications)
+ *
+ * @param mixed $value The current value of the field
+ * @param string $sVerb The verb specifying the representation of the value
+ * @param DBObject $oHostObject The object
+ * @param bool $bLocalize Whether or not to localize the value
+ *
+ * @return string
+ * @throws \Exception
+ */
+ public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
+ {
+ if (is_object($value) && ($value instanceof ormTagSet)) {
+ if ($bLocalize) {
+ $aValues = $value->GetLabels();
+ $sSep = ', ';
+ } else {
+ $aValues = $value->GetValues();
+ $sSep = ' ';
+ }
+
+ switch ($sVerb) {
+ case '':
+ return implode($sSep, $aValues);
+
+ case 'html':
+ return '- ' . implode("
- ", $aValues) . '
';
+
+ default:
+ throw new Exception("Unknown verb '$sVerb' for attribute " . $this->GetCode() . ' in class ' . get_class($oHostObject));
+ }
+ }
+ throw new CoreUnexpectedValue("Bad value '$value' for attribute " . $this->GetCode() . ' in class ' . get_class($oHostObject));
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param \ormTagSet $value
+ *
+ * @return array
+ */
+ public function GetForJSON($value)
+ {
+ $aRet = array();
+ if (is_object($value) && ($value instanceof ormTagSet)) {
+ $aRet = $value->GetValues();
+ }
+
+ return $aRet;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return \ormTagSet
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \Exception
+ */
+ public function FromJSONToValue($json)
+ {
+ $oSet = new ormTagSet($this->GetHostClass(), $this->GetCode(), $this->GetMaxItems());
+ $oSet->SetValues($json);
+
+ return $oSet;
+ }
+
+ /**
+ * The part of the current attribute in the object's signature, for the supplied value
+ *
+ * @param mixed $value The value of this attribute for the object
+ *
+ * @return string The "signature" for this field/attribute
+ */
+ public function Fingerprint($value)
+ {
+ if ($value instanceof ormTagSet) {
+ $aValues = $value->GetValues();
+
+ return implode(' ', $aValues);
+ }
+
+ return parent::Fingerprint($value);
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\TagSetField';
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeTemplateHTML.php b/sources/Core/AttributeDefinition/AttributeTemplateHTML.php
new file mode 100644
index 0000000000..f039fe9ba4
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeTemplateHTML.php
@@ -0,0 +1,53 @@
+name$)
+ *
+ * @package iTopORM
+ */
+class AttributeTemplateHTML extends AttributeText
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
+
+ /**
+ * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
+ *
+ * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited
+ * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9
+ *
+ * @param string $sCode
+ * @param array $aParams
+ *
+ * @throws \Exception
+ * @noinspection SenselessProxyMethodInspection
+ */
+ public function __construct($sCode, $aParams)
+ {
+ parent::__construct($sCode, $aParams);
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ $aColumns = array();
+ $aColumns[$this->Get('sql')] = $this->GetSQLCol();
+ if ($this->GetOptional('format', null) != null) {
+ // Add the extra column only if the property 'format' is specified for the attribute
+ $aColumns[$this->Get('sql') . '_format'] = "ENUM('text','html')";
+ if ($bFullSpec) {
+ $aColumns[$this->Get('sql') . '_format'] .= " DEFAULT 'html'"; // default 'html' is for migrating old records
+ }
+ }
+
+ return $aColumns;
+ }
+
+ /**
+ * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup)
+ *
+ * @return string
+ */
+ public function GetFormat()
+ {
+ return $this->GetOptional('format', 'html'); // Defaults to HTML
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeTemplateString.php b/sources/Core/AttributeDefinition/AttributeTemplateString.php
new file mode 100644
index 0000000000..7c450d4039
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeTemplateString.php
@@ -0,0 +1,28 @@
+name$)
+ *
+ * @package iTopORM
+ */
+class AttributeTemplateString extends AttributeString
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
+
+ /**
+ * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
+ *
+ * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited
+ * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9
+ *
+ * @param string $sCode
+ * @param array $aParams
+ *
+ * @throws \Exception
+ * @noinspection SenselessProxyMethodInspection
+ */
+ public function __construct($sCode, $aParams)
+ {
+ parent::__construct($sCode, $aParams);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeTemplateText.php b/sources/Core/AttributeDefinition/AttributeTemplateText.php
new file mode 100644
index 0000000000..4f57b83134
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeTemplateText.php
@@ -0,0 +1,28 @@
+name$)
+ *
+ * @package iTopORM
+ */
+class AttributeTemplateText extends AttributeText
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
+
+ /**
+ * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
+ *
+ * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited
+ * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9
+ *
+ * @param string $sCode
+ * @param array $aParams
+ *
+ * @throws \Exception
+ * @noinspection SenselessProxyMethodInspection
+ */
+ public function __construct($sCode, $aParams)
+ {
+ parent::__construct($sCode, $aParams);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeText.php b/sources/Core/AttributeDefinition/AttributeText.php
new file mode 100644
index 0000000000..d590886cc2
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeText.php
@@ -0,0 +1,385 @@
+ ?) to an attribute
+ *
+ * @package iTopORM
+ */
+class AttributeText extends AttributeString
+{
+ /**
+ * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
+ *
+ * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited
+ * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9
+ *
+ * @param string $sCode
+ * @param array $aParams
+ *
+ * @throws \Exception
+ * @noinspection SenselessProxyMethodInspection
+ */
+ public function __construct($sCode, $aParams)
+ {
+ parent::__construct($sCode, $aParams);
+ }
+
+ public function GetEditClass()
+ {
+ return ($this->GetFormat() == 'text') ? 'Text' : "HTML";
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "TEXT" . CMDBSource::GetSqlStringColumnDefinition();
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ $aColumns = array();
+ $aColumns[$this->Get('sql')] = $this->GetSQLCol($bFullSpec);
+ if ($this->GetOptional('format', null) != null) {
+ // Add the extra column only if the property 'format' is specified for the attribute
+ $aColumns[$this->Get('sql') . '_format'] = "ENUM('text','html')" . CMDBSource::GetSqlStringColumnDefinition();
+ if ($bFullSpec) {
+ $aColumns[$this->Get('sql') . '_format'] .= " DEFAULT 'text'"; // default 'text' is for migrating old records
+ }
+ }
+
+ return $aColumns;
+ }
+
+ public function GetSQLExpressions($sPrefix = '')
+ {
+ if ($sPrefix == '') {
+ $sPrefix = $this->Get('sql');
+ }
+ $aColumns = array();
+ // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
+ $aColumns[''] = $sPrefix;
+ if ($this->GetOptional('format', null) != null) {
+ // Add the extra column only if the property 'format' is specified for the attribute
+ $aColumns['_format'] = $sPrefix . '_format';
+ }
+
+ return $aColumns;
+ }
+
+ public function GetMaxSize()
+ {
+ // Is there a way to know the current limitation for mysql?
+ // See mysql_field_len()
+ return 65535;
+ }
+
+ public static function RenderWikiHtml($sText, $bWikiOnly = false)
+ {
+ if (!$bWikiOnly) {
+ $sPattern = '/' . str_replace('/', '\/', utils::GetConfig()->Get('url_validation_pattern')) . '/i';
+ if (preg_match_all($sPattern, $sText, $aAllMatches,
+ PREG_SET_ORDER /* important !*/ | PREG_OFFSET_CAPTURE /* important ! */)) {
+ $i = count($aAllMatches);
+ // Replace the URLs by an actual hyperlink ...
+ // Let's do it backwards so that the initial positions are not modified by the replacement
+ // This works if the matches are captured: in the order they occur in the string AND
+ // with their offset (i.e. position) inside the string
+ while ($i > 0) {
+ $i--;
+ $sUrl = $aAllMatches[$i][0][0]; // String corresponding to the main pattern
+ $iPos = $aAllMatches[$i][0][1]; // Position of the main pattern
+ $sText = substr_replace($sText, "$sUrl", $iPos, strlen($sUrl));
+
+ }
+ }
+ }
+ if (preg_match_all(WIKI_OBJECT_REGEXP, $sText, $aAllMatches, PREG_SET_ORDER)) {
+ foreach ($aAllMatches as $iPos => $aMatches) {
+ $sClass = trim($aMatches[1]);
+ $sName = trim($aMatches[2]);
+ $sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null;
+
+ if (MetaModel::IsValidClass($sClass)) {
+ $bFound = false;
+
+ // Try to find by name, then by id
+ if (is_object($oObj = MetaModel::GetObjectByName($sClass, $sName, false /* MustBeFound */))) {
+ $bFound = true;
+ } elseif (is_object($oObj = MetaModel::GetObject($sClass, (int)$sName, false /* MustBeFound */, true))) {
+ $bFound = true;
+ }
+
+ if ($bFound === true) {
+ // Propose a std link to the object
+ $sHyperlinkLabel = (empty($sLabel)) ? $oObj->GetName() : $sLabel;
+ $sText = str_replace($aMatches[0], $oObj->GetHyperlink(null, true, $sHyperlinkLabel), $sText);
+ } else {
+ // Propose a std link to the object
+ $sClassLabel = MetaModel::GetName($sClass);
+ $sToolTipForHtml = utils::EscapeHtml(Dict::Format('Core:UnknownObjectLabel', $sClass, $sName));
+ $sReplacement = "$sClassLabel:$sName" . (!empty($sLabel) ? " ($sLabel)" : "") . "";
+ $sText = str_replace($aMatches[0], $sReplacement, $sText);
+ // Later: propose a link to create a new object
+ // Anyhow... there is no easy way to suggest default values based on the given FRIENDLY name
+ //$sText = preg_replace('/\[\[(.+):(.+)\]\]/', ''.$sName.'', $sText);
+ }
+ }
+ }
+ }
+
+ return $sText;
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ $aStyles = array();
+ if ($this->GetWidth() != '') {
+ $aStyles[] = 'width:' . $this->GetWidth();
+ }
+ if ($this->GetHeight() != '') {
+ $aStyles[] = 'height:' . $this->GetHeight();
+ }
+ $sStyle = '';
+ if (count($aStyles) > 0) {
+ $sStyle = 'style="' . implode(';', $aStyles) . '"';
+ }
+
+ if ($this->GetFormat() == 'text') {
+ $sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize);
+ $sValue = self::RenderWikiHtml($sValue);
+ $sValue = nl2br($sValue);
+
+ return "$sValue
";
+ } else {
+ $sValue = self::RenderWikiHtml($sValue, true /* wiki only */);
+
+ return "" . InlineImage::FixUrls($sValue) . '
';
+ }
+
+ }
+
+ public function GetEditValue($sValue, $oHostObj = null)
+ {
+ // N°4517 - PHP 8.1 compatibility: str_replace call with null cause deprecated message
+ if ($sValue == null) {
+ return '';
+ }
+
+ if ($this->GetFormat() == 'text') {
+ if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) {
+ foreach ($aAllMatches as $iPos => $aMatches) {
+ $sClass = trim($aMatches[1]);
+ $sName = trim($aMatches[2]);
+ $sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null;
+
+ if (MetaModel::IsValidClass($sClass)) {
+ $sClassLabel = MetaModel::GetName($sClass);
+ $sReplacement = "[[$sClassLabel:$sName" . (!empty($sLabel) ? " | $sLabel" : "") . "]]";
+ $sValue = str_replace($aMatches[0], $sReplacement, $sValue);
+ }
+ }
+ }
+ }
+
+ return $sValue;
+ }
+
+ /**
+ * For fields containing a potential markup, return the value without this markup
+ *
+ * @param string $sValue
+ * @param \DBObject $oHostObj
+ *
+ * @return string
+ */
+ public function GetAsPlainText($sValue, $oHostObj = null)
+ {
+ if ($this->GetFormat() == 'html') {
+ return (string)utils::HtmlToText($this->GetEditValue($sValue, $oHostObj));
+ } else {
+ return parent::GetAsPlainText($sValue, $oHostObj);
+ }
+ }
+
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ $sValue = $proposedValue;
+
+ // N°4517 - PHP 8.1 compatibility: str_replace call with null cause deprecated message
+ if ($sValue == null) {
+ return '';
+ }
+
+ switch ($this->GetFormat()) {
+ case 'html':
+ if (($sValue !== null) && ($sValue !== '')) {
+ $sValue = HTMLSanitizer::Sanitize($sValue);
+ }
+ break;
+
+ case 'text':
+ default:
+ if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) {
+ foreach ($aAllMatches as $iPos => $aMatches) {
+ $sClassLabel = trim($aMatches[1]);
+ $sName = trim($aMatches[2]);
+ $sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null;
+
+ if (!MetaModel::IsValidClass($sClassLabel)) {
+ $sClass = MetaModel::GetClassFromLabel($sClassLabel);
+ if ($sClass) {
+ $sReplacement = "[[$sClassLabel:$sName" . (!empty($sLabel) ? " | $sLabel" : "") . "]]";
+ $sValue = str_replace($aMatches[0], $sReplacement, $sValue);
+ }
+ }
+ }
+ }
+ }
+
+ return $sValue;
+ }
+
+ public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
+ {
+ return Str::pure2xml($value);
+ }
+
+ public function GetWidth()
+ {
+ return $this->GetOptional('width', '');
+ }
+
+ public function GetHeight()
+ {
+ return $this->GetOptional('height', '');
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\TextAreaField';
+ }
+
+ /**
+ * @param \DBObject $oObject
+ * @param \Combodo\iTop\Form\Field\TextAreaField $oFormField
+ *
+ * @return \Combodo\iTop\Form\Field\TextAreaField
+ * @throws \CoreException
+ */
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ if ($oFormField === null) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ /** @var \Combodo\iTop\Form\Field\TextAreaField $oFormField */
+ $oFormField = new $sFormFieldClass($this->GetCode(), null, $oObject);
+ $oFormField->SetFormat($this->GetFormat());
+ }
+ parent::MakeFormField($oObject, $oFormField);
+
+ return $oFormField;
+ }
+
+ /**
+ * The actual formatting of the field: either text (=plain text) or html (= text with HTML markup)
+ *
+ * @return string
+ */
+ public function GetFormat()
+ {
+ return $this->GetOptional('format', 'text');
+ }
+
+ /**
+ * Read the value from the row returned by the SQL query and transorms it to the appropriate
+ * internal format (either text or html)
+ *
+ * @param array $aCols
+ * @param string $sPrefix
+ *
+ * @return string
+ * @see AttributeDBFieldVoid::FromSQLToValue()
+ *
+ */
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ $value = $aCols[$sPrefix . ''];
+ if ($this->GetOptional('format', null) != null) {
+ // Read from the extra column only if the property 'format' is specified for the attribute
+ $sFormat = $aCols[$sPrefix . '_format'];
+ } else {
+ $sFormat = $this->GetFormat();
+ }
+
+ switch ($sFormat) {
+ case 'text':
+ if ($this->GetFormat() == 'html') {
+ $value = utils::TextToHtml($value);
+ }
+ break;
+
+ case 'html':
+ if ($this->GetFormat() == 'text') {
+ $value = utils::HtmlToText($value);
+ } else {
+ $value = InlineImage::FixUrls((string)$value);
+ }
+ break;
+
+ default:
+ // unknown format ??
+ }
+
+ return $value;
+ }
+
+ public function GetSQLValues($value)
+ {
+ $aValues = array();
+ $aValues[$this->Get("sql")] = $this->ScalarToSQL($value);
+ if ($this->GetOptional('format', null) != null) {
+ // Add the extra column only if the property 'format' is specified for the attribute
+ $aValues[$this->Get("sql") . '_format'] = $this->GetFormat();
+ }
+
+ return $aValues;
+ }
+
+ public function GetAsCSV(
+ $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ )
+ {
+ switch ($this->GetFormat()) {
+ case 'html':
+ if ($bConvertToPlainText) {
+ $sValue = utils::HtmlToText((string)$sValue);
+ }
+ $sFrom = array("\r\n", $sTextQualifier);
+ $sTo = array("\n", $sTextQualifier . $sTextQualifier);
+ $sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
+
+ return $sTextQualifier . $sEscaped . $sTextQualifier;
+ break;
+
+ case 'text':
+ default:
+ return parent::GetAsCSV($sValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize,
+ $bConvertToPlainText);
+ }
+ }
+
+ protected function GetChangeRecordAdditionalData(CMDBChangeOp $oMyChangeOp, DBObject $oObject, $original, $value): void
+ {
+ /** @noinspection PhpConditionCheckedByNextConditionInspection */
+ if (false === is_null($original) && ($original instanceof ormCaseLog)) {
+ $original = $original->GetText();
+ }
+ $oMyChangeOp->Set("prevdata", $original);
+ }
+
+ protected function GetChangeRecordClassName(): string
+ {
+ return ($this->GetFormat() === 'html')
+ ? CMDBChangeOpSetAttributeHTML::class
+ : CMDBChangeOpSetAttributeText::class;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/AttributeURL.php b/sources/Core/AttributeDefinition/AttributeURL.php
new file mode 100644
index 0000000000..89dc55c676
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeURL.php
@@ -0,0 +1,110 @@
+GetSQLColSpec() : '');
+ }
+
+ public function GetMaxSize()
+ {
+ return 2048;
+ }
+
+ public function GetEditClass()
+ {
+ return "String";
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ $sTarget = $this->Get("target");
+ if (empty($sTarget)) {
+ $sTarget = "_blank";
+ }
+ $sLabel = Str::pure2html($sValue);
+ if (strlen($sLabel) > 128) {
+ // Truncate the length to 128 characters, by removing the middle
+ $sLabel = substr($sLabel, 0, 100) . '.....' . substr($sLabel, -20);
+ }
+
+ return "$sLabel";
+ }
+
+ public function GetValidationPattern()
+ {
+ return $this->GetOptional('validation_pattern', '^' . utils::GetConfig()->Get('url_validation_pattern') . '$');
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\UrlField';
+ }
+
+ /**
+ * @param \DBObject $oObject
+ * @param \Combodo\iTop\Form\Field\UrlField $oFormField
+ *
+ * @return null
+ * @throws \CoreException
+ */
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ if ($oFormField === null) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ }
+ parent::MakeFormField($oObject, $oFormField);
+
+ $oFormField->SetTarget($this->Get('target'));
+
+ return $oFormField;
+ }
+
+ protected function GetChangeRecordClassName(): string
+ {
+ return CMDBChangeOpSetAttributeURL::class;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/AttributeDefinition/MissingColumnException.php b/sources/Core/AttributeDefinition/MissingColumnException.php
new file mode 100644
index 0000000000..351224f58b
--- /dev/null
+++ b/sources/Core/AttributeDefinition/MissingColumnException.php
@@ -0,0 +1,10 @@
+ attcode as key, iCol as value */
+ protected $m_aAttList;
+ /** @var array> sExtKeyAttCode as key, array of sExtReconcKeyAttCode/iCol as value */
+ protected $m_aExtKeys;
+ /** @var string[] list of attcode (attcode = 'id' for the pkey) */
+ protected $m_aReconcilKeys;
+ /** @var string OQL - if specified, then the missing items will be reported */
+ protected $m_sSynchroScope;
+ /**
+ * @var array attcode as key, attvalue as value. Values to be set when an object gets out of scope
+ * (ignored if no scope has been defined)
+ */
+ protected $m_aOnDisappear;
+ /**
+ * @see DateTime::createFromFormat
+ * @var string Date format specification
+ */
+ protected $m_sDateFormat;
+ /**
+ * @see AttributeEnum
+ * @var boolean true if Values in the data set are localized
+ */
+ protected $m_bLocalizedValues;
+ /** @var array Cache for resolving external keys based on the given search criterias */
+ protected $m_aExtKeysMappingCache;
+ /** @var int number of columns */
+ protected $m_iNbCol;
+
+ public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false, $iNbCol = 0)
+ {
+ $this->m_sClass = $sClass;
+ $this->m_aData = $aData;
+ $this->m_aAttList = $aAttList;
+ $this->m_aReconcilKeys = $aReconcilKeys;
+ $this->m_aExtKeys = $aExtKeys;
+ $this->m_sSynchroScope = $sSynchroScope;
+ $this->m_aOnDisappear = $aOnDisappear;
+ $this->m_sDateFormat = $sDateFormat;
+ $this->m_bLocalizedValues = $bLocalize;
+ $this->m_aExtKeysMappingCache = array();
+ $this->m_iNbCol = $iNbCol;
+ }
+
+ protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults)
+ {
+ $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+ $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
+ foreach ($this->m_aExtKeys[$sAttCode] as $sReconKeyAttCode => $iCol) {
+ if ($sReconKeyAttCode == 'id') {
+ $value = (int)$aRowData[$iCol];
+ } else {
+ // The foreign attribute is one of our reconciliation key
+ $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
+ $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
+ }
+ $oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
+ $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
+ }
+
+ $oExtObjects = new CMDBObjectSet($oReconFilter);
+ $aKeys = $oExtObjects->ToArray();
+ return array($oReconFilter, $aKeys);
+ }
+
+ // Returns true if the CSV data specifies that the external key must be left undefined
+ protected function IsNullExternalKeySpec($aRowData, $sAttCode)
+ {
+ //$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+ foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) {
+ // The foreign attribute is one of our reconciliation key
+ if (isset($aRowData[$iCol]) && strlen($aRowData[$iCol]) > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @param DBObject $oTargetObj
+ * @param array $aRowData
+ * @param array $aErrors
+ *
+ * @return array
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \MissingQueryArgument
+ * @throws \MySQLException
+ * @throws \MySQLHasGoneAwayException
+ */
+ protected function PrepareObject(&$oTargetObj, $aRowData, &$aErrors)
+ {
+ $aResults = array();
+ $aErrors = array();
+
+ // External keys reconciliation
+ //
+ foreach ($this->m_aExtKeys as $sAttCode => $aReconKeys) {
+ // Skip external keys used for the reconciliation process
+ // if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
+
+ $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
+
+ if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) {
+ foreach ($aReconKeys as $sReconKeyAttCode => $iCol) {
+ // Default reporting
+ // $aRowData[$iCol] is always null
+ $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
+ }
+ if ($oExtKey->IsNullAllowed()) {
+ $oTargetObj->Set($sAttCode, $oExtKey->GetNullValue());
+ $aResults[$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue());
+ } else {
+ $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-Null');
+ $aResults[$sAttCode] = new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), Dict::S('UI:CSVReport-Value-Issue-Null'));
+ }
+ } else {
+ $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
+
+ $aCacheKeys = array();
+ foreach ($aReconKeys as $sReconKeyAttCode => $iCol) {
+ // The foreign attribute is one of our reconciliation key
+ if ($sReconKeyAttCode == 'id') {
+ $value = $aRowData[$iCol];
+ } else {
+ $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
+ $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
+ }
+ $aCacheKeys[] = $value;
+ $oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
+ $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
+ }
+ $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
+ $iForeignKey = null;
+ // TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case...
+ if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache)) {
+ $this->m_aExtKeysMappingCache[$sAttCode] = array();
+ }
+ if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode])) {
+ // Cache hit
+ $iObjectFoundCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
+ $iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k'];
+ // Record the hit
+ $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++;
+ } else {
+ // Cache miss, let's initialize it
+ $oExtObjects = new CMDBObjectSet($oReconFilter);
+ $iObjectFoundCount = $oExtObjects->Count();
+ if ($iObjectFoundCount == 1) {
+ $oForeignObj = $oExtObjects->Fetch();
+ $iForeignKey = $oForeignObj->GetKey();
+ }
+ $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
+ 'c' => $iObjectFoundCount,
+ 'k' => $iForeignKey,
+ 'oql' => $oReconFilter->ToOql(),
+ 'h' => 0, // number of hits on this cache entry
+ );
+ }
+ switch ($iObjectFoundCount) {
+ case 0:
+ $oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
+ $aResults[$sAttCode] = $oCellStatus_SearchIssue;
+ $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
+ break;
+
+ case 1:
+ // Do change the external key attribute
+ $oTargetObj->Set($sAttCode, $iForeignKey);
+ break;
+
+ default:
+ $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iObjectFoundCount);
+ $aResults[$sAttCode] = new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iObjectFoundCount, $oReconFilter->serialize());
+ }
+ }
+
+ // Report
+ if (!array_key_exists($sAttCode, $aResults)) {
+ $iForeignObj = $oTargetObj->Get($sAttCode);
+ if (array_key_exists($sAttCode, $oTargetObj->ListChanges())) {
+ if ($oTargetObj->IsNew()) {
+ $aResults[$sAttCode] = new CellStatus_Void($iForeignObj);
+ } else {
+ $aResults[$sAttCode] = new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode));
+ foreach ($aReconKeys as $sReconKeyAttCode => $iCol) {
+ // Report the change on reconciliation values as well
+ $aResults[$iCol] = new CellStatus_Modify($aRowData[$iCol]);
+ }
+ }
+ } else {
+ $aResults[$sAttCode] = new CellStatus_Void($iForeignObj);
+ }
+ }
+ }
+
+ // Set the object attributes
+ //
+ foreach ($this->m_aAttList as $sAttCode => $iCol) {
+ // skip the private key, if any
+ if (($sAttCode == 'id') || ($sAttCode == 'friendlyname')) {
+ continue;
+ }
+
+ $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+
+ // skip reconciliation keys
+ if (!$oAttDef->IsWritable() && in_array($sAttCode, $this->m_aReconcilKeys)) {
+ continue;
+ }
+
+ $aReasons = array();
+ $iFlags = ($oTargetObj->IsNew())
+ ? $oTargetObj->GetInitialStateAttributeFlags($sAttCode, $aReasons)
+ : $oTargetObj->GetAttributeFlags($sAttCode, $aReasons);
+ if ((($iFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY) && ($oTargetObj->Get($sAttCode) != $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues))) {
+ $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Readonly', $sAttCode, $oTargetObj->Get($sAttCode), $aRowData[$iCol]);
+ } else if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) {
+ try {
+ $oSet = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
+ $oTargetObj->Set($sAttCode, $oSet);
+ } catch (CoreException $e) {
+ $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Format', $e->getMessage());
+ }
+ } else {
+ $value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
+ if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) {
+ if ($oAttDef instanceof AttributeEnum || $oAttDef instanceof AttributeTagSet) {
+ /** @var AttributeDefinition $oAttributeDefinition */
+ $oAttributeDefinition = $oAttDef;
+ $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-AllowedValues', $sAttCode, implode(',', $oAttributeDefinition->GetAllowedValues()));
+ } else {
+ $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
+ }
+ } else {
+ $res = $oTargetObj->CheckValue($sAttCode, $value);
+ if ($res === true) {
+ $oTargetObj->Set($sAttCode, $value);
+ } else {
+ // $res is a string with the error description
+ $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Unknown', $sAttCode, $res);
+ }
+ }
+ }
+ }
+
+ // Reporting on fields
+ //
+ $aChangedFields = $oTargetObj->ListChanges();
+ foreach ($this->m_aAttList as $sAttCode => $iCol) {
+ if ($sAttCode == 'id') {
+ $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
+ } else {
+ $sCurValue = new ReportValue($oTargetObj, $sAttCode, false);
+ $sOrigValue = new ReportValue($oTargetObj, $sAttCode, true);
+ if (isset($aErrors[$sAttCode])) {
+ $aResults[$iCol] = new CellStatus_Issue($aRowData[$iCol], $sOrigValue, $aErrors[$sAttCode]);
+ } elseif (array_key_exists($sAttCode, $aChangedFields)) {
+ if ($oTargetObj->IsNew()) {
+ $aResults[$iCol] = new CellStatus_Void($sCurValue);
+ } else {
+ $aResults[$iCol] = new CellStatus_Modify($sCurValue, $sOrigValue);
+ }
+ } else {
+ // By default... nothing happens
+ $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+ if ($oAttDef instanceof AttributeDateTime) {
+ $aResults[$iCol] = new CellStatus_Void($oAttDef->GetFormat()->Format($aRowData[$iCol]));
+ } else {
+ $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
+ }
+ }
+ }
+ }
+
+ // Checks
+ //
+ $res = $oTargetObj->CheckConsistency();
+ if ($res !== true) {
+ // $res contains the error description
+ $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res);
+ }
+ return $aResults;
+ }
+
+ /**
+ * search with current permissions did not match
+ * let's search why and give some more feedbacks to the user through proper labels
+ *
+ * @param DBObjectSearch $oDbSearchWithConditions search used to find external key
+ *
+ * @return \CellStatus_SearchIssue
+ * @throws \CoreException
+ * @throws \MissingQueryArgument
+ * @throws \MySQLException
+ * @throws \MySQLHasGoneAwayException
+ *
+ * @since 3.1.0 N°5305
+ */
+ protected function GetCellSearchIssue($oDbSearchWithConditions): CellStatus_SearchIssue
+ {
+ //current search with current permissions did not match
+ //let's search why and give some more feedback to the user
+
+ $sSerializedSearch = $oDbSearchWithConditions->serialize();
+
+ // Count all objects with all permissions without any condition
+ $oDbSearchWithoutAnyCondition = new DBObjectSearch($oDbSearchWithConditions->GetClass());
+ $oDbSearchWithoutAnyCondition->AllowAllData(true);
+ $oExtObjectSet = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
+ $iAllowAllDataObjectCount = $oExtObjectSet->Count();
+
+ if ($iAllowAllDataObjectCount === 0) {
+ $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject', $oDbSearchWithConditions->GetClass());
+ return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
+ }
+
+ // Count all objects with current user permissions
+ $oDbSearchWithoutAnyCondition->AllowAllData(false);
+ $oExtObjectSetWithCurrentUserPermissions = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
+ $iCurrentUserRightsObjectCount = $oExtObjectSetWithCurrentUserPermissions->Count();
+ $sAllowedValuesOql = $oDbSearchWithoutAnyCondition->serialize();
+
+ if ($iCurrentUserRightsObjectCount === 0) {
+ // No objects visible by current user
+ $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
+ return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
+ }
+
+ try {
+ $aDisplayedAllowedValues = [];
+ // Possibles values are displayed to UI user. we have to limit the amount of displayed values
+ $oExtObjectSetWithCurrentUserPermissions->SetLimit(4);
+ for ($i = 0; $i < 3; $i++) {
+ /** @var DBObject $oVisibleObject */
+ $oVisibleObject = $oExtObjectSetWithCurrentUserPermissions->Fetch();
+ if (is_null($oVisibleObject)) {
+ break;
+ }
+
+ $aCurrentAllowedValueFields = [];
+ foreach ($oDbSearchWithConditions->GetInternalParams() as $sForeignAttCode => $sValue) {
+ $aCurrentAllowedValueFields[] = $oVisibleObject->Get($sForeignAttCode);
+ }
+ $aDisplayedAllowedValues[] = implode(" ", $aCurrentAllowedValueFields);
+
+ }
+ $allowedValues = implode(", ", $aDisplayedAllowedValues);
+ if ($oExtObjectSetWithCurrentUserPermissions->Count() > 3) {
+ $allowedValues .= "...";
+ }
+ } catch (Exception $e) {
+ IssueLog::Error("failure during CSV import when fetching few visible objects: ", null,
+ ['target_class' => $oDbSearchWithConditions->GetClass(), 'criteria' => $oDbSearchWithConditions->GetCriteria(), 'message' => $e->getMessage()]
+ );
+ $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
+ return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
+ }
+
+ if ($iAllowAllDataObjectCount != $iCurrentUserRightsObjectCount) {
+ // No match and some objects NOT visible by current user. including current search maybe...
+ $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser', $oDbSearchWithConditions->GetClass());
+ return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues, $sAllowedValuesOql);
+ }
+
+ // No match. This is not linked to any right issue
+ // Possible values: DD,DD
+ $aCurrentValueFields = [];
+ foreach ($oDbSearchWithConditions->GetInternalParams() as $sValue) {
+ $aCurrentValueFields[] = $sValue;
+ }
+ $value = implode(" ", $aCurrentValueFields);
+ $sReason = Dict::Format('UI:CSVReport-Value-NoMatch', $value);
+ return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues, $sAllowedValuesOql);
+ }
+
+ protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
+ {
+ $aResults = array();
+ $aErrors = array();
+
+ // External keys
+ //
+ foreach ($this->m_aExtKeys as $sAttCode => $aKeyConfig) {
+ //$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
+ $aResults[$sAttCode] = new CellStatus_Void($oTargetObj->Get($sAttCode));
+
+ foreach ($aKeyConfig as $sForeignAttCode => $iCol) {
+ $aResults[$iCol] = new CellStatus_Void('?');
+ }
+ }
+
+ // Update attributes
+ //
+ foreach ($this->m_aOnDisappear as $sAttCode => $value) {
+ if (!MetaModel::IsValidAttCode(get_class($oTargetObj), $sAttCode)) {
+ throw new BulkChangeException('Invalid attribute code', array('class' => get_class($oTargetObj), 'attcode' => $sAttCode));
+ }
+ $oTargetObj->Set($sAttCode, $value);
+ }
+
+ // Reporting on fields
+ //
+ $aChangedFields = $oTargetObj->ListChanges();
+ foreach ($this->m_aAttList as $sAttCode => $iCol) {
+ if ($sAttCode == 'id') {
+ $aResults[$iCol] = new CellStatus_Void($oTargetObj->GetKey());
+ }
+ if (array_key_exists($sAttCode, $aChangedFields)) {
+ $aResults[$iCol] = new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode));
+ } else {
+ // By default... nothing happens
+ $aResults[$iCol] = new CellStatus_Void($oTargetObj->Get($sAttCode));
+ }
+ }
+
+ // Checks
+ //
+ $res = $oTargetObj->CheckConsistency();
+ if ($res !== true) {
+ // $res contains the error description
+ $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res);
+ }
+ return $aResults;
+ }
+
+
+ protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null)
+ {
+ $oTargetObj = MetaModel::NewObject($this->m_sClass);
+
+ // Populate the cache for hierarchical keys (only if in verify mode)
+ if (is_null($oChange)) {
+ // 1. determine if a hierarchical key exists
+ foreach ($this->m_aExtKeys as $sAttCode => $aKeyConfig) {
+ $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
+ if (!$this->IsNullExternalKeySpec($aRowData, $sAttCode) && MetaModel::IsParentClass(get_class($oTargetObj), $this->m_sClass)) {
+ // 2. Populate the cache for further checks
+ $aCacheKeys = array();
+ foreach ($aKeyConfig as $sForeignAttCode => $iCol) {
+ // The foreign attribute is one of our reconciliation key
+ if ($sForeignAttCode == 'id') {
+ $value = $aRowData[$iCol];
+ } else {
+ if (!isset($this->m_aAttList[$sForeignAttCode]) || !isset($aRowData[$this->m_aAttList[$sForeignAttCode]])) {
+ // the key is not in the import
+ break 2;
+ }
+ $value = $aRowData[$this->m_aAttList[$sForeignAttCode]];
+ }
+ $aCacheKeys[] = $value;
+ }
+ $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
+ $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
+ 'c' => 1,
+ 'k' => -1,
+ 'oql' => '',
+ 'h' => 0, // number of hits on this cache entry
+ );
+ }
+ }
+ }
+
+ $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
+
+ if (count($aErrors) > 0) {
+ $sErrors = implode(', ', $aErrors);
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
+ //__ERRORS__ used by tests only
+ $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
+ return $oTargetObj;
+ }
+
+ // Check that any external key will have a value proposed
+ $aMissingKeys = array();
+ foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey) {
+ if (!$oExtKey->IsNullAllowed()) {
+ if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList)) {
+ $aMissingKeys[] = $oExtKey->GetLabel();
+ }
+ }
+ }
+ if (count($aMissingKeys) > 0) {
+ $sMissingKeys = implode(', ', $aMissingKeys);
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-MissingExtKey', $sMissingKeys));
+ return $oTargetObj;
+ }
+
+ // Optionally record the results
+ //
+ if ($oChange) {
+ $newID = $oTargetObj->DBInsert();
+ } else {
+ $newID = 0;
+ }
+
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj();
+ $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
+ $aResult[$iRow]["id"] = new CellStatus_Void($newID);
+
+ return $oTargetObj;
+ }
+
+ /**
+ * @param array $aResult
+ * @param int $iRow
+ * @param \CMDBObject $oTargetObj
+ * @param array $aRowData
+ * @param \CMDBChange $oChange
+ *
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \MissingQueryArgument
+ * @throws \MySQLException
+ * @throws \MySQLHasGoneAwayException
+ */
+ protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null)
+ {
+ $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
+
+ // Reporting
+ //
+ $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
+ $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
+
+ if (count($aErrors) > 0) {
+ $sErrors = implode(', ', $aErrors);
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
+ //__ERRORS__ used by tests only
+ $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
+ return;
+ }
+
+ $aChangedFields = $oTargetObj->ListChanges();
+ if (count($aChangedFields) > 0) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields));
+
+ // Optionaly record the results
+ //
+ if ($oChange) {
+ try {
+ $oTargetObj->DBUpdate();
+ } catch (CoreException $e) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage());
+ }
+ }
+ } else {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange();
+ }
+ }
+
+ /**
+ * @param array $aResult
+ * @param int $iRow
+ * @param \CMDBObject $oTargetObj
+ * @param \CMDBChange $oChange
+ *
+ * @throws \BulkChangeException
+ */
+ protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null)
+ {
+ $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors);
+
+ // Reporting
+ //
+ $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
+ $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
+
+ if (count($aErrors) > 0) {
+ $sErrors = implode(', ', $aErrors);
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
+ //__ERRORS__ used by tests only
+ $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
+ return;
+ }
+
+ $aChangedFields = $oTargetObj->ListChanges();
+ if (count($aChangedFields) > 0) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields));
+
+ // Optionaly record the results
+ //
+ if ($oChange) {
+ try {
+ $oTargetObj->DBUpdate();
+ } catch (CoreException $e) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage());
+ }
+ }
+ } else {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0);
+ }
+ }
+
+ public function Process(CMDBChange $oChange = null)
+ {
+ if ($oChange) {
+ CMDBObject::SetCurrentChange($oChange);
+ }
+
+ // Note: $oChange can be null, in which case the aim is to check what would be done
+
+ // Debug...
+ //
+ if (false) {
+ echo "\n";
+ echo "Attributes:\n";
+ print_r($this->m_aAttList);
+ echo "ExtKeys:\n";
+ print_r($this->m_aExtKeys);
+ echo "Reconciliation:\n";
+ print_r($this->m_aReconcilKeys);
+ echo "Synchro scope:\n";
+ print_r($this->m_sSynchroScope);
+ echo "Synchro changes:\n";
+ print_r($this->m_aOnDisappear);
+ //echo "Data:\n";
+ //print_r($this->m_aData);
+ echo "
\n";
+ exit;
+ }
+
+ $aResult = array();
+
+ if (!is_null($this->m_sDateFormat) && (strlen($this->m_sDateFormat) > 0)) {
+ $sDateTimeFormat = $this->m_sDateFormat; // the specified format is actually the date AND time format
+ $oDateTimeFormat = new DateTimeFormat($sDateTimeFormat);
+ $sDateFormat = $oDateTimeFormat->ToDateFormat();
+ AttributeDateTime::SetFormat($oDateTimeFormat);
+ AttributeDate::SetFormat(new DateTimeFormat($sDateFormat));
+ // Translate dates from the source data
+ //
+ foreach ($this->m_aAttList as $sAttCode => $iCol) {
+ if ($sAttCode == 'id') continue;
+
+ $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+ if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime
+ {
+ foreach ($this->m_aData as $iRow => $aRowData) {
+ $sFormat = $sDateTimeFormat;
+ if (!isset($this->m_aData[$iRow][$iCol])) {
+ continue;
+ }
+ $sValue = $this->m_aData[$iRow][$iCol];
+ if (!empty($sValue)) {
+ if ($oAttDef instanceof AttributeDate) {
+ $sFormat = $sDateFormat;
+ }
+ $oFormat = new DateTimeFormat($sFormat);
+ $sDateExample = $oFormat->Format(new DateTime('2022-10-23 16:25:33'));
+ $sRegExp = $oFormat->ToRegExpr('/');
+ $sErrorMsg = Dict::Format('UI:CSVReport-Row-Issue-ExpectedDateFormat', $sDateExample);
+ if (!preg_match($sRegExp, $sValue)) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
+ $aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg);
+
+ } else {
+ $oDate = DateTime::createFromFormat($sFormat, $sValue);
+ if ($oDate !== false) {
+ $sNewDate = $oDate->format($oAttDef->GetInternalFormat());
+ $this->m_aData[$iRow][$iCol] = $sNewDate;
+ } else {
+ // almost impossible ti reproduce since even incorrect dates with correct formats are formated and $oDate will not be false
+ // Leave the cell unchanged
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
+ $aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg);
+ }
+ }
+ } else {
+ $this->m_aData[$iRow][$iCol] = '';
+ }
+ }
+ }
+ }
+ }
+
+ // Compute the results
+ //
+ if (!is_null($this->m_sSynchroScope)) {
+ $aVisited = array();
+ }
+ $iPreviousTimeLimit = ini_get('max_execution_time');
+ $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+
+ // Avoid too many events
+ cmdbAbstractObject::SetEventDBLinksChangedBlocked(true);
+ try {
+ foreach ($this->m_aData as $iRow => $aRowData) {
+ set_time_limit(intval($iLoopTimeLimit));
+ // Stop if not the good number of cols in $aRowData
+ if ($this->m_iNbCol > 0 && count($aRowData) != $this->m_iNbCol) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-NbField', count($aRowData), $this->m_iNbCol));
+ continue;
+ }
+
+ if (isset($aResult[$iRow]["__STATUS__"])) {
+ // An issue at the earlier steps - skip the rest
+ continue;
+ }
+ try {
+ $oReconciliationFilter = new DBObjectSearch($this->m_sClass);
+ $bSkipQuery = false;
+ foreach ($this->m_aReconcilKeys as $sAttCode) {
+ $valuecondition = null;
+ if (array_key_exists($sAttCode, $this->m_aExtKeys)) {
+ if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) {
+ $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+ if ($oExtKey->IsNullAllowed()) {
+ $valuecondition = $oExtKey->GetNullValue();
+ $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue());
+ } else {
+ $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue();
+ }
+ } else {
+ // The value has to be found or verified
+
+ /** var DBObjectSearch $oReconFilter */
+ list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
+
+ if (count($aMatches) == 1) {
+ $oRemoteObj = reset($aMatches); // first item
+ $valuecondition = $oRemoteObj->GetKey();
+ $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
+ } elseif (count($aMatches) == 0) {
+ $oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
+ $aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue;
+ } else {
+ $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize());
+ }
+ }
+ } else {
+ // The value is given in the data row
+ $iCol = $this->m_aAttList[$sAttCode];
+ if ($sAttCode == 'id') {
+ $valuecondition = $aRowData[$iCol];
+ } else {
+ $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+ $valuecondition = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
+ }
+ }
+ if (is_null($valuecondition)) {
+ $bSkipQuery = true;
+ } else {
+ $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=', true);
+ }
+ }
+ if ($bSkipQuery) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Reconciliation'));
+ } else {
+ $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
+ switch ($oReconciliationSet->Count()) {
+ case 0:
+ $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange);
+ // $aResult[$iRow]["__STATUS__"]=> set in CreateObject
+ $aVisited[] = $oTargetObj->GetKey();
+ break;
+ case 1:
+ $oTargetObj = $oReconciliationSet->Fetch();
+ $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange);
+ // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
+ if (!is_null($this->m_sSynchroScope)) {
+ $aVisited[] = $oTargetObj->GetKey();
+ }
+ break;
+ default:
+ // Found several matches, ambiguous
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous'));
+ $aResult[$iRow]["id"] = new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize());
+ $aResult[$iRow]["finalclass"] = 'n/a';
+ }
+ }
+ } catch (Exception $e) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-Internal', get_class($e), $e->getMessage()));
+ }
+ }
+
+ if (!is_null($this->m_sSynchroScope)) {
+ // Compute the delta between the scope and visited objects
+ $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope);
+ $oScopeSet = new DBObjectSet($oScopeSearch);
+ while ($oObj = $oScopeSet->Fetch()) {
+ $iObj = $oObj->GetKey();
+ if (!in_array($iObj, $aVisited)) {
+ set_time_limit(intval($iLoopTimeLimit));
+ $iRow++;
+ $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange);
+ }
+ }
+ }
+ } finally {
+ // Send all the retained events for further computations
+ cmdbAbstractObject::SetEventDBLinksChangedBlocked(false);
+ cmdbAbstractObject::FireEventDbLinksChangedForAllObjects();
+ }
+
+ set_time_limit(intval($iPreviousTimeLimit));
+
+ // Fill in the blanks - the result matrix is expected to be 100% complete
+ //
+ foreach ($this->m_aData as $iRow => $aRowData) {
+ foreach ($this->m_aAttList as $iCol) {
+ if (!array_key_exists($iCol, $aResult[$iRow])) {
+ if (isset($aRowData[$iCol])) {
+ $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
+ } else {
+ $aResult[$iRow][$iCol] = new CellStatus_Issue('', null, Dict::S('UI:CSVReport-Value-Issue-NoValue'));
+ }
+ }
+ }
+ foreach ($this->m_aExtKeys as $sAttCode => $aForeignAtts) {
+ if (!array_key_exists($sAttCode, $aResult[$iRow])) {
+ $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a');
+ }
+ foreach ($aForeignAtts as $sForeignAttCode => $iCol) {
+ if (!array_key_exists($iCol, $aResult[$iRow])) {
+ // The foreign attribute is one of our reconciliation key
+ if (isset($aRowData[$iCol])) {
+ $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
+ } else {
+ $aResult[$iRow][$iCol] = new CellStatus_Issue('', null, 'UI:CSVReport-Value-Issue-NoValue');
+ }
+ }
+ }
+ }
+ }
+
+ return $aResult;
+ }
+
+ /**
+ * Display the history of bulk imports
+ */
+ static function DisplayImportHistory(\Combodo\iTop\Application\WebPage\WebPage $oPage, $bFromAjax = false, $bShowAll = false)
+ {
+ $sAjaxDivId = "CSVImportHistory";
+ if (!$bFromAjax) {
+ $oPage->add('');
+ }
+
+ $oPage->p(Dict::S('UI:History:BulkImports+') . '
');
+
+ $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE origin IN ('csv-interactive', 'csv-import.php')");
+
+ $iQueryLimit = $bShowAll ? 0 : appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit());
+ $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit);
+
+ $oAppContext = new ApplicationContext();
+
+ $bLimitExceeded = false;
+ if ($oBulkChanges->Count() > (appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()))) {
+ $bLimitExceeded = true;
+ if (!$bShowAll) {
+ $iMaxObjects = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit());
+ $oBulkChanges->SetLimit($iMaxObjects);
+ }
+ }
+ $oBulkChanges->Seek(0);
+
+ $aDetails = array();
+ while ($oChange = $oBulkChanges->Fetch()) {
+ $sDate = '
' . $oChange->Get('date') . '';
+ $sUser = $oChange->GetUserName();
+ if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches)) {
+ $sUser = $aMatches[1];
+ } else {
+ $sUser = $oChange->Get('userinfo');
+ }
+
+ $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id");
+ $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
+ $iCreated = $oOpSet->Count();
+
+ // Get the class from the first item found (assumption: a CSV load is done for a single class)
+ if ($oCreateOp = $oOpSet->Fetch()) {
+ $sClass = $oCreateOp->Get('objclass');
+ }
+
+ $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id");
+ $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
+
+ $aModified = array();
+ $aAttList = array();
+ while ($oModified = $oOpSet->Fetch()) {
+ // Get the class (if not done earlier on object creation)
+ $sClass = $oModified->Get('objclass');
+ $iKey = $oModified->Get('objkey');
+ $sAttCode = $oModified->Get('attcode');
+
+ $aAttList[$sClass][$sAttCode] = true;
+ $aModified["$sClass::$iKey"] = true;
+ }
+ $iModified = count($aModified);
+
+ // Assumption: there is only one class of objects being loaded
+ // Then the last class found gives us the class for every object
+ if (($iModified > 0) || ($iCreated > 0)) {
+ $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified);
+ }
+ }
+
+ $aConfig = array('date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')),
+ 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')),
+ 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')),
+ 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')),
+ 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')),
+ );
+
+ if ($bLimitExceeded) {
+ if ($bShowAll) {
+ // Collapsible list
+ $oPage->add('
' . Dict::Format('UI:CountOfResults', $oBulkChanges->Count()) . ' ' . Dict::S('UI:CollapseList') . '
');
+ } else {
+ // Truncated list
+ $iMinDisplayLimit = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit());
+ $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count());
+ $sLinkLabel = Dict::S('UI:DisplayAll');
+ $oPage->add('
' . $sCollapsedLabel . ' ' . $sLinkLabel . '
');
+
+ $oPage->add_ready_script(
+ <<
GetForLink();
+ $oPage->add_script(
+ <<');
+ $.get(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?$sAppContext', {operation: 'displayCSVHistory', showall: bShowAll}, function(data)
+ {
+ $('#$sAjaxDivId').html(data);
+ }
+ );
+ }
+EOF
+ );
+ }
+ } else {
+ // Normal display - full list without any decoration
+ }
+
+ $oPage->table($aConfig, $aDetails);
+
+ if (!$bFromAjax) {
+ $oPage->add(' ');
+ }
+ }
+
+ /**
+ * Display the details of an import
+ *
+ * @param \Combodo\iTop\Application\WebPage\iTopWebPage $oPage
+ * @param $iChange
+ *
+ * @throws Exception
+ */
+ static function DisplayImportHistoryDetails(\Combodo\iTop\Application\WebPage\iTopWebPage $oPage, $iChange)
+ {
+ if ($iChange == 0) {
+ throw new Exception("Missing parameter changeid");
+ }
+ $oChange = MetaModel::GetObject('CMDBChange', $iChange, false);
+ if (is_null($oChange)) {
+ throw new Exception("Unknown change: $iChange");
+ }
+ $oPage->add("" . Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName()) . "
\n");
+
+ // Assumption : change made one single class of objects
+ $aObjects = array();
+ $aAttributes = array(); // array of attcode => occurences
+
+ $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id");
+ $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange));
+ while ($oOperation = $oOpSet->Fetch()) {
+ $sClass = $oOperation->Get('objclass');
+ $iKey = $oOperation->Get('objkey');
+ $iObjId = "$sClass::$iKey";
+ if (!isset($aObjects[$iObjId])) {
+ $aObjects[$iObjId] = array();
+ $aObjects[$iObjId]['__class__'] = $sClass;
+ $aObjects[$iObjId]['__id__'] = $iKey;
+ }
+ if (get_class($oOperation) == 'CMDBChangeOpCreate') {
+ $aObjects[$iObjId]['__created__'] = true;
+ } elseif ($oOperation instanceof CMDBChangeOpSetAttribute) {
+ $sAttCode = $oOperation->Get('attcode');
+
+ if ((get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar') || (get_class($oOperation) == 'CMDBChangeOpSetAttributeURL')) {
+ $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+ if ($oAttDef->IsExternalKey()) {
+ $sOldValue = Dict::S('UI:UndefinedObject');
+ if ($oOperation->Get('oldvalue') != 0) {
+ $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue'));
+ $sOldValue = $oOldTarget->GetHyperlink();
+ }
+
+ $sNewValue = Dict::S('UI:UndefinedObject');
+ if ($oOperation->Get('newvalue') != 0) {
+ $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue'));
+ $sNewValue = $oNewTarget->GetHyperlink();
+ }
+ } else {
+ $sOldValue = $oOperation->GetAsHTML('oldvalue');
+ $sNewValue = $oOperation->GetAsHTML('newvalue');
+ }
+ $aObjects[$iObjId][$sAttCode] = $sOldValue . ' -> ' . $sNewValue;
+ } else {
+ $aObjects[$iObjId][$sAttCode] = 'n/a';
+ }
+
+ if (isset($aAttributes[$sAttCode])) {
+ $aAttributes[$sAttCode]++;
+ } else {
+ $aAttributes[$sAttCode] = 1;
+ }
+ }
+ }
+
+ $aDetails = array();
+ foreach ($aObjects as $iUId => $aObjData) {
+ $aRow = array();
+ $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false);
+ if (is_null($oObject)) {
+ $aRow['object'] = $aObjData['__class__'] . '::' . $aObjData['__id__'] . ' (deleted)';
+ } else {
+ $aRow['object'] = $oObject->GetHyperlink();
+ }
+ if (isset($aObjData['__created__'])) {
+ $aRow['operation'] = Dict::S('Change:ObjectCreated');
+ } else {
+ $aRow['operation'] = Dict::S('Change:ObjectModified');
+ }
+ foreach ($aAttributes as $sAttCode => $iOccurences) {
+ if (isset($aObjData[$sAttCode])) {
+ $aRow[$sAttCode] = $aObjData[$sAttCode];
+ } elseif (!is_null($oObject)) {
+ // This is the current vaslue: $oObject->GetAsHtml($sAttCode)
+ // whereas we are displaying the value that was set at the time
+ // the object was created
+ // This requires addtional coding...let's do that later
+ $aRow[$sAttCode] = '';
+ } else {
+ $aRow[$sAttCode] = '';
+ }
+ }
+ $aDetails[] = $aRow;
+ }
+
+ $aConfig = array();
+ $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass));
+ $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+'));
+ foreach ($aAttributes as $sAttCode => $iOccurences) {
+ $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode));
+ }
+ $oPage->table($aConfig, $aDetails);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/CellChangeSpec.php b/sources/Core/BulkChange/CellChangeSpec.php
new file mode 100644
index 0000000000..cdb88e492b
--- /dev/null
+++ b/sources/Core/BulkChange/CellChangeSpec.php
@@ -0,0 +1,82 @@
+m_proposedValue = $proposedValue;
+ $this->m_sOql = $sOql;
+ }
+
+ public function GetPureValue()
+ {
+ // Todo - distinguish both values
+ return $this->m_proposedValue;
+ }
+
+ /**
+ * @throws \Exception
+ * @since 3.2.0
+ */
+ public function GetCLIValue(bool $bLocalizedValues = false): string
+ {
+ if (is_object($this->m_proposedValue)) {
+ if ($this->m_proposedValue instanceof ReportValue) {
+ return $this->m_proposedValue->GetAsCSV($bLocalizedValues, ',', '"');
+ }
+ throw new Exception('Unexpected class : ' . get_class($this->m_proposedValue));
+ }
+ return $this->m_proposedValue;
+ }
+
+ /**
+ * @throws \Exception
+ * @since 3.2.0
+ */
+ public function GetHTMLValue(bool $bLocalizedValues = false): string
+ {
+ if (is_object($this->m_proposedValue)) {
+ if ($this->m_proposedValue instanceof ReportValue) {
+ return $this->m_proposedValue->GetAsHTML($bLocalizedValues);
+ }
+ throw new Exception('Unexpected class : ' . get_class($this->m_proposedValue));
+ }
+ return utils::EscapeHtml($this->m_proposedValue);
+ }
+
+
+ /**
+ * @since 3.1.0 N°5305
+ */
+ public function SetDisplayableValue(string $sDisplayableValue)
+ {
+ $this->m_proposedValue = $sDisplayableValue;
+ }
+
+ public function GetOql()
+ {
+ return $this->m_sOql;
+ }
+
+ /**
+ * @since 3.2.0
+ */
+ public function GetCLIValueAndDescription(): string
+ {
+ return sprintf("%s%s",
+ $this->GetCLIValue(),
+ $this->GetDescription()
+ );
+ }
+
+ abstract public function GetDescription();
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/CellStatus_Ambiguous.php b/sources/Core/BulkChange/CellStatus_Ambiguous.php
new file mode 100644
index 0000000000..171e87bcc2
--- /dev/null
+++ b/sources/Core/BulkChange/CellStatus_Ambiguous.php
@@ -0,0 +1,43 @@
+m_iCount = $iCount;
+ $this->sSerializedSearch = $sSerializedSearch;
+ parent::__construct(null, $previousValue, '');
+ }
+
+ public function GetDescription()
+ {
+ $sCount = $this->m_iCount;
+ return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount);
+ }
+
+ /**
+ * @return string
+ * @since 3.1.0 N°5305
+ */
+ public function GetSearchLinkUrl()
+ {
+ return sprintf("UI.php?operation=search&filter=%s",
+ rawurlencode($this->sSerializedSearch ?? "")
+ );
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/CellStatus_Issue.php b/sources/Core/BulkChange/CellStatus_Issue.php
new file mode 100644
index 0000000000..38ffce5f2c
--- /dev/null
+++ b/sources/Core/BulkChange/CellStatus_Issue.php
@@ -0,0 +1,47 @@
+m_sReason = $sReason;
+ parent::__construct($proposedValue, $previousValue);
+ }
+
+ public function GetCLIValue(bool $bLocalizedValues = false): string
+ {
+ if (is_null($this->m_proposedValue)) {
+ return Dict::Format('UI:CSVReport-Value-SetIssue');
+ }
+ return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue);
+ }
+
+ public function GetHTMLValue(bool $bLocalizedValues = false): string
+ {
+ if (is_null($this->m_proposedValue)) {
+ return Dict::Format('UI:CSVReport-Value-SetIssue');
+ }
+ if ($this->m_proposedValue instanceof ReportValue) {
+ return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue->GetAsHTML($bLocalizedValues));
+ }
+ return Dict::Format('UI:CSVReport-Value-ChangeIssue', utils::EscapeHtml($this->m_proposedValue));
+ }
+
+ public function GetDescription()
+ {
+ return $this->m_sReason;
+ }
+
+ /*
+ * @since 3.2.0
+ */
+ public function GetCLIValueAndDescription(): string
+ {
+ return sprintf("%s. %s",
+ $this->GetCLIValue(),
+ $this->GetDescription()
+ );
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/CellStatus_Modify.php b/sources/Core/BulkChange/CellStatus_Modify.php
new file mode 100644
index 0000000000..3a5cd46307
--- /dev/null
+++ b/sources/Core/BulkChange/CellStatus_Modify.php
@@ -0,0 +1,23 @@
+m_previousValue = $previousValue;
+ parent::__construct($proposedValue);
+ }
+
+ public function GetDescription()
+ {
+ return Dict::S('UI:CSVReport-Value-Modified');
+ }
+
+ //public function GetPreviousValue()
+ //{
+ // return $this->m_previousValue;
+ //}
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/CellStatus_NullIssue.php b/sources/Core/BulkChange/CellStatus_NullIssue.php
new file mode 100644
index 0000000000..1036da47cf
--- /dev/null
+++ b/sources/Core/BulkChange/CellStatus_NullIssue.php
@@ -0,0 +1,14 @@
+sSerializedSearch = $sSerializedSearch;
+ $this->m_sAllowedValues = $sAllowedValues;
+ $this->m_sTargetClass = $sClass;
+ $this->sAllowedValuesSearch = $sAllowedValuesSearch;
+ }
+
+ public function GetCLIValue(bool $bLocalizedValues = false): string
+ {
+ if (null === $this->m_sReason) {
+ return Dict::Format('UI:CSVReport-Value-NoMatch', '');
+ }
+
+ return $this->m_sReason;
+ }
+
+ public function GetHTMLValue(bool $bLocalizedValues = false): string
+ {
+ if (null === $this->m_sReason) {
+ return Dict::Format('UI:CSVReport-Value-NoMatch', '');
+ }
+
+ return utils::EscapeHtml($this->m_sReason);
+ }
+
+ public function GetDescription()
+ {
+ if (\utils::IsNullOrEmptyString($this->m_sAllowedValues) ||
+ \utils::IsNullOrEmptyString($this->m_sTargetClass)) {
+ return '';
+ }
+
+ return Dict::Format('UI:CSVReport-Value-NoMatch-PossibleValues', $this->m_sTargetClass, $this->m_sAllowedValues);
+ }
+
+ /**
+ * @return string
+ * @since 3.1.0 N°5305
+ */
+ public function GetSearchLinkUrl()
+ {
+ return sprintf("UI.php?operation=search&filter=%s",
+ rawurlencode($this->sSerializedSearch ?? "")
+ );
+ }
+
+ /**
+ * @return null|string
+ * @since 3.1.0 N°5305
+ */
+ public function GetAllowedValuesLinkUrl(): ?string
+ {
+ return sprintf("UI.php?operation=search&filter=%s",
+ rawurlencode($this->sAllowedValuesSearch ?? "")
+ );
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/CellStatus_Void.php b/sources/Core/BulkChange/CellStatus_Void.php
new file mode 100644
index 0000000000..badbc483cf
--- /dev/null
+++ b/sources/Core/BulkChange/CellStatus_Void.php
@@ -0,0 +1,9 @@
+bOriginal) {
+ return $this->oObject->GetOriginalAsHTML($this->sAttCode, $bLocalizedValues);
+ }
+ return $this->oObject->GetAsHTML($this->sAttCode, $bLocalizedValues);
+ }
+
+ public function GetAsCSV(bool $bLocalizedValues, string $sCsvSep, string $sCsvDelimiter)
+ {
+ if ($this->bOriginal) {
+ return $this->oObject->GetOriginalAsCSV($this->sAttCode, $sCsvSep, $sCsvDelimiter, $bLocalizedValues);
+ }
+ return $this->oObject->GetAsCSV($this->sAttCode, $sCsvSep, $sCsvDelimiter, $bLocalizedValues);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/RowStatus.php b/sources/Core/BulkChange/RowStatus.php
new file mode 100644
index 0000000000..abf6299e07
--- /dev/null
+++ b/sources/Core/BulkChange/RowStatus.php
@@ -0,0 +1,16 @@
+m_iChanged);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/RowStatus_Error.php b/sources/Core/BulkChange/RowStatus_Error.php
new file mode 100644
index 0000000000..d91b152112
--- /dev/null
+++ b/sources/Core/BulkChange/RowStatus_Error.php
@@ -0,0 +1,22 @@
+m_sError = $sError;
+ }
+
+ public function GetDescription()
+ {
+ return $this->m_sError;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/RowStatus_Issue.php b/sources/Core/BulkChange/RowStatus_Issue.php
new file mode 100644
index 0000000000..548d362423
--- /dev/null
+++ b/sources/Core/BulkChange/RowStatus_Issue.php
@@ -0,0 +1,16 @@
+m_sReason = $sReason;
+ }
+
+ public function GetDescription()
+ {
+ return Dict::Format('UI:CSVReport-Row-Issue', $this->m_sReason);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/RowStatus_Modify.php b/sources/Core/BulkChange/RowStatus_Modify.php
new file mode 100644
index 0000000000..e5bf20b479
--- /dev/null
+++ b/sources/Core/BulkChange/RowStatus_Modify.php
@@ -0,0 +1,16 @@
+m_iChanged = $iChanged;
+ }
+
+ public function GetDescription()
+ {
+ return Dict::Format('UI:CSVReport-Row-Updated', $this->m_iChanged);
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/BulkChange/RowStatus_NewObj.php b/sources/Core/BulkChange/RowStatus_NewObj.php
new file mode 100644
index 0000000000..17c6023ad6
--- /dev/null
+++ b/sources/Core/BulkChange/RowStatus_NewObj.php
@@ -0,0 +1,9 @@
+ "core/cmdb",
+ "key_type" => "autoincrement",
+ "name_attcode" => "created",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_async_send_email",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+
+ MetaModel::Init_AddAttribute(new AttributeInteger("version", array("allowed_values" => null, "sql" => "version", "default_value" => Email::ORIGINAL_FORMAT, "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values" => null, "sql" => "to", "default_value" => null, "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values" => null, "sql" => "subject", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeLongText("message", array("allowed_values" => null, "sql" => "message", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
+
+ // Display lists
+// MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'test_recipient', 'from', 'reply_to', 'to', 'cc', 'bcc', 'subject', 'body', 'importance', 'trigger_list')); // Attributes to be displayed for the complete details
+// MetaModel::Init_SetZListItems('list', array('name', 'status', 'to', 'subject')); // Attributes to be displayed for a list
+ // Search criteria
+// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
+// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
+ }
+
+ static public function AddToQueue(EMail $oEMail, $oLog)
+ {
+ $oNew = MetaModel::NewObject(__class__);
+ if ($oLog) {
+ $oNew->Set('event_id', $oLog->GetKey());
+ }
+ $oNew->Set('to', $oEMail->GetRecipientTO(true /* string */));
+ $oNew->Set('subject', $oEMail->GetSubject());
+
+ $oNew->Set('version', 2);
+ $sMessage = $oEMail->SerializeV2();
+ $oNew->Set('message', $sMessage);
+ $oNew->DBInsert();
+ }
+
+ /**
+ * @inheritDoc
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ */
+ public function DoProcess()
+ {
+ $sMessage = $this->Get('message');
+ $iVersion = (int)$this->Get('version');
+ switch ($iVersion) {
+ case Email::FORMAT_V2:
+ $oEMail = Email::UnSerializeV2($sMessage);
+ break;
+
+ case Email::ORIGINAL_FORMAT:
+ $oEMail = unserialize($sMessage);
+ break;
+
+ default:
+ return 'Unknown version of the serialization format: ' . $iVersion;
+ }
+ $iRes = $oEMail->Send($aIssues, true /* force synchro !!!!! */);
+ switch ($iRes) {
+ case EMAIL_SEND_OK:
+ return "Sent";
+
+ case EMAIL_SEND_PENDING:
+ return "Bug - the email should be sent in synchronous mode";
+
+ case EMAIL_SEND_ERROR:
+ if (is_array($aIssues)) {
+ $sMessage = "Sending eMail failed: " . implode(', ', $aIssues);
+ } else {
+ $sMessage = "Sending eMail failed.";
+ }
+ throw new Exception($sMessage);
+ }
+ return '';
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/AsyncTask/AsyncSendNewsroom.php b/sources/Core/DataModel/AsyncTask/AsyncSendNewsroom.php
new file mode 100644
index 0000000000..0b2d93525f
--- /dev/null
+++ b/sources/Core/DataModel/AsyncTask/AsyncSendNewsroom.php
@@ -0,0 +1,85 @@
+ "core/cmdb",
+ "key_type" => "autoincrement",
+ "name_attcode" => "created",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_async_send_newsroom",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+
+ MetaModel::Init_AddAttribute(new AttributeText("recipients", array("allowed_values" => null, "sql" => "recipients", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass" => "Action", "allowed_values" => null, "sql" => "action_id", "default_value" => null, "is_null_allowed" => false, "on_target_delete" => DEL_AUTO, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass" => "Trigger", "allowed_values" => null, "sql" => "trigger_id", "default_value" => null, "is_null_allowed" => false, "on_target_delete" => DEL_AUTO, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeText("title", array("allowed_values" => null, "sql" => "title", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeText("message", array("allowed_values" => null, "sql" => "message", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeInteger("object_id", array("allowed_values" => null, "sql" => "object_id", "default_value" => null, "is_null_allowed" => false, "on_target_delete" => DEL_AUTO, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeString("object_class", array("allowed_values" => null, "sql" => "object_class", "default_value" => null, "is_null_allowed" => false, "on_target_delete" => DEL_AUTO, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeText("url", array("allowed_values" => null, "sql" => "url", "default_value" => null, "is_null_allowed" => false, "on_target_delete" => DEL_AUTO, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values" => null, "sql" => "date", "default_value" => 'NOW()', "is_null_allowed" => false, "on_target_delete" => DEL_AUTO, "depends_on" => array())));
+
+ }
+
+ /**
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \CoreWarning
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public static function AddToQueue(int $iActionId, int $iTriggerId, array $aRecipients, string $sMessage, string $sTitle, string $sUrl, int $iObjectId, ?string $sObjectClass): void
+ {
+ $oNew = new static();
+ $oNew->Set('action_id', $iActionId);
+ $oNew->Set('trigger_id', $iTriggerId);
+ $oNew->Set('recipients', json_encode($aRecipients));
+ $oNew->Set('message', $sMessage);
+ $oNew->Set('title', $sTitle);
+ $oNew->Set('url', $sUrl);
+ $oNew->Set('object_id', $iObjectId);
+ $oNew->Set('object_class', $sObjectClass);
+ $oNew->SetCurrentDate('date');
+
+ $oNew->DBInsert();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function DoProcess()
+ {
+ $oAction = MetaModel::GetObject('Action', $this->Get('action_id'));
+ $iTriggerId = $this->Get('trigger_id');
+ $aRecipients = json_decode($this->Get('recipients'));
+ $sMessage = $this->Get('message');
+ $sTitle = $this->Get('title');
+ $sUrl = $this->Get('url');
+ $iObjectId = $this->Get('object_id');
+ $sObjectClass = $this->Get('object_class');
+ $sDate = $this->Get('date');
+
+ foreach ($aRecipients as $iRecipientId) {
+ $oEvent = \Combodo\iTop\Service\Notification\Event\EventNotificationNewsroomService::MakeEventFromAction($oAction, $iRecipientId, $iTriggerId, $sMessage, $sTitle, $sUrl, $iObjectId, $sObjectClass, $sDate);
+ $oEvent->DBInsertNoReload();
+ }
+
+ return "Sent";
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/AsyncTask/AsyncTask.php b/sources/Core/DataModel/AsyncTask/AsyncTask.php
new file mode 100644
index 0000000000..ed7923927f
--- /dev/null
+++ b/sources/Core/DataModel/AsyncTask/AsyncTask.php
@@ -0,0 +1,269 @@
+ "core/cmdb",
+ "key_type" => "autoincrement",
+ "name_attcode" => array('created'),
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_async_task",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "realclass",
+ );
+ MetaModel::Init_Params($aParams);
+
+ // Null is allowed to ease the migration from iTop 2.0.2 and earlier, when the status did not exist, and because the default value is not taken into account in the SQL definition
+ // The value is set from null to planned in the setup program
+ MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values" => new ValueSetEnum('planned,running,idle,error'), "sql" => "status", "default_value" => "planned", "is_null_allowed" => true, "depends_on" => array())));
+
+ MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values" => null, "sql" => "created", "default_value" => "NOW()", "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeDateTime("started", array("allowed_values" => null, "sql" => "started", "default_value" => "", "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeDateTime("planned", array("allowed_values" => null, "sql" => "planned", "default_value" => "", "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeExternalKey("event_id", array("targetclass" => "Event", "jointype" => "", "allowed_values" => null, "sql" => "event_id", "is_null_allowed" => true, "on_target_delete" => DEL_SILENT, "depends_on" => array())));
+
+ MetaModel::Init_AddAttribute(new AttributeInteger("remaining_retries", array("allowed_values" => null, "sql" => "remaining_retries", "default_value" => 0, "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeInteger("last_error_code", array("allowed_values" => null, "sql" => "last_error_code", "default_value" => 0, "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeString("last_error", array("allowed_values" => null, "sql" => "last_error", "default_value" => '', "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeDateTime("last_attempt", array("allowed_values" => null, "sql" => "last_attempt", "default_value" => "", "is_null_allowed" => true, "depends_on" => array())));
+ }
+
+ /**
+ * Every is fine
+ */
+ const OK = 0;
+ /**
+ * The task no longer exists
+ */
+ const DELETED = 1;
+ /**
+ * The task is already being executed
+ */
+ const ALREADY_RUNNING = 2;
+
+ /**
+ * The current process requests the ownership on the task.
+ * In case the task can be accessed concurrently, this function can be overloaded to add a critical section.
+ * The function must not block the caller if another process is already owning the task
+ *
+ * @return integer A code among OK/DELETED/ALREADY_RUNNING.
+ */
+ public function MarkAsRunning()
+ {
+ try {
+ if ($this->Get('status') == 'running') {
+ return self::ALREADY_RUNNING;
+ } else {
+ $this->Set('status', 'running');
+ $this->Set('started', time());
+ $this->DBUpdate();
+ return self::OK;
+ }
+ } catch (Exception $e) {
+ // Corrupted task !! (for example: "Failed to reload object")
+ IssueLog::Error('Failed to process async task #' . $this->GetKey() . ' - reason: ' . $e->getMessage() . ' - fatal error, deleting the task.');
+ if ($this->Get('event_id') != 0) {
+ $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
+ $oEventLog->Set('message', 'Failed, corrupted data: ' . $e->getMessage());
+ $oEventLog->DBUpdate();
+ }
+ $this->DBDelete();
+ return self::DELETED;
+ }
+ }
+
+ public function GetRetryDelay($iErrorCode = null)
+ {
+ $iRetryDelay = 600;
+ $aRetries = MetaModel::GetConfig()->Get('async_task_retries');
+ if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) {
+ $aConfig = $aRetries[get_class($this)];
+ $iRetryDelay = $aConfig['retry_delay'] ?? $iRetryDelay;
+ }
+ return $iRetryDelay;
+ }
+
+ public function GetMaxRetries($iErrorCode = null)
+ {
+ $iMaxRetries = 0;
+ $aRetries = MetaModel::GetConfig()->Get('async_task_retries');
+ if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) {
+ $aConfig = $aRetries[get_class($this)];
+ $iMaxRetries = $aConfig['max_retries'] ?? $iMaxRetries;
+ }
+ return $iMaxRetries;
+ }
+
+ public function IsRetryDelayExponential()
+ {
+ $bExponential = false;
+ $aRetries = MetaModel::GetConfig()->Get('async_task_retries');
+ if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) {
+ $aConfig = $aRetries[get_class($this)];
+ $bExponential = (bool)($aConfig['exponential_delay'] ?? $bExponential);
+ }
+ return $bExponential;
+ }
+
+ public static function CheckRetryConfig(Config $oConfig, $sAsyncTaskClass)
+ {
+ $aMessages = [];
+ $aRetries = $oConfig->Get('async_task_retries');
+ if (is_array($aRetries) && array_key_exists($sAsyncTaskClass, $aRetries)) {
+ $aValidKeys = array("retry_delay", "max_retries", "exponential_delay");
+ $aConfig = $aRetries[$sAsyncTaskClass];
+ if (!is_array($aConfig)) {
+ $aMessages[] = Dict::Format('Class:AsyncTask:InvalidConfig_Class_Keys', $sAsyncTaskClass, implode(', ', $aValidKeys));
+ } else {
+ foreach ($aConfig as $key => $value) {
+ if (!in_array($key, $aValidKeys)) {
+ $aMessages[] = Dict::Format('Class:AsyncTask:InvalidConfig_Class_InvalidKey_Keys', $sAsyncTaskClass, $key, implode(', ', $aValidKeys));
+ }
+ }
+ }
+ }
+ return $aMessages;
+ }
+
+ /**
+ * Compute the delay to wait for the "next retry", based on the given parameters
+ * @param bool $bIsExponential
+ * @param int $iRetryDelay
+ * @param int $iMaxRetries
+ * @param int $iRemainingRetries
+ * @return int
+ */
+ public static function GetNextRetryDelay($bIsExponential, $iRetryDelay, $iMaxRetries, $iRemainingRetries)
+ {
+ if ($bIsExponential) {
+ $iExponent = $iMaxRetries - $iRemainingRetries;
+ if ($iExponent < 0) $iExponent = 0; // Safety net in case on configuration change in the middle of retries
+ return $iRetryDelay * (2 ** $iExponent);
+ } else {
+ return $iRetryDelay;
+ }
+ }
+
+ /**
+ * Override to notify people that a task cannot be performed
+ */
+ protected function OnDefinitiveFailure()
+ {
+ }
+
+ protected function OnInsert()
+ {
+ $this->Set('created', time());
+ }
+
+ /**
+ * @return boolean True if the task record can be deleted
+ */
+ public function Process()
+ {
+ // By default: consider that the task is not completed
+ $bRet = false;
+
+ // Attempt to take the ownership
+ $iStatus = $this->MarkAsRunning();
+ if ($iStatus == self::OK) {
+ try {
+ $sStatus = $this->DoProcess();
+ if ($this->Get('event_id') != 0) {
+ $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
+ $oEventLog->Set('message', $sStatus);
+ $oEventLog->DBUpdate();
+ }
+ $bRet = true;
+ } catch (Exception $e) {
+ $this->HandleError($e->getMessage(), $e->getCode());
+ }
+ } else {
+ // Already done or being handled by another process... skip...
+ $bRet = false;
+ }
+
+ return $bRet;
+ }
+
+ /**
+ * Overridable to extend the behavior in case of error (logging)
+ */
+ protected function HandleError($sErrorMessage, $iErrorCode)
+ {
+ if ($this->Get('last_attempt') == '') {
+ // First attempt
+ $this->Set('remaining_retries', $this->GetMaxRetries($iErrorCode));
+ }
+
+ $this->SetTrim('last_error', $sErrorMessage);
+ $this->Set('last_error_code', $iErrorCode); // Note: can be ZERO !!!
+ $this->Set('last_attempt', time());
+
+ $iRemaining = $this->Get('remaining_retries');
+ if ($iRemaining > 0) {
+ $iRetryDelay = $this->GetRetryDelay($iErrorCode);
+ $iNextRetryDelay = static::GetNextRetryDelay($this->IsRetryDelayExponential(), $iRetryDelay, $this->GetMaxRetries($iErrorCode), $iRemaining);
+ IssueLog::Info('Failed to process async task #' . $this->GetKey() . ' - reason: ' . $sErrorMessage . ' - remaining retries: ' . $iRemaining . ' - next retry in ' . $iNextRetryDelay . 's');
+ if ($this->Get('event_id') != 0) {
+ $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
+ $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task. Remaining retries: $iRemaining. Next retry in {$iNextRetryDelay}s");
+ try {
+ $oEventLog->DBUpdate();
+ } catch (Exception $e) {
+ $oEventLog->Set('message', "Failed to process async task. Remaining retries: $iRemaining. Next retry in {$iNextRetryDelay}s, more details in the log");
+ $oEventLog->DBUpdate();
+ }
+ }
+ $this->Set('remaining_retries', $iRemaining - 1);
+ $this->Set('status', 'planned');
+ $this->Set('started', null);
+ $this->Set('planned', time() + $iNextRetryDelay);
+ } else {
+ IssueLog::Error('Failed to process async task #' . $this->GetKey() . ' - reason: ' . $sErrorMessage);
+ if ($this->Get('event_id') != 0) {
+ $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
+ $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task.");
+ try {
+ $oEventLog->DBUpdate();
+ } catch (Exception $e) {
+ $oEventLog->Set('message', 'Failed to process async task, more details in the log');
+ $oEventLog->DBUpdate();
+ }
+ }
+ $this->Set('status', 'error');
+ $this->Set('started', null);
+ $this->Set('planned', null);
+ $this->OnDefinitiveFailure();
+ }
+ $this->DBUpdate();
+ }
+
+ /**
+ * Throws an exception (message and code)
+ *
+ * @return string
+ */
+ abstract public function DoProcess();
+
+ /**
+ * Describes the error codes that DoProcess can return by the mean of exceptions
+ */
+ static public function EnumErrorCodes()
+ {
+ return array();
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/AsyncTask/ExecAsyncTask.php b/sources/Core/DataModel/AsyncTask/ExecAsyncTask.php
new file mode 100644
index 0000000000..9c22b5e822
--- /dev/null
+++ b/sources/Core/DataModel/AsyncTask/ExecAsyncTask.php
@@ -0,0 +1,31 @@
+ true) /* order by*/, array(), null, 1 /* limit count */);
+ $oTask = $oSet->Fetch();
+ if (is_null($oTask)) {
+ // Nothing to be done
+ break;
+ }
+ $iProcessed++;
+ if ($oTask->Process()) {
+ $oTask->DBDelete();
+ }
+ }
+ return "processed $iProcessed tasks";
+ }
+}
\ No newline at end of file
diff --git a/core/backgroundtask.class.inc.php b/sources/Core/DataModel/BackgroundTask.php
similarity index 100%
rename from core/backgroundtask.class.inc.php
rename to sources/Core/DataModel/BackgroundTask.php
diff --git a/sources/Core/DataModel/BulkExportResult.php b/sources/Core/DataModel/BulkExportResult.php
new file mode 100644
index 0000000000..38b5aa0717
--- /dev/null
+++ b/sources/Core/DataModel/BulkExportResult.php
@@ -0,0 +1,46 @@
+ 'core/cmdb',
+ "key_type" => 'autoincrement',
+ "name_attcode" => array('created'),
+ "state_attcode" => '',
+ "reconc_keys" => array(),
+ "db_table" => 'priv_bulk_export_result',
+ "db_key_field" => 'id',
+ "db_finalclass_field" => '',
+ "display_template" => '',
+ );
+ MetaModel::Init_Params($aParams);
+
+ MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values" => null, "sql" => "created", "default_value" => "NOW()", "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeInteger("user_id", array("allowed_values" => null, "sql" => "user_id", "default_value" => 0, "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeInteger("chunk_size", array("allowed_values" => null, "sql" => "chunk_size", "default_value" => 0, "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeString("format", array("allowed_values" => null, "sql" => "format", "default_value" => '', "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeString("temp_file_path", array("allowed_values" => null, "sql" => "temp_file_path", "default_value" => '', "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeLongText("search", array("allowed_values" => null, "sql" => "search", "default_value" => '', "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeLongText("status_info", array("allowed_values" => null, "sql" => "status_info", "default_value" => '', "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeBoolean("localize_output", array("allowed_values" => null, "sql" => "localize_output", "default_value" => true, "is_null_allowed" => true, "depends_on" => array())));
+
+ }
+
+ /**
+ * @throws CoreUnexpectedValue
+ * @throws Exception
+ */
+ public function ComputeValues()
+ {
+ $this->Set('user_id', UserRights::GetUserId());
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOp.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOp.php
new file mode 100644
index 0000000000..6db47390bc
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOp.php
@@ -0,0 +1,66 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "autoincrement",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "optype",
+ 'indexes' => array(
+ array('objclass', 'objkey'),
+ ),
+ );
+ MetaModel::Init_Params($aParams);
+ //MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeExternalKey("change", array("allowed_values" => null, "sql" => "changeid", "targetclass" => "CMDBChange", "is_null_allowed" => false, "on_target_delete" => DEL_MANUAL, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeExternalField("date", array("allowed_values" => null, "extkey_attcode" => "change", "target_attcode" => "date")));
+ MetaModel::Init_AddAttribute(new AttributeExternalField("userinfo", array("allowed_values" => null, "extkey_attcode" => "change", "target_attcode" => "userinfo")));
+ MetaModel::Init_AddAttribute(new AttributeExternalField("user_id", array("allowed_values" => null, "extkey_attcode" => "change", "target_attcode" => "user_id")));
+ MetaModel::Init_AddAttribute(new AttributeString("objclass", array("allowed_values" => null, "sql" => "objclass", "default_value" => "", "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeObjectKey("objkey", array("allowed_values" => null, "class_attcode" => "objclass", "sql" => "objkey", "is_null_allowed" => false, "depends_on" => array())));
+
+ MetaModel::Init_SetZListItems('details', array('change', 'date', 'userinfo')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('change', 'date', 'userinfo')); // Attributes to be displayed for the complete details
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ return '';
+ }
+
+ /**
+ * Safety net:
+ * * if change isn't persisted yet, use the current change and persist it if needed
+ * * in case the change is not given, let's guarantee that it will be set to the current ongoing change (or create a new one)
+ *
+ * @since 2.7.7 3.0.2 3.1.0 N°3717 do persist the current change if needed
+ */
+ protected function OnInsert()
+ {
+ $iChange = $this->Get('change');
+ if (($iChange <= 0) || (is_null($iChange))) {
+ $oChange = CMDBObject::GetCurrentChange();
+ if ($oChange->IsNew()) {
+ $oChange->DBWrite();
+ }
+ $this->Set('change', $oChange);
+ }
+
+ parent::OnInsert();
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpCreate.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpCreate.php
new file mode 100644
index 0000000000..f307bde8b6
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpCreate.php
@@ -0,0 +1,37 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_create",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ return Dict::S('Change:ObjectCreated');
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpDelete.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpDelete.php
new file mode 100644
index 0000000000..23dad7263c
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpDelete.php
@@ -0,0 +1,42 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_delete",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+
+ // Final class of the object (objclass must be set to the root class for efficiency purposes)
+ MetaModel::Init_AddAttribute(new AttributeString("fclass", array("allowed_values" => null, "sql" => "fclass", "default_value" => "", "is_null_allowed" => false, "depends_on" => array())));
+ // Last friendly name of the object
+ MetaModel::Init_AddAttribute(new AttributeString("fname", array("allowed_values" => null, "sql" => "fname", "default_value" => "", "is_null_allowed" => true, "depends_on" => array())));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ return Dict::S('Change:ObjectDeleted');
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpPlugin.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpPlugin.php
new file mode 100644
index 0000000000..e9933c5694
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpPlugin.php
@@ -0,0 +1,42 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_plugin",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values" => null, "sql" => "description", "default_value" => '', "is_null_allowed" => false, "depends_on" => array())));
+ /* May be used later when implementing an extension mechanism that will allow the plug-ins to store some extra information and still degrades gracefully when the plug-in is desinstalled
+ MetaModel::Init_AddAttribute(new AttributeString("extension_class", array("allowed_values"=>null, "sql"=>"extension_class", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
+ MetaModel::Init_AddAttribute(new AttributeInteger("extension_id", array("allowed_values"=>null, "sql"=>"extension_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
+ */
+ MetaModel::Init_InheritAttributes();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ return $this->Get('description');
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttribute.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttribute.php
new file mode 100644
index 0000000000..5a3a7b7f5f
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttribute.php
@@ -0,0 +1,34 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values" => null, "sql" => "attcode", "default_value" => "", "is_null_allowed" => false, "depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeBlob.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeBlob.php
new file mode 100644
index 0000000000..4fea93cc4c
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeBlob.php
@@ -0,0 +1,81 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_data",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeBlob("prevdata", array("depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ // Temporary, until we change the options of GetDescription() -needs a more global revision
+ $bIsHtml = true;
+
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) {
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ } else {
+ // The attribute was renamed or removed from the object ?
+ $sAttName = $this->Get('attcode');
+ }
+ /** @var \ormDocument $oPrevDoc */
+ $oPrevDoc = $this->Get('prevdata');
+ if ($oPrevDoc->IsEmpty()) {
+ $sPrevious = '';
+ $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sPrevious);
+ } else {
+ $sFieldAsHtml = $oPrevDoc->GetAsHTML();
+
+ $sDisplayLabel = Dict::S('UI:OpenDocumentInNewWindow_');
+ $sDisplayUrl = $oPrevDoc->GetDisplayURL(get_class($this), $this->GetKey(), 'prevdata');
+
+ $sDownloadLabel = Dict::S('UI:DownloadDocument_');
+ $sDownloadUrl = $oPrevDoc->GetDownloadURL(get_class($this), $this->GetKey(), 'prevdata');
+
+ $sDocView = <<{$sDisplayLabel} / {$sDownloadLabel}
+HTML;
+ $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sDocView);
+ }
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeCaseLog.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeCaseLog.php
new file mode 100644
index 0000000000..eb47859277
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeCaseLog.php
@@ -0,0 +1,79 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_log",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeInteger("lastentry", array("allowed_values" => null, "sql" => "lastentry", "default_value" => 0, "is_null_allowed" => true, "depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ // Temporary, until we change the options of GetDescription() -needs a more global revision
+ $bIsHtml = true;
+
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) {
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ } else {
+ // The attribute was renamed or removed from the object ?
+ $sAttName = $this->Get('attcode');
+ }
+ $oObj = $oMonoObjectSet->Fetch();
+ $oCaseLog = $oObj->Get($this->Get('attcode'));
+ $sTextEntry = '' . $oCaseLog->GetEntryAt($this->Get('lastentry')) . '
';
+
+ $sResult = Dict::Format('Change:AttName_EntryAdded', $sAttName, $sTextEntry);
+ }
+ return $sResult;
+ }
+
+ /**
+ * @param string $sRawText
+ *
+ * @return string
+ */
+ protected function ToHtml($sRawText)
+ {
+ return str_replace(array("\r\n", "\n", "\r"), "
", utils::EscapeHtml($sRawText));
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeCustomFields.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeCustomFields.php
new file mode 100644
index 0000000000..baa09430af
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeCustomFields.php
@@ -0,0 +1,70 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_custfields",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeLongText("prevdata", array("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ $sResult = '';
+ if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode')))
+ {
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES)
+ {
+ $aValues = json_decode($this->Get('prevdata'), true);
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+
+ try
+ {
+ $oHandler = $oAttDef->GetHandler($aValues);
+ $sValueDesc = $oHandler->GetAsHTML($aValues);
+ }
+ catch (Exception $e) {
+ $sValueDesc = 'Custom field error: '.utils::EscapeHtml($e->getMessage());
+ }
+ $sTextView = ''.$sValueDesc.'
';
+
+ $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView);
+ }
+ }
+ return $sResult;
+ }
+}
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeEncrypted.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeEncrypted.php
new file mode 100644
index 0000000000..1f983f495e
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeEncrypted.php
@@ -0,0 +1,61 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_encrypted",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeEncryptedString("prevstring", array("sql" => 'data', "default_value" => '', "is_null_allowed" => true, "allowed_values" => null, "depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ // Temporary, until we change the options of GetDescription() -needs a more global revision
+ $bIsHtml = true;
+
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) {
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ } else {
+ // The attribute was renamed or removed from the object ?
+ $sAttName = $this->Get('attcode');
+ }
+ $sPrevString = $this->GetAsHTML('prevstring');
+ $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sPrevString);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeHTML.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeHTML.php
new file mode 100644
index 0000000000..7a9d8b1625
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeHTML.php
@@ -0,0 +1,61 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_html",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) {
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ } else {
+ // The attribute was renamed or removed from the object ?
+ $sAttName = $this->Get('attcode');
+ }
+ $sTextView = $this->Get('prevdata');
+
+ //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata');
+ $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinks.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinks.php
new file mode 100644
index 0000000000..601d41bc83
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinks.php
@@ -0,0 +1,34 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_links",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+
+ // Note: item class/id points to the link class itself in case of a direct link set (e.g. Server::interface_list => Interface)
+ // item class/id points to the remote class in case of a indirect link set (e.g. Server::contract_list => Contract)
+ MetaModel::Init_AddAttribute(new AttributeString("item_class", array("allowed_values" => null, "sql" => "item_class", "default_value" => '', "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeInteger("item_id", array("allowed_values" => null, "sql" => "item_id", "default_value" => 0, "is_null_allowed" => false, "depends_on" => array())));
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinksAddRemove.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinksAddRemove.php
new file mode 100644
index 0000000000..e40dc3eb12
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinksAddRemove.php
@@ -0,0 +1,65 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_links_addremove",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+
+ MetaModel::Init_AddAttribute(new AttributeEnum("type", array("allowed_values" => new ValueSetEnum('added,removed'), "sql" => "type", "default_value" => "added", "is_null_allowed" => false, "depends_on" => array())));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes...
+
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+
+ $sItemDesc = MetaModel::GetHyperLink($this->Get('item_class'), $this->Get('item_id'));
+
+ $sResult = $sAttName . ' - ';
+ switch ($this->Get('type')) {
+ case 'added':
+ $sResult .= Dict::Format('Change:LinkSet:Added', $sItemDesc);
+ break;
+
+ case 'removed':
+ $sResult .= Dict::Format('Change:LinkSet:Removed', $sItemDesc);
+ break;
+ }
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinksTune.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinksTune.php
new file mode 100644
index 0000000000..0c46fb4152
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLinksTune.php
@@ -0,0 +1,86 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_links_tune",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+
+ MetaModel::Init_AddAttribute(new AttributeInteger("link_id", array("allowed_values" => null, "sql" => "link_id", "default_value" => 0, "is_null_allowed" => false, "depends_on" => array())));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes...
+
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+
+ $sLinkClass = $oAttDef->GetLinkedClass();
+ $aLinkClasses = MetaModel::EnumChildClasses($sLinkClass, ENUM_CHILD_CLASSES_ALL);
+
+ // Search for changes on the corresponding link
+ //
+ $oSearch = new DBObjectSearch('CMDBChangeOpSetAttribute');
+ $oSearch->AddCondition('change', $this->Get('change'), '=');
+ $oSearch->AddCondition('objkey', $this->Get('link_id'), '=');
+ if (count($aLinkClasses) == 1) {
+ // Faster than the whole building of the expression below for just one value ??
+ $oSearch->AddCondition('objclass', $sLinkClass, '=');
+ } else {
+ $oField = new FieldExpression('objclass', $oSearch->GetClassAlias());
+ $sListExpr = '(' . implode(', ', CMDBSource::Quote($aLinkClasses)) . ')';
+ $sOQLCondition = $oField->RenderExpression() . " IN $sListExpr";
+ $oNewCondition = Expression::FromOQL($sOQLCondition);
+ $oSearch->AddConditionExpression($oNewCondition);
+ }
+ $oSet = new DBObjectSet($oSearch);
+ $aChanges = array();
+ while ($oChangeOp = $oSet->Fetch()) {
+ $aChanges[] = $oChangeOp->GetDescription();
+ }
+ if (count($aChanges) == 0) {
+ return '';
+ }
+
+ $sItemDesc = MetaModel::GetHyperLink($this->Get('item_class'), $this->Get('item_id'));
+
+ $sResult = $sAttName . ' - ';
+ $sResult .= Dict::Format('Change:LinkSet:Modified', $sItemDesc);
+ $sResult .= ' : ' . implode(', ', $aChanges);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLongText.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLongText.php
new file mode 100644
index 0000000000..c020a0eedf
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeLongText.php
@@ -0,0 +1,62 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_longtext",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeLongText("prevdata", array("allowed_values" => null, "sql" => "prevdata", "default_value" => "", "is_null_allowed" => true, "depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) {
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ } else {
+ // The attribute was renamed or removed from the object ?
+ $sAttName = $this->Get('attcode');
+ }
+ $sTextView = '' . $this->GetAsHtml('prevdata') . '
';
+
+ //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata');
+ $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeOneWayPassword.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeOneWayPassword.php
new file mode 100644
index 0000000000..1ae5c93858
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeOneWayPassword.php
@@ -0,0 +1,60 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_pwd",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeOneWayPassword("prev_pwd", array("sql" => 'data', "default_value" => '', "is_null_allowed" => true, "allowed_values" => null, "depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ // Temporary, until we change the options of GetDescription() -needs a more global revision
+ $bIsHtml = true;
+
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) {
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ } else {
+ // The attribute was renamed or removed from the object ?
+ $sAttName = $this->Get('attcode');
+ }
+ $sResult = Dict::Format('Change:AttName_Changed', $sAttName);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeScalar.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeScalar.php
new file mode 100644
index 0000000000..2a5e286895
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeScalar.php
@@ -0,0 +1,59 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_scalar",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeString("oldvalue", array("allowed_values" => null, "sql" => "oldvalue", "default_value" => null, "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeString("newvalue", array("allowed_values" => null, "sql" => "newvalue", "default_value" => null, "is_null_allowed" => true, "depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes...
+
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ $sNewValue = $this->Get('newvalue');
+ $sOldValue = $this->Get('oldvalue');
+ $sResult = $oAttDef->DescribeChangeAsHTML($sOldValue, $sNewValue);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeTagSet.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeTagSet.php
new file mode 100644
index 0000000000..c0ab1bf0a7
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeTagSet.php
@@ -0,0 +1,60 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_tagset",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeText("oldvalue", array("allowed_values" => null, "sql" => "oldvalue", "default_value" => null, "is_null_allowed" => true, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeText("newvalue", array("allowed_values" => null, "sql" => "newvalue", "default_value" => null, "is_null_allowed" => true, "depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ $sResult = '';
+ $sTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $sAttCode = $this->Get('attcode');
+ $oTargetSearch = new DBObjectSearch($sTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($sTargetObjectClass, $sAttCode, UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes...
+
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ $sNewValue = $this->Get('newvalue');
+ $sOldValue = $this->Get('oldvalue');
+ $sResult = $oAttDef->DescribeChangeAsHTML($sOldValue, $sNewValue);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeText.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeText.php
new file mode 100644
index 0000000000..4636917ce0
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeText.php
@@ -0,0 +1,65 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_text",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeText("prevdata", array("allowed_values" => null, "sql" => "prevdata", "default_value" => "", "is_null_allowed" => true, "depends_on" => array())));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ // Temporary, until we change the options of GetDescription() -needs a more global revision
+ $bIsHtml = true;
+
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) {
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ } else {
+ // The attribute was renamed or removed from the object ?
+ $sAttName = $this->Get('attcode');
+ }
+ $sTextView = '' . $this->GetAsHtml('prevdata') . '
';
+
+ //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata');
+ $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeURL.php b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeURL.php
new file mode 100644
index 0000000000..4b061232c9
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/CMDBChangeOpSetAttributeURL.php
@@ -0,0 +1,69 @@
+ "core/cmdb, grant_by_profile",
+ "key_type" => "",
+ "name_attcode" => "change",
+ "state_attcode" => "",
+ "reconc_keys" => array(),
+ "db_table" => "priv_changeop_setatt_url",
+ "db_key_field" => "id",
+ "db_finalclass_field" => "",
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+
+ // N°4910 (oldvalue), N°5423 (newvalue)
+ // We cannot have validation here, as AttributeUrl validation is field dependant.
+ // The validation will be done when editing the iTop object, it isn't the history API responsibility
+ //
+ // Pattern is retrieved using this order :
+ // 1. try to get the pattern from the field definition (datamodel)
+ // 2. from the iTop config
+ // 3. config parameter default value
+ // see \AttributeURL::GetValidationPattern
+ MetaModel::Init_AddAttribute(new AttributeURL("oldvalue", array("allowed_values" => null, "sql" => "oldvalue", "target" => '_blank', "default_value" => null, "is_null_allowed" => true, "depends_on" => array(), "validation_pattern" => '.*')));
+ MetaModel::Init_AddAttribute(new AttributeURL("newvalue", array("allowed_values" => null, "sql" => "newvalue", "target" => '_blank', "default_value" => null, "is_null_allowed" => true, "depends_on" => array(), "validation_pattern" => '.*')));
+
+ // Display lists
+ MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for the complete details
+ MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for a list
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function GetDescription()
+ {
+ $sResult = '';
+ $oTargetObjectClass = $this->Get('objclass');
+ $oTargetObjectKey = $this->Get('objkey');
+ $oTargetSearch = new DBObjectSearch($oTargetObjectClass);
+ $oTargetSearch->AddCondition('id', $oTargetObjectKey, '=');
+
+ $oMonoObjectSet = new DBObjectSet($oTargetSearch);
+ if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) {
+ if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes...
+
+ $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
+ $sAttName = $oAttDef->GetLabel();
+ $sNewValue = $this->Get('newvalue');
+ $sOldValue = $this->Get('oldvalue');
+ $sResult = $oAttDef->DescribeChangeAsHTML($sOldValue, $sNewValue);
+ }
+ return $sResult;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/DataModel/CMDBChange/iCMDBChangeOp.php b/sources/Core/DataModel/CMDBChange/iCMDBChangeOp.php
new file mode 100644
index 0000000000..9e8c14b141
--- /dev/null
+++ b/sources/Core/DataModel/CMDBChange/iCMDBChangeOp.php
@@ -0,0 +1,16 @@
+ '',
+ 'key_type' => 'autoincrement',
+ 'name_attcode' => array('key_name'),
+ 'state_attcode' => '',
+ 'reconc_keys' => array(''),
+ 'db_table' => 'key_value_store',
+ 'db_key_field' => 'id',
+ 'db_finalclass_field' => '',
+ 'indexes' => array(
+ array(
+ 0 => 'key_name',
+ 1 => 'namespace',
+ ),
+ ),
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeString("namespace", array("allowed_values" => null, "sql" => 'namespace', "default_value" => null, "is_null_allowed" => true, "depends_on" => array(), "always_load_in_tables" => false)));
+ MetaModel::Init_AddAttribute(new AttributeString("key_name", array("allowed_values" => null, "sql" => 'key_name', "default_value" => '', "is_null_allowed" => false, "depends_on" => array(), "always_load_in_tables" => false)));
+ MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values" => null, "sql" => 'value', "default_value" => '0', "is_null_allowed" => false, "depends_on" => array(), "always_load_in_tables" => false)));
+
+ MetaModel::Init_SetZListItems('details', array(
+ 0 => 'key_name',
+ 1 => 'value',
+ 2 => 'namespace',
+ ));
+ MetaModel::Init_SetZListItems('standard_search', array(
+ 0 => 'key_name',
+ 1 => 'value',
+ 2 => 'namespace',
+ ));
+ MetaModel::Init_SetZListItems('list', array(
+ 0 => 'key_name',
+ 1 => 'value',
+ 2 => 'namespace',
+ ));;
+ }
+
+
+}
\ No newline at end of file
diff --git a/core/tagsetfield.class.inc.php b/sources/Core/DataModel/TagSetFieldData.php
similarity index 100%
rename from core/tagsetfield.class.inc.php
rename to sources/Core/DataModel/TagSetFieldData.php
diff --git a/core/TemporaryObjectDescriptor.php b/sources/Core/DataModel/TemporaryObjectDescriptor.php
similarity index 100%
rename from core/TemporaryObjectDescriptor.php
rename to sources/Core/DataModel/TemporaryObjectDescriptor.php
diff --git a/sources/Core/ItopCounter.php b/sources/Core/ItopCounter.php
new file mode 100644
index 0000000000..6f79590142
--- /dev/null
+++ b/sources/Core/ItopCounter.php
@@ -0,0 +1,163 @@
+Lock();
+
+ $bIsInsideTransaction = CMDBSource::IsInsideTransaction();
+ if ($bIsInsideTransaction) {
+ // # Transaction isolation hack:
+ // When inside a transaction, we need to open a new connection for the counter.
+ // So it is visible immediately to the connections outside of the transaction.
+ // Either way, the lock is not long enought, and there would be duplicate ref.
+ //
+ // SELECT ... FOR UPDATE would have also worked but with the cost of extra long lock (until the commit),
+ // we did not wanted this! As opening a short connection is less prone to starving than a long running one.
+ // Plus it would trigger way more deadlocks!
+ $hDBLink = self::InitMySQLSession();
+ } else {
+ $hDBLink = CMDBSource::GetMysqli();
+ }
+
+ try {
+ $oFilter = DBObjectSearch::FromOQL('SELECT KeyValueStore WHERE key_name=:key_name AND namespace=:namespace', array(
+ 'key_name' => $sCounterName,
+ 'namespace' => $sSelfClassName,
+ ));
+ $oAttDef = MetaModel::GetAttributeDef(KeyValueStore::class, 'value');
+ $aAttToLoad = array(KeyValueStore::class => array('value' => $oAttDef));
+ $sSql = $oFilter->MakeSelectQuery(array(), array(), $aAttToLoad);
+ $hResult = mysqli_query($hDBLink, $sSql);
+ $aCounter = mysqli_fetch_array($hResult, MYSQLI_NUM);
+ mysqli_free_result($hResult);
+
+ //Rebuild the filter, as the MakeSelectQuery polluted the orignal and it cannot be reused
+ $oFilter = DBObjectSearch::FromOQL('SELECT KeyValueStore WHERE key_name=:key_name AND namespace=:namespace', array(
+ 'key_name' => $sCounterName,
+ 'namespace' => $sSelfClassName,
+ ));
+
+ if (is_null($aCounter)) {
+ if (null != $oNewObjectValueProvider) {
+ $iComputedValue = $oNewObjectValueProvider();
+ } else {
+ $iComputedValue = 0;
+ }
+
+ $iCurrentValue = $iComputedValue + 1;
+
+ $aQueryParams = array(
+ 'key_name' => $sCounterName,
+ 'value' => "$iCurrentValue",
+ 'namespace' => $sSelfClassName,
+ );
+
+ $sSql = $oFilter->MakeInsertQuery($aQueryParams);
+ } else {
+ $iCurrentValue = (int)$aCounter[1];
+ $iCurrentValue++;
+ $aQueryParams = array(
+ 'value' => "$iCurrentValue",
+ );
+
+ $sSql = $oFilter->MakeUpdateQuery($aQueryParams);
+ }
+
+ $hResult = mysqli_query($hDBLink, $sSql);
+
+ } catch (Exception $e) {
+ IssueLog::Error($e->getMessage());
+ throw $e;
+ } finally {
+ if ($bIsInsideTransaction) {
+ mysqli_close($hDBLink);
+ }
+ $oiTopMutex->Unlock();
+ }
+
+ return $iCurrentValue;
+ }
+
+ /**
+ * handle a counter for the root class of given $sLeafClass.
+ * If no counter exist initialize it with the `max(id) + 1`
+ *
+ *
+ *
+ * @param $sLeafClass
+ *
+ * @return int
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException
+ * @throws \CoreOqlMultipleResultsForbiddenException
+ * @throws \CoreUnexpectedValue
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public static function IncClass($sLeafClass)
+ {
+ $sRootClass = MetaModel::GetRootClass($sLeafClass);
+
+ $oNewObjectCallback = function () use ($sRootClass) {
+ $sRootTable = MetaModel::DBGetTable($sRootClass);
+ $sIdField = MetaModel::DBGetKey($sRootClass);
+
+ return CMDBSource::QueryToScalar("SELECT max(`$sIdField`) FROM `$sRootTable`");
+ };
+
+ return self::Inc($sRootClass, $oNewObjectCallback);
+ }
+
+ /**
+ * @return \mysqli
+ * @throws \ConfigException
+ * @throws \CoreException
+ * @throws \MySQLException
+ */
+ private static function InitMySQLSession()
+ {
+ $oConfig = utils::GetConfig();
+ $sDBHost = $oConfig->Get('db_host');
+ $sDBUser = $oConfig->Get('db_user');
+ $sDBPwd = $oConfig->Get('db_pwd');
+ $sDBName = $oConfig->Get('db_name');
+ $bDBTlsEnabled = $oConfig->Get('db_tls.enabled');
+ $sDBTlsCA = $oConfig->Get('db_tls.ca');
+
+ $hDBLink = CMDBSource::GetMysqliInstance($sDBHost, $sDBUser, $sDBPwd, $sDBName, $bDBTlsEnabled, $sDBTlsCA, false);
+
+ if (!$hDBLink) {
+ throw new MySQLException('Could not connect to the DB server ' . mysqli_connect_error() . ' (mysql errno: ' . mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser));
+ }
+
+ return $hDBLink;
+ }
+}
\ No newline at end of file
diff --git a/core/ormcaselog.class.inc.php b/sources/Core/Orm/ormCaseLog.php
similarity index 100%
rename from core/ormcaselog.class.inc.php
rename to sources/Core/Orm/ormCaseLog.php
diff --git a/core/ormcustomfieldsvalue.class.inc.php b/sources/Core/Orm/ormCustomFieldsValue.php
similarity index 100%
rename from core/ormcustomfieldsvalue.class.inc.php
rename to sources/Core/Orm/ormCustomFieldsValue.php
diff --git a/core/ormdocument.class.inc.php b/sources/Core/Orm/ormDocument.php
similarity index 100%
rename from core/ormdocument.class.inc.php
rename to sources/Core/Orm/ormDocument.php
diff --git a/core/ormlinkset.class.inc.php b/sources/Core/Orm/ormLinkSet.php
similarity index 99%
rename from core/ormlinkset.class.inc.php
rename to sources/Core/Orm/ormLinkSet.php
index b875fe0b6a..36f7867303 100644
--- a/core/ormlinkset.class.inc.php
+++ b/sources/Core/Orm/ormLinkSet.php
@@ -17,7 +17,7 @@
* You should have received a copy of the GNU Affero General Public License
*/
-require_once('dbobjectiterator.php');
+require_once(APPROOT.'core/dbobjectiterator.php');
/**
diff --git a/core/ormpassword.class.inc.php b/sources/Core/Orm/ormPassword.php
similarity index 84%
rename from core/ormpassword.class.inc.php
rename to sources/Core/Orm/ormPassword.php
index a90780bdd8..a643e0a42e 100644
--- a/core/ormpassword.class.inc.php
+++ b/sources/Core/Orm/ormPassword.php
@@ -17,7 +17,13 @@
// along with iTop. If not, see
-require_once(APPROOT.'/core/simplecrypt.class.inc.php');
+require_once(APPROOT.'/sources/Application/SimpleCrypt/CryptEngine.php');
+require_once(APPROOT.'/sources/Application/SimpleCrypt/SimpleCrypt.php');
+require_once(APPROOT.'/sources/Application/SimpleCrypt/SimpleCryptMcryptEngine.php');
+require_once(APPROOT.'/sources/Application/SimpleCrypt/SimpleCryptOpenSSLEngine.php');
+require_once(APPROOT.'/sources/Application/SimpleCrypt/SimpleCryptOpenSSLMcryptCompatibilityEngine.php');
+require_once(APPROOT.'/sources/Application/SimpleCrypt/SimpleCryptSimpleEngine.php');
+require_once(APPROOT.'/sources/Application/SimpleCrypt/SimpleCryptSodiumEngine.php');
/**
* ormPassword
diff --git a/core/ormset.class.inc.php b/sources/Core/Orm/ormSet.php
similarity index 100%
rename from core/ormset.class.inc.php
rename to sources/Core/Orm/ormSet.php
diff --git a/core/ormstopwatch.class.inc.php b/sources/Core/Orm/ormStopWatch.php
similarity index 99%
rename from core/ormstopwatch.class.inc.php
rename to sources/Core/Orm/ormStopWatch.php
index f8e38da0ff..c1188997ed 100644
--- a/core/ormstopwatch.class.inc.php
+++ b/sources/Core/Orm/ormStopWatch.php
@@ -16,7 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see
-require_once('backgroundprocess.inc.php');
+require_once(APPROOT.'core/backgroundprocess.inc.php');
/**
* ormStopWatch
diff --git a/core/ormStyle.class.inc.php b/sources/Core/Orm/ormStyle.php
similarity index 100%
rename from core/ormStyle.class.inc.php
rename to sources/Core/Orm/ormStyle.php
diff --git a/core/ormtagset.class.inc.php b/sources/Core/Orm/ormTagSet.php
similarity index 100%
rename from core/ormtagset.class.inc.php
rename to sources/Core/Orm/ormTagSet.php
diff --git a/sources/Core/RelationGraph/RelationEdge.php b/sources/Core/RelationGraph/RelationEdge.php
new file mode 100644
index 0000000000..f89da3c731
--- /dev/null
+++ b/sources/Core/RelationGraph/RelationEdge.php
@@ -0,0 +1,23 @@
+GetId().'-to-'.$oSinkNode->GetId();
+ parent::__construct($oGraph, $sId, $oSourceNode, $oSinkNode, $bMustBeUnique);
+ }
+}
\ No newline at end of file
diff --git a/core/relationgraph.class.inc.php b/sources/Core/RelationGraph/RelationGraph.php
similarity index 79%
rename from core/relationgraph.class.inc.php
rename to sources/Core/RelationGraph/RelationGraph.php
index 43b82f703f..febccf3c6e 100644
--- a/core/relationgraph.class.inc.php
+++ b/sources/Core/RelationGraph/RelationGraph.php
@@ -1,21 +1,4 @@
-
/**
* Data structures (i.e. PHP classes) to build and use relation graphs
@@ -25,169 +8,6 @@
*
*/
-require_once(APPROOT.'core/simplegraph.class.inc.php');
-
-/**
- * An object Node inside a RelationGraph
- */
-class RelationObjectNode extends GraphNode
-{
- public function __construct($oGraph, $oObject)
- {
- parent::__construct($oGraph, self::MakeId($oObject));
- $this->SetProperty('object', $oObject);
- $this->SetProperty('label', get_class($oObject).'::'.$oObject->GetKey().' ('.$oObject->Get('friendlyname').')');
- }
-
- /**
- * Make a normalized ID to ensure the uniqueness of such a node
- *
- * @param string $oObject
- *
- * @return string
- */
- public static function MakeId($oObject)
- {
- return get_class($oObject).'::'.$oObject->GetKey();
- }
-
- /**
- * Formatting for GraphViz
- *
- * @param bool $bNoLabel
- *
- * @return string
- */
- public function GetDotAttributes($bNoLabel = false)
- {
- $sDot = parent::GetDotAttributes();
- if ($this->GetProperty('developped', false))
- {
- $sDot .= ',fontcolor=black';
- }
- else
- {
- $sDot .= ',fontcolor=lightgrey';
- }
- if ($this->GetProperty('source', false) || $this->GetProperty('sink', false))
- {
- $sDot .= ',shape=rectangle';
- }
- if ($this->GetProperty('is_reached', false))
- {
- $sDot .= ',fillcolor="#ffdddd"';
- }
- else
- {
- $sDot .= ',fillcolor=white';
- }
- return $sDot;
- }
-
- /**
- * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node or a 'not allowed' node
- *
- * @param string $sProperty
- * @param $value
- */
- public function ReachDown($sProperty, $value)
- {
- if (is_null($this->GetProperty($sProperty)) && ($this->GetProperty($sProperty.'_allowed') !== false))
- {
- $this->SetProperty($sProperty, $value);
- foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
- {
- // Recurse
- $oOutgoingEdge->GetSinkNode()->ReachDown($sProperty, $value);
- }
- }
- }
-}
-
-/**
- * An redundancy Node inside a RelationGraph
- */
-class RelationRedundancyNode extends GraphNode
-{
- public function __construct($oGraph, $sId, $iMinUp, $fThreshold)
- {
- parent::__construct($oGraph, $sId);
- $this->SetProperty('min_up', $iMinUp);
- $this->SetProperty('threshold', $fThreshold);
- }
-
- /**
- * Make a normalized ID to ensure the uniqueness of such a node
- *
- * @param string $sRelCode
- * @param string $sNeighbourId
- * @param $oSourceObject
- * @param \DBObject $oSinkObject
- *
- * @return string
- */
- public static function MakeId($sRelCode, $sNeighbourId, $oSourceObject, $oSinkObject)
- {
- return 'redundancy-'.$sRelCode.'-'.$sNeighbourId.'-'.get_class($oSinkObject).'::'.$oSinkObject->GetKey();
- }
-
- /**
- * Formatting for GraphViz
- *
- * @param bool $bNoLabel
- *
- * @return string
- */
- public function GetDotAttributes($bNoLabel = false)
- {
- $sDisplayThreshold = sprintf('%.1f', $this->GetProperty('threshold'));
- $sDot = 'shape=doublecircle,fillcolor=indianred,fontcolor=papayawhip,label="'.$sDisplayThreshold.'"';
- return $sDot;
- }
-
- /**
- * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node
- *
- * @param string $sProperty
- * @param $value
- */
- public function ReachDown($sProperty, $value)
- {
- $this->SetProperty($sProperty.'_count', $this->GetProperty($sProperty.'_count', 0) + 1);
- if ($this->GetProperty($sProperty.'_count') > $this->GetProperty('threshold'))
- {
- // Looping... though there should be only ONE SINGLE outgoing edge
- foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
- {
- // Recurse
- $oOutgoingEdge->GetSinkNode()->ReachDown($sProperty, $value);
- }
- }
- }
-}
-
-
-/**
- * Helper to name the edges in a unique way
- */
-class RelationEdge extends GraphEdge
-{
- /**
- * RelationEdge constructor.
- *
- * @param \SimpleGraph $oGraph
- * @param \GraphNode $oSourceNode
- * @param \GraphNode $oSinkNode
- * @param bool $bMustBeUnique
- *
- * @throws \SimpleGraphException
- */
- public function __construct(SimpleGraph $oGraph, GraphNode $oSourceNode, GraphNode $oSinkNode, $bMustBeUnique = false)
- {
- $sId = $oSourceNode->GetId().'-to-'.$oSinkNode->GetId();
- parent::__construct($oGraph, $sId, $oSourceNode, $oSinkNode, $bMustBeUnique);
- }
-}
/**
* A graph representing the relations between objects
diff --git a/sources/Core/RelationGraph/RelationObjectNode.php b/sources/Core/RelationGraph/RelationObjectNode.php
new file mode 100644
index 0000000000..5aa479788a
--- /dev/null
+++ b/sources/Core/RelationGraph/RelationObjectNode.php
@@ -0,0 +1,70 @@
+SetProperty('object', $oObject);
+ $this->SetProperty('label', get_class($oObject).'::'.$oObject->GetKey().' ('.$oObject->Get('friendlyname').')');
+ }
+
+ /**
+ * Make a normalized ID to ensure the uniqueness of such a node
+ *
+ * @param string $oObject
+ *
+ * @return string
+ */
+ public static function MakeId($oObject)
+ {
+ return get_class($oObject).'::'.$oObject->GetKey();
+ }
+
+ /**
+ * Formatting for GraphViz
+ *
+ * @param bool $bNoLabel
+ *
+ * @return string
+ */
+ public function GetDotAttributes($bNoLabel = false)
+ {
+ $sDot = parent::GetDotAttributes();
+ if ($this->GetProperty('developped', false)) {
+ $sDot .= ',fontcolor=black';
+ } else {
+ $sDot .= ',fontcolor=lightgrey';
+ }
+ if ($this->GetProperty('source', false) || $this->GetProperty('sink', false)) {
+ $sDot .= ',shape=rectangle';
+ }
+ if ($this->GetProperty('is_reached', false)) {
+ $sDot .= ',fillcolor="#ffdddd"';
+ } else {
+ $sDot .= ',fillcolor=white';
+ }
+
+ return $sDot;
+ }
+
+ /**
+ * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node or a 'not allowed' node
+ *
+ * @param string $sProperty
+ * @param $value
+ */
+ public function ReachDown($sProperty, $value)
+ {
+ if (is_null($this->GetProperty($sProperty)) && ($this->GetProperty($sProperty.'_allowed') !== false)) {
+ $this->SetProperty($sProperty, $value);
+ foreach ($this->GetOutgoingEdges() as $oOutgoingEdge) {
+ // Recurse
+ $oOutgoingEdge->GetSinkNode()->ReachDown($sProperty, $value);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/RelationGraph/RelationRedundancyNode.php b/sources/Core/RelationGraph/RelationRedundancyNode.php
new file mode 100644
index 0000000000..d5aa2ed710
--- /dev/null
+++ b/sources/Core/RelationGraph/RelationRedundancyNode.php
@@ -0,0 +1,62 @@
+SetProperty('min_up', $iMinUp);
+ $this->SetProperty('threshold', $fThreshold);
+ }
+
+ /**
+ * Make a normalized ID to ensure the uniqueness of such a node
+ *
+ * @param string $sRelCode
+ * @param string $sNeighbourId
+ * @param $oSourceObject
+ * @param \DBObject $oSinkObject
+ *
+ * @return string
+ */
+ public static function MakeId($sRelCode, $sNeighbourId, $oSourceObject, $oSinkObject)
+ {
+ return 'redundancy-'.$sRelCode.'-'.$sNeighbourId.'-'.get_class($oSinkObject).'::'.$oSinkObject->GetKey();
+ }
+
+ /**
+ * Formatting for GraphViz
+ *
+ * @param bool $bNoLabel
+ *
+ * @return string
+ */
+ public function GetDotAttributes($bNoLabel = false)
+ {
+ $sDisplayThreshold = sprintf('%.1f', $this->GetProperty('threshold'));
+ $sDot = 'shape=doublecircle,fillcolor=indianred,fontcolor=papayawhip,label="'.$sDisplayThreshold.'"';
+
+ return $sDot;
+ }
+
+ /**
+ * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node
+ *
+ * @param string $sProperty
+ * @param $value
+ */
+ public function ReachDown($sProperty, $value)
+ {
+ $this->SetProperty($sProperty.'_count', $this->GetProperty($sProperty.'_count', 0) + 1);
+ if ($this->GetProperty($sProperty.'_count') > $this->GetProperty('threshold')) {
+ // Looping... though there should be only ONE SINGLE outgoing edge
+ foreach ($this->GetOutgoingEdges() as $oOutgoingEdge) {
+ // Recurse
+ $oOutgoingEdge->GetSinkNode()->ReachDown($sProperty, $value);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/ValueSetDefinition/ValueSetDefinition.php b/sources/Core/ValueSetDefinition/ValueSetDefinition.php
new file mode 100644
index 0000000000..bcc117c972
--- /dev/null
+++ b/sources/Core/ValueSetDefinition/ValueSetDefinition.php
@@ -0,0 +1,69 @@
+GetValues(array(), '');
+ $aDisplayedValues = array();
+ foreach ($aValues as $key => $value) {
+ $aDisplayedValues[] = "$key => $value";
+ }
+ $sAllowedValues = implode(', ', $aDisplayedValues);
+ return $sAllowedValues;
+ }
+
+ /**
+ * @param array $aArgs
+ * @param string $sContains
+ * @param string $sOperation for the values {@see static::LoadValues()}
+ *
+ * @return array hash array of keys => values
+ */
+ public function GetValues($aArgs, $sContains = '', $sOperation = 'contains')
+ {
+ if (!$this->m_bIsLoaded) {
+ $this->LoadValues($aArgs);
+ $this->m_bIsLoaded = true;
+ }
+ if (strlen($sContains) == 0) {
+ // No filtering
+ $aRet = $this->m_aValues;
+ } else {
+ // Filter on results containing the needle
+ $aRet = array();
+ foreach ($this->m_aValues as $sKey => $sValue) {
+ if (stripos($sValue, $sContains) !== false) {
+ $aRet[$sKey] = $sValue;
+ }
+ }
+ }
+ $this->SortValues($aRet);
+ return $aRet;
+ }
+
+ /**
+ * @param array $aValues Values to sort in the form keys => values
+ *
+ * @return void
+ * @since 3.1.0 N°1646 Create method
+ */
+ public function SortValues(array &$aValues): void
+ {
+ // Sort alphabetically on values
+ natcasesort($aValues);
+ }
+
+ abstract protected function LoadValues($aArgs);
+}
\ No newline at end of file
diff --git a/sources/Core/ValueSetDefinition/ValueSetEnum.php b/sources/Core/ValueSetDefinition/ValueSetEnum.php
new file mode 100644
index 0000000000..574a9cfc09
--- /dev/null
+++ b/sources/Core/ValueSetDefinition/ValueSetEnum.php
@@ -0,0 +1,94 @@
+m_values = $Values;
+ $this->bSortByValues = $bSortByValues;
+ }
+
+ /**
+ * @return bool
+ * @see \ValueSetEnum::$bSortByValues
+ * @since 3.1.0 N°1646
+ */
+ public function IsSortedByValues(): bool
+ {
+ return $this->bSortByValues;
+ }
+
+ // Helper to export the data model
+ public function GetValueList()
+ {
+ $this->LoadValues(null);
+ return $this->m_aValues;
+ }
+
+ /**
+ * @inheritDoc
+ * @since 3.1.0 N°1646 Overload method
+ */
+ public function SortValues(array &$aValues): void
+ {
+ // Force sort by values only if necessary
+ if ($this->bSortByValues) {
+ natcasesort($aValues);
+ return;
+ }
+
+ // Don't sort values as we rely on the order defined during compilation
+ return;
+ }
+
+ /**
+ * @param array|string $aArgs
+ *
+ * @return true
+ */
+ protected function LoadValues($aArgs)
+ {
+ $aValues = [];
+ if (is_array($this->m_values)) {
+ foreach ($this->m_values as $key => $value) {
+ // Handle backed-enum case
+ if (is_object($value) && enum_exists(get_class($value))) {
+ $aValues[$value->value] = $value->value;
+ continue;
+ }
+
+ $aValues[$key] = $value;
+ }
+ } elseif (is_string($this->m_values) && strlen($this->m_values) > 0) {
+ foreach (explode(",", $this->m_values) as $sVal) {
+ $sVal = trim($sVal);
+ $sKey = $sVal;
+ $aValues[$sKey] = $sVal;
+ }
+ } else {
+ $aValues = [];
+ }
+ $this->m_aValues = $aValues;
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/ValueSetDefinition/ValueSetEnumClasses.php b/sources/Core/ValueSetDefinition/ValueSetEnumClasses.php
new file mode 100644
index 0000000000..b2788f738f
--- /dev/null
+++ b/sources/Core/ValueSetDefinition/ValueSetEnumClasses.php
@@ -0,0 +1,43 @@
+m_sCategories = $sCategories;
+ parent::__construct($sAdditionalValues, true /* Classes are always sorted alphabetically */);
+ }
+
+ protected function LoadValues($aArgs)
+ {
+ // Call the parent to parse the additional values...
+ parent::LoadValues($aArgs);
+
+ // Translate the labels of the additional values
+ foreach ($this->m_aValues as $sClass => $void) {
+ if (MetaModel::IsValidClass($sClass)) {
+ $this->m_aValues[$sClass] = MetaModel::GetName($sClass);
+ } else {
+ unset($this->m_aValues[$sClass]);
+ }
+ }
+
+ // Then, add the classes from the category definition
+ foreach (MetaModel::GetClasses($this->m_sCategories) as $sClass) {
+ if (MetaModel::IsValidClass($sClass)) {
+ $this->m_aValues[$sClass] = MetaModel::GetName($sClass);
+ } else {
+ unset($this->m_aValues[$sClass]);
+ }
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/ValueSetDefinition/ValueSetEnumPadded.php b/sources/Core/ValueSetDefinition/ValueSetEnumPadded.php
new file mode 100644
index 0000000000..2f6f9e0758
--- /dev/null
+++ b/sources/Core/ValueSetDefinition/ValueSetEnumPadded.php
@@ -0,0 +1,25 @@
+LoadValues(null);
+ } else {
+ $this->m_aValues = $Values;
+ }
+ $aPaddedValues = array();
+ foreach ($this->m_aValues as $sKey => $sVal) {
+ // Pad keys to the min. length required by the \AttributeSet
+ $sKey = str_pad($sKey, 3, '_', STR_PAD_LEFT);
+ $aPaddedValues[$sKey] = $sVal;
+ }
+ $this->m_values = $aPaddedValues;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/ValueSetDefinition/ValueSetObjects.php b/sources/Core/ValueSetDefinition/ValueSetObjects.php
new file mode 100644
index 0000000000..6576d74fe2
--- /dev/null
+++ b/sources/Core/ValueSetDefinition/ValueSetObjects.php
@@ -0,0 +1,345 @@
+.]attcode' => bAscending
+ */
+ public function __construct($sFilterExp, $sValueAttCode = '', $aOrderBy = array(), $bAllowAllData = false, $aModifierProperties = array())
+ {
+ $this->m_sContains = '';
+ $this->m_sOperation = '';
+ $this->m_sFilterExpr = $sFilterExp;
+ $this->m_sValueAttCode = $sValueAttCode;
+ $this->m_aOrderBy = $aOrderBy;
+ $this->m_bAllowAllData = $bAllowAllData;
+ $this->m_aModifierProperties = $aModifierProperties;
+ $this->m_oExtraCondition = null;
+ $this->m_bSort = true;
+ $this->m_iLimit = 0;
+ }
+
+ public function SetModifierProperty($sPluginClass, $sProperty, $value)
+ {
+ $this->m_aModifierProperties[$sPluginClass][$sProperty] = $value;
+ $this->m_bIsLoaded = false;
+ }
+
+ /**
+ * @param \DBSearch $oFilter
+ * @deprecated use SetCondition instead
+ *
+ */
+ public function AddCondition(DBSearch $oFilter)
+ {
+ DeprecatedCallsLog::NotifyDeprecatedPhpMethod('use SetCondition instead');
+ $this->SetCondition($oFilter);
+ }
+
+ public function SetCondition(DBSearch $oFilter)
+ {
+ $this->m_oExtraCondition = $oFilter;
+ $this->m_bIsLoaded = false;
+ }
+
+ public function SetOrderBy(array $aOrderBy)
+ {
+ $this->m_aOrderBy = $aOrderBy;
+ }
+
+ public function ToObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null)
+ {
+ if ($this->m_bAllowAllData) {
+ $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr);
+ } else {
+ $oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr);
+ }
+ if (!is_null($this->m_oExtraCondition)) {
+ $oFilter = $oFilter->Intersect($this->m_oExtraCondition);
+ }
+ foreach ($this->m_aModifierProperties as $sPluginClass => $aProperties) {
+ foreach ($aProperties as $sProperty => $value) {
+ $oFilter->SetModifierProperty($sPluginClass, $sProperty, $value);
+ }
+ }
+ if ($iAdditionalValue > 0) {
+ $oSearchAdditionalValue = new DBObjectSearch($oFilter->GetClass());
+ $oSearchAdditionalValue->AddConditionExpression(new BinaryExpression(
+ new FieldExpression('id', $oSearchAdditionalValue->GetClassAlias()),
+ '=',
+ new VariableExpression('current_extkey_id'))
+ );
+ $oSearchAdditionalValue->AllowAllData();
+ $oSearchAdditionalValue->SetArchiveMode(true);
+ $oSearchAdditionalValue->SetInternalParams(array('current_extkey_id' => $iAdditionalValue));
+
+ $oFilter = new DBUnionSearch(array($oFilter, $oSearchAdditionalValue));
+ }
+
+ return new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs);
+ }
+
+ /**
+ * @inheritDoc
+ * @throws CoreException
+ * @throws OQLException
+ */
+ public function GetValues($aArgs, $sContains = '', $sOperation = 'contains')
+ {
+ if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains) || ($sOperation != $this->m_sOperation)) {
+ $this->LoadValues($aArgs, $sContains, $sOperation);
+ $this->m_bIsLoaded = true;
+ }
+ // The results are already filtered and sorted (on friendly name)
+ $aRet = $this->m_aValues;
+ return $aRet;
+ }
+
+ /**
+ * @param $aArgs
+ * @param string $sContains
+ * @param string $sOperation 'contains' or 'equals_start_with'
+ *
+ * @return bool
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ protected function LoadValues($aArgs, $sContains = '', $sOperation = 'contains')
+ {
+ $this->m_sContains = $sContains;
+ $this->m_sOperation = $sOperation;
+
+ $this->m_aValues = array();
+
+ $oFilter = $this->GetFilter($sOperation, $sContains);
+
+ $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs, null, $this->m_iLimit, 0, $this->m_bSort);
+ if (empty($this->m_sValueAttCode)) {
+ $aAttToLoad = array($oFilter->GetClassAlias() => array('friendlyname'));
+ } else {
+ $aAttToLoad = array($oFilter->GetClassAlias() => array($this->m_sValueAttCode));
+ }
+ $oObjects->OptimizeColumnLoad($aAttToLoad);
+ while ($oObject = $oObjects->Fetch()) {
+ if (empty($this->m_sValueAttCode)) {
+ $this->m_aValues[$oObject->GetKey()] = $oObject->GetName();
+ } else {
+ $this->m_aValues[$oObject->GetKey()] = $oObject->Get($this->m_sValueAttCode);
+ }
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Get filter for functions LoadValues and LoadValuesForAutocomplete
+ *
+ * @param $sOperation
+ * @param $sContains
+ *
+ * @return \DBObjectSearch|\DBSearch|\DBUnionSearch|false|mixed
+ * @throws \CoreException
+ * @throws \OQLException
+ * @since 3.0.3 3.1.0
+ */
+ protected function GetFilter($sOperation, $sContains)
+ {
+ $this->m_sContains = $sContains;
+ $this->m_sOperation = $sOperation;
+
+ if ($this->m_bAllowAllData) {
+ $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr);
+ } else {
+ $oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr);
+ $oFilter->SetShowObsoleteData(utils::ShowObsoleteData());
+ }
+ if (!$oFilter) {
+ return false;
+ }
+ if (!is_null($this->m_oExtraCondition)) {
+ $oFilter = $oFilter->Intersect($this->m_oExtraCondition);
+ }
+ foreach ($this->m_aModifierProperties as $sPluginClass => $aProperties) {
+ foreach ($aProperties as $sProperty => $value) {
+ $oFilter->SetModifierProperty($sPluginClass, $sProperty, $value);
+ }
+ }
+
+ $sClass = $oFilter->GetClass();
+
+ switch ($this->m_sOperation) {
+ case 'equals':
+ case 'start_with':
+ if ($this->m_sOperation === 'start_with') {
+ $this->m_sContains .= '%';
+ $sOperator = 'LIKE';
+ } else {
+ $sOperator = '=';
+ }
+
+ $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass);
+ if (count($aAttributes) > 0) {
+ $sClassAlias = $oFilter->GetClassAlias();
+ $aFilters = array();
+ $oValueExpr = new ScalarExpression($this->m_sContains);
+ foreach ($aAttributes as $sAttribute) {
+ $oNewFilter = $oFilter->DeepClone();
+ $oNameExpr = new FieldExpression($sAttribute, $sClassAlias);
+ $oCondition = new BinaryExpression($oNameExpr, $sOperator, $oValueExpr);
+ $oNewFilter->AddConditionExpression($oCondition);
+ $aFilters[] = $oNewFilter;
+ }
+ // Unions are much faster than OR conditions
+ $oFilter = new DBUnionSearch($aFilters);
+ } else {
+ $oValueExpr = new ScalarExpression($this->m_sContains);
+ $oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias());
+ $oNewCondition = new BinaryExpression($oNameExpr, $sOperator, $oValueExpr);
+ $oFilter->AddConditionExpression($oNewCondition);
+ }
+ break;
+
+ default:
+ $oValueExpr = new ScalarExpression('%' . $this->m_sContains . '%');
+ $oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias());
+ $oNewCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr);
+ $oFilter->AddConditionExpression($oNewCondition);
+ break;
+ }
+
+ return $oFilter;
+ }
+
+ public function GetValuesDescription()
+ {
+ return 'Filter: ' . $this->m_sFilterExpr;
+ }
+
+ public function GetFilterExpression()
+ {
+ return $this->m_sFilterExpr;
+ }
+
+ /**
+ * @param $iLimit
+ */
+ public function SetLimit($iLimit)
+ {
+ $this->m_iLimit = $iLimit;
+ }
+
+ /**
+ * @param $bSort
+ */
+ public function SetSort($bSort)
+ {
+ $this->m_bSort = $bSort;
+ }
+
+ public function GetValuesForAutocomplete($aArgs, $sContains = '', $sOperation = 'contains')
+ {
+ if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains) || ($sOperation != $this->m_sOperation)) {
+ $this->LoadValuesForAutocomplete($aArgs, $sContains, $sOperation);
+ $this->m_bIsLoaded = true;
+ }
+ // The results are already filtered and sorted (on friendly name)
+ $aRet = $this->m_aValues;
+ return $aRet;
+ }
+
+ /**
+ * @param $aArgs
+ * @param string $sContains
+ * @param string $sOperation 'contains' or 'equals_start_with'
+ *
+ * @return bool
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ protected function LoadValuesForAutocomplete($aArgs, $sContains = '', $sOperation = 'contains')
+ {
+ $this->m_aValues = array();
+
+ $oFilter = $this->GetFilter($sOperation, $sContains);
+ $sClass = $oFilter->GetClass();
+ $sClassAlias = $oFilter->GetClassAlias();
+
+ $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs, null, $this->m_iLimit, 0, $this->m_bSort);
+ if (empty($this->m_sValueAttCode)) {
+ $aAttToLoad = ['friendlyname'];
+ } else {
+ $aAttToLoad = [$this->m_sValueAttCode];
+ }
+
+ $sImageAttr = MetaModel::GetImageAttributeCode($sClass);
+ if (!empty($sImageAttr)) {
+ $aAttToLoad [] = $sImageAttr;
+ }
+
+ $aComplementAttributeSpec = MetaModel::GetNameSpec($sClass, \Combodo\iTop\Core\MetaModel\FriendlyNameType::COMPLEMENTARY);
+ $sFormatAdditionalField = $aComplementAttributeSpec[0];
+ $aAdditionalField = $aComplementAttributeSpec[1];
+
+ if (count($aAdditionalField) > 0) {
+ if (is_array($aAdditionalField)) {
+ $aAttToLoad = array_merge($aAttToLoad, $aAdditionalField);
+ } else {
+ $aAttToLoad [] = $aAdditionalField;
+ }
+ }
+
+ $oObjects->OptimizeColumnLoad([$sClassAlias => $aAttToLoad]);
+ while ($oObject = $oObjects->Fetch()) {
+ $aData = [];
+ if (empty($this->m_sValueAttCode)) {
+ $aData['label'] = $oObject->GetName();
+ } else {
+ $aData['label'] = $oObject->Get($this->m_sValueAttCode);
+ }
+ if ($oObject->IsObsolete()) {
+ $aData['obsolescence_flag'] = '1';
+ } else {
+ $aData['obsolescence_flag'] = '0';
+ }
+ if (count($aAdditionalField) > 0) {
+ $aArguments = [];
+ foreach ($aAdditionalField as $sAdditionalField) {
+ array_push($aArguments, $oObject->Get($sAdditionalField));
+ }
+ $aData['additional_field'] = utils::VSprintf($sFormatAdditionalField, $aArguments);
+ } else {
+ $aData['additional_field'] = '';
+ }
+ if (!empty($sImageAttr)) {
+ /** @var \ormDocument $oImage */
+ $oImage = $oObject->Get($sImageAttr);
+ if (!$oImage->IsEmpty()) {
+ $aData['picture_url'] = $oImage->GetDisplayURL($sClass, $oObject->GetKey(), $sImageAttr);
+ $aData['initials'] = '';
+ } else {
+ $aData['initials'] = utils::ToAcronym($aData['label']);
+ }
+ }
+ $this->m_aValues[$oObject->GetKey()] = $aData;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/sources/Core/ValueSetDefinition/ValueSetRange.php b/sources/Core/ValueSetDefinition/ValueSetRange.php
new file mode 100644
index 0000000000..f0353251b9
--- /dev/null
+++ b/sources/Core/ValueSetDefinition/ValueSetRange.php
@@ -0,0 +1,28 @@
+m_iStart = $iStart;
+ $this->m_iEnd = $iEnd;
+ $this->m_iStep = $iStep;
+ }
+
+ protected function LoadValues($aArgs)
+ {
+ $iValue = $this->m_iStart;
+ for ($iValue = $this->m_iStart; $iValue <= $this->m_iEnd; $iValue += $this->m_iStep) {
+ $this->m_aValues[$iValue] = $iValue;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/tests/php-unit-tests/unitary-tests/core/ValueSetObjectsTest.php b/tests/php-unit-tests/unitary-tests/core/ValueSetObjectsTest.php
index 74620b10d1..317e6b606e 100644
--- a/tests/php-unit-tests/unitary-tests/core/ValueSetObjectsTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/ValueSetObjectsTest.php
@@ -17,7 +17,16 @@ class ValueSetObjectsTest extends ItopTestCase
protected function setUp(): void
{
parent::setUp();
- $this->RequireOnceItopFile('core/valuesetdef.class.inc.php');
+
+ $this->RequireOnceItopFile('MyHelpers.class.inc.php');
+
+ $this->RequireOnceItopFile(APPROOT.'/sources/Core/ValueSetDefinition/ValueSetDefinition.php');
+ $this->RequireOnceItopFile(APPROOT.'/sources/Core/ValueSetDefinition/ValueSetObjects.php');
+ $this->RequireOnceItopFile(APPROOT.'/sources/Core/ValueSetDefinition/ValueSetEnum.php');
+ $this->RequireOnceItopFile(APPROOT.'/sources/Core/ValueSetDefinition/ValueSetEnumPadded.php');
+ $this->RequireOnceItopFile(APPROOT.'/sources/Core/ValueSetDefinition/ValueSetRange.php');
+ $this->RequireOnceItopFile(APPROOT.'/sources/Core/ValueSetDefinition/ValueSetEnumClasses.php');
+
$this->RequireOnceItopFile('application/startup.inc.php');
$this->RequireOnceUnitTestFile('./MockValueSetObjects.php');
}
diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-config/BulkChangeExtKeyTest.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-config/BulkChangeExtKeyTest.php
index f16adeb739..c698d4e690 100644
--- a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-config/BulkChangeExtKeyTest.php
+++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-config/BulkChangeExtKeyTest.php
@@ -27,7 +27,7 @@ class BulkChangeExtKeyTest extends ItopDataTestCase {
protected function setUp() : void {
parent::setUp();
- require_once(APPROOT.'core/bulkchange.class.inc.php');
+ //require_once(APPROOT.'core/bulkchange.class.inc.php');
}
private function deleteAllRacks(){
diff --git a/webservices/export-v2.php b/webservices/export-v2.php
index a902bf0442..6b68da052a 100644
--- a/webservices/export-v2.php
+++ b/webservices/export-v2.php
@@ -26,7 +26,10 @@
require_once(__DIR__.'/../approot.inc.php');
require_once(APPROOT.'/application/application.inc.php');
require_once(APPROOT.'/application/excelexporter.class.inc.php');
-require_once(APPROOT.'/core/bulkexport.class.inc.php');
+require_once(APPROOT.'/sources/Application/BulkExport/BulkExport.php');
+require_once(APPROOT.'/sources/Application/BulkExport/BulkExportException.php');
+require_once(APPROOT.'/sources/Application/BulkExport/BulkExportMissingParameterException.php');
+require_once(APPROOT.'/sources/Application/BulkExport/BulkExportResultGC.php');
require_once(APPROOT.'/application/startup.inc.php');