diff --git a/core/attributedef.class.inc.php b/core/attributedefrequires.class.inc.php
similarity index 100%
rename from core/attributedef.class.inc.php
rename to core/attributedefrequires.class.inc.php
diff --git a/sources/Core/AttributeDefinition/AttributeApplicationLanguage.php b/sources/Core/AttributeDefinition/AttributeApplicationLanguage.php
new file mode 100644
index 000000000..b5103b12d
--- /dev/null
+++ b/sources/Core/AttributeDefinition/AttributeApplicationLanguage.php
@@ -0,0 +1,13835 @@
+aCSSClasses;
+ }
+
+ /**
+ * Return the search widget type corresponding to this attribute
+ *
+ * @return string
+ */
+ public function GetSearchType()
+ {
+ return static::SEARCH_WIDGET_TYPE;
+ }
+
+ /**
+ * @return bool
+ */
+ public function IsSearchable()
+ {
+ return $this->GetSearchType() != static::SEARCH_WIDGET_TYPE_RAW;
+ }
+
+ /** @var string */
+ protected $m_sCode;
+ /** @var array */
+ protected $m_aParams;
+ /** @var string */
+ protected $m_sHostClass = '!undefined!';
+
+ public function Get($sParamName)
+ {
+ return $this->m_aParams[$sParamName];
+ }
+
+ public function GetIndexLength()
+ {
+ $iMaxLength = $this->GetMaxSize();
+ if (is_null($iMaxLength))
+ {
+ return null;
+ }
+ if ($iMaxLength > static::INDEX_LENGTH)
+ {
+ return static::INDEX_LENGTH;
+ }
+
+ return $iMaxLength;
+ }
+
+ public function IsParam($sParamName)
+ {
+ return (array_key_exists($sParamName, $this->m_aParams));
+ }
+
+ protected function GetOptional($sParamName, $default)
+ {
+ if (array_key_exists($sParamName, $this->m_aParams))
+ {
+ return $this->m_aParams[$sParamName];
+ }
+ else
+ {
+ return $default;
+ }
+ }
+
+ /**
+ * AttributeDefinition constructor.
+ *
+ * @param string $sCode
+ * @param array $aParams
+ *
+ * @throws \Exception
+ */
+ public function __construct($sCode, $aParams)
+ {
+ $this->m_sCode = $sCode;
+ $this->m_aParams = $aParams;
+ $this->ConsistencyCheck();
+ $this->aCSSClasses = array('attribute');
+ }
+
+ public function GetParams()
+ {
+ return $this->m_aParams;
+ }
+
+ public function HasParam($sParam)
+ {
+ return array_key_exists($sParam, $this->m_aParams);
+ }
+
+ public function SetHostClass($sHostClass)
+ {
+ $this->m_sHostClass = $sHostClass;
+ }
+
+ public function GetHostClass()
+ {
+ return $this->m_sHostClass;
+ }
+
+ /**
+ * @return array
+ *
+ * @throws \CoreException
+ */
+ public function ListSubItems()
+ {
+ $aSubItems = array();
+ foreach(MetaModel::ListAttributeDefs($this->m_sHostClass) as $sAttCode => $oAttDef)
+ {
+ if ($oAttDef instanceof AttributeSubItem)
+ {
+ if ($oAttDef->Get('target_attcode') == $this->m_sCode)
+ {
+ $aSubItems[$sAttCode] = $oAttDef;
+ }
+ }
+ }
+
+ return $aSubItems;
+ }
+
+ // Note: I could factorize this code with the parameter management made for the AttributeDef class
+ // to be overloaded
+ public static function ListExpectedParams()
+ {
+ return array();
+ }
+
+ /**
+ * @throws \Exception
+ */
+ protected function ConsistencyCheck()
+ {
+ // Check that any mandatory param has been specified
+ //
+ $aExpectedParams = static::ListExpectedParams();
+ foreach($aExpectedParams as $sParamName)
+ {
+ if (!array_key_exists($sParamName, $this->m_aParams))
+ {
+ $aBacktrace = debug_backtrace();
+ $sTargetClass = $aBacktrace[2]["class"];
+ $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"];
+ throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)");
+ }
+ }
+ }
+
+ /**
+ * Check the validity of the given value
+ *
+ * @param \DBObject $oHostObject
+ * @param $value Object error if any, null otherwise
+ *
+ * @return bool|string true for no errors, false or error message otherwise
+ */
+ public function CheckValue(DBObject $oHostObject, $value)
+ {
+ // later: factorize here the cases implemented into DBObject
+ return true;
+ }
+
+ // table, key field, name field
+
+ /**
+ * @return string
+ * @deprecated never used
+ */
+ public function ListDBJoins()
+ {
+ DeprecatedCallsLog::NotifyDeprecatedPhpMethod();
+
+ return "";
+ // e.g: return array("Site", "infrid", "name");
+ }
+
+ public function GetFinalAttDef()
+ {
+ return $this;
+ }
+
+ /**
+ * Deprecated - use IsBasedOnDBColumns instead
+ *
+ * @return bool
+ */
+ public function IsDirectField()
+ {
+ return static::IsBasedOnDBColumns();
+ }
+
+ /**
+ * Returns true if the attribute value is built after DB columns
+ *
+ * @return bool
+ */
+ 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 false;
+ }
+
+ /**
+ * Returns true if the attribute value can be shown as a string
+ *
+ * @return bool
+ */
+ public static function IsScalar()
+ {
+ return false;
+ }
+
+ /**
+ * Returns true if the attribute can be used in bulk modify.
+ *
+ * @return bool
+ * @since 3.1.0 N°3190
+ *
+ */
+ public static function IsBulkModifyCompatible(): bool
+ {
+ return static::IsScalar();
+ }
+
+ /**
+ * Returns true if the attribute value is a set of related objects (1-N or N-N)
+ *
+ * @return bool
+ */
+ public static function IsLinkSet()
+ {
+ return false;
+ }
+
+ /**
+ * @param int $iType
+ *
+ * @return bool true if the attribute is an external key, either directly (RELATIVE to the host class), or
+ * indirectly (ABSOLUTELY)
+ */
+ public function IsExternalKey($iType = EXTKEY_RELATIVE)
+ {
+ return false;
+ }
+
+ /**
+ * @return bool true if the attribute value is an external key, pointing to the host class
+ */
+ public static function IsHierarchicalKey()
+ {
+ return false;
+ }
+
+ /**
+ * @return bool true if the attribute value is stored on an object pointed to be an external key
+ */
+ public static function IsExternalField()
+ {
+ return false;
+ }
+
+ /**
+ * @return bool true if the attribute can be written (by essence : metamodel field option)
+ * @see \DBObject::IsAttributeReadOnlyForCurrentState() for a specific object instance (depending on its workflow)
+ */
+ public function IsWritable()
+ {
+ return false;
+ }
+
+ /**
+ * @return bool true if the attribute has been added automatically by the framework
+ */
+ public function IsMagic()
+ {
+ return $this->GetOptional('magic', false);
+ }
+
+ /**
+ * @return bool true if the attribute value is kept in the loaded object (in memory)
+ */
+ public static function LoadInObject()
+ {
+ return true;
+ }
+
+ /**
+ * @return bool true if the attribute value comes from the database in one way or another
+ */
+ public static function LoadFromClassTables()
+ {
+ return true;
+ }
+
+ /**
+ * Write attribute values outside the current class tables
+ *
+ * @param \DBObject $oHostObject
+ *
+ * @return void
+ * @since 3.1.0 Method creation, to offer a generic method for all attributes - before we were calling directly \AttributeCustomFields::WriteValue
+ *
+ * @used-by \DBObject::WriteExternalAttributes()
+ */
+ public function WriteExternalValues(DBObject $oHostObject): void
+ {
+ }
+
+ /**
+ * Read the data from where it has been stored (outside the current class tables).
+ * This verb must be implemented as soon as LoadFromClassTables returns false and LoadInObject returns true
+ *
+ * @param DBObject $oHostObject
+ *
+ * @return mixed|null
+ * @since 3.1.0
+ */
+ public function ReadExternalValues(DBObject $oHostObject)
+ {
+ return null;
+ }
+
+ /**
+ * Cleanup data upon object deletion (outside the current class tables)
+ * object id still available here
+ *
+ * @param \DBObject $oHostObject
+ *
+ * @since 3.1.0
+ */
+ public function DeleteExternalValues(DBObject $oHostObject): void
+ {
+ }
+
+ /**
+ * @return bool true if the attribute should be loaded anytime (in addition to the column selected by the user)
+ */
+ public function AlwaysLoadInTables()
+ {
+ return $this->GetOptional('always_load_in_tables', false);
+ }
+
+ /**
+ * @param \DBObject $oHostObject
+ *
+ * @return mixed Must return the value if LoadInObject returns false
+ */
+ public function GetValue($oHostObject)
+ {
+ return null;
+ }
+
+ /**
+ * Returns true if the attribute must not be stored if its current value is "null" (Cf. IsNull())
+ *
+ * @return bool
+ */
+ public function IsNullAllowed()
+ {
+ return true;
+ }
+
+ /**
+ * Returns the attribute code (identifies the attribute in the host class)
+ *
+ * @return string
+ */
+ public function GetCode()
+ {
+ return $this->m_sCode;
+ }
+
+ /**
+ * Find the corresponding "link" attribute on the target class, if any
+ *
+ * @return null | AttributeDefinition
+ */
+ public function GetMirrorLinkAttribute()
+ {
+ return null;
+ }
+
+ /**
+ * Helper to browse the hierarchy of classes, searching for a label
+ *
+ * @param string $sDictEntrySuffix
+ * @param string $sDefault
+ * @param bool $bUserLanguageOnly
+ *
+ * @return string
+ * @throws \Exception
+ */
+ protected function SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly)
+ {
+ $sLabel = Dict::S('Class:'.$this->m_sHostClass.$sDictEntrySuffix, '', $bUserLanguageOnly);
+ if (strlen($sLabel) == 0)
+ {
+ // Nothing found: go higher in the hierarchy (if possible)
+ //
+ $sLabel = $sDefault;
+ $sParentClass = MetaModel::GetParentClass($this->m_sHostClass);
+ if ($sParentClass)
+ {
+ if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode))
+ {
+ $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode);
+ $sLabel = $oAttDef->SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly);
+ }
+ }
+ }
+
+ return $sLabel;
+ }
+
+ /**
+ * @param string|null $sDefault if null, will return the attribute code replacing "_" by " "
+ *
+ * @return string
+ *
+ * @throws \Exception
+ */
+ public function GetLabel($sDefault = null)
+ {
+ $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, 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);
+ }
+ // Browse the hierarchy again, accepting default (english) translations
+ $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, $sDefault, false);
+ }
+
+ return $sLabel;
+ }
+
+ /**
+ * To be overloaded for localized enums
+ *
+ * @param string $sValue
+ *
+ * @return string label corresponding to the given value (in plain text)
+ */
+ public function GetValueLabel($sValue)
+ {
+ return $sValue;
+ }
+
+ /**
+ * 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
+ */
+ public function MakeValueFromString(
+ $sProposedValue,
+ $bLocalizedValue = false,
+ $sSepItem = null,
+ $sSepAttribute = null,
+ $sSepValue = null,
+ $sAttributeQualifier = null
+ ) {
+ return $this->MakeRealValue($sProposedValue, null);
+ }
+
+ /**
+ * Parses a search string coming from user input
+ *
+ * @param string $sSearchString
+ *
+ * @return string
+ */
+ public function ParseSearchString($sSearchString)
+ {
+ return $sSearchString;
+ }
+
+ /**
+ * @return string
+ *
+ * @throws \Exception
+ */
+ public function GetLabel_Obsolete()
+ {
+ // Written for compatibility with a data model written prior to version 0.9.1
+ if (array_key_exists('label', $this->m_aParams))
+ {
+ return $this->m_aParams['label'];
+ }
+ else
+ {
+ return $this->GetLabel();
+ }
+ }
+
+ /**
+ * @param string|null $sDefault
+ *
+ * @return string
+ *
+ * @throws \Exception
+ */
+ public function GetDescription($sDefault = null)
+ {
+ $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', 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 = '';
+ }
+ // Browse the hierarchy again, accepting default (english) translations
+ $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', $sDefault, false);
+ }
+
+ return $sLabel;
+ }
+
+ /**
+ * @return bool True if the attribute has a description {@see \AttributeDefinition::GetDescription()}
+ * @throws \Exception
+ * @since 3.1.0
+ */
+ public function HasDescription(): bool
+ {
+ return utils::IsNotNullOrEmptyString($this->GetDescription());
+ }
+
+ /**
+ * @param string|null $sDefault
+ *
+ * @return string
+ *
+ * @throws \Exception
+ */
+ public function GetHelpOnEdition($sDefault = null)
+ {
+ $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', 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 = '';
+ }
+ // Browse the hierarchy again, accepting default (english) translations
+ $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', $sDefault, false);
+ }
+
+ return $sLabel;
+ }
+
+ public function GetHelpOnSmartSearch()
+ {
+ $aParents = array_merge(array(get_class($this) => get_class($this)), class_parents($this));
+ foreach($aParents as $sClass)
+ {
+ $sHelp = Dict::S("Core:$sClass?SmartSearch", '-missing-');
+ if ($sHelp != '-missing-')
+ {
+ return $sHelp;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * @return string
+ *
+ * @throws \Exception
+ */
+ public function GetDescription_Obsolete()
+ {
+ // Written for compatibility with a data model written prior to version 0.9.1
+ if (array_key_exists('description', $this->m_aParams))
+ {
+ return $this->m_aParams['description'];
+ }
+ else
+ {
+ return $this->GetDescription();
+ }
+ }
+
+ public function GetTrackingLevel()
+ {
+ return $this->GetOptional('tracking_level', ATTRIBUTE_TRACKING_ALL);
+ }
+
+ /**
+ * @return \ValueSetObjects
+ */
+ public function GetValuesDef()
+ {
+ return null;
+ }
+
+ public function GetPrerequisiteAttributes($sClass = null)
+ {
+ return array();
+ }
+
+ public function GetNullValue()
+ {
+ return null;
+ }
+
+ public function IsNull($proposedValue)
+ {
+ return is_null($proposedValue);
+ }
+
+ /**
+ * @param mixed $proposedValue
+ *
+ * @return bool True if $proposedValue is an actual value set in the attribute, false is the attribute remains "empty"
+ * @since 3.0.3, 3.1.0 N°5784
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ // Default implementation, we don't really know what type $proposedValue will be
+ return !(is_null($proposedValue));
+ }
+
+ /**
+ * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!
+ *
+ * @param mixed $proposedValue
+ * @param \DBObject $oHostObj
+ *
+ * @return mixed
+ */
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ return $proposedValue;
+ }
+
+ public function Equals($val1, $val2)
+ {
+ return ($val1 == $val2);
+ }
+
+ /**
+ * @param string $sPrefix
+ *
+ * @return array suffix/expression pairs (1 in most of the cases), for READING (Select)
+ */
+ public function GetSQLExpressions($sPrefix = '')
+ {
+ return array();
+ }
+
+ /**
+ * @param array $aCols
+ * @param string $sPrefix
+ *
+ * @return mixed a value out of suffix/value pairs, for SELECT result interpretation
+ */
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ return null;
+ }
+
+ /**
+ * @param bool $bFullSpec
+ *
+ * @return array column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation)
+ * @see \CMDBSource::GetFieldSpec()
+ */
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ return array();
+ }
+
+ /**
+ * @param $value
+ *
+ * @return array column/value pairs (1 in most of the cases), for WRITING (Insert, Update)
+ */
+ public function GetSQLValues($value)
+ {
+ return array();
+ }
+
+ public function RequiresIndex()
+ {
+ return false;
+ }
+
+ public function RequiresFullTextIndex()
+ {
+ return false;
+ }
+
+ public function CopyOnAllTables()
+ {
+ return false;
+ }
+
+ public function GetOrderBySQLExpressions($sClassAlias)
+ {
+ // Note: This is the responsibility of this function to place backticks around column aliases
+ return array('`'.$sClassAlias.$this->GetCode().'`');
+ }
+
+ public function GetOrderByHint()
+ {
+ return '';
+ }
+
+ // Import - differs slightly from SQL input, but identical in most cases
+ //
+ public function GetImportColumns()
+ {
+ return $this->GetSQLColumns();
+ }
+
+ public function FromImportToValue($aCols, $sPrefix = '')
+ {
+ $aValues = array();
+ foreach($this->GetSQLExpressions($sPrefix) as $sAlias => $sExpr)
+ {
+ // This is working, based on the assumption that importable fields
+ // are not computed fields => the expression is the name of a column
+ $aValues[$sPrefix.$sAlias] = $aCols[$sExpr];
+ }
+
+ return $this->FromSQLToValue($aValues, $sPrefix);
+ }
+
+ public function GetValidationPattern()
+ {
+ return '';
+ }
+
+ public function CheckFormat($value)
+ {
+ return true;
+ }
+
+ public function GetMaxSize()
+ {
+ return null;
+ }
+
+ /**
+ * @return mixed|null
+ * @deprecated never used
+ */
+ public function MakeValue()
+ {
+ DeprecatedCallsLog::NotifyDeprecatedPhpMethod();
+ $sComputeFunc = $this->Get("compute_func");
+ if (empty($sComputeFunc)) {
+ return null;
+ }
+
+ return call_user_func($sComputeFunc);
+ }
+
+ abstract public function GetDefaultValue(DBObject $oHostObject = null);
+
+ //
+ // To be overloaded in subclasses
+ //
+
+ abstract public function GetBasicFilterOperators(); // returns an array of "opCode"=>"description"
+
+ abstract public function GetBasicFilterLooseOperator(); // returns an "opCode"
+
+ //abstract protected GetBasicFilterHTMLInput();
+ abstract public function GetBasicFilterSQLExpr($sOpCode, $value);
+
+ public function GetMagicFields()
+ {
+ return [];
+ }
+
+ public function GetEditValue($sValue, $oHostObj = null)
+ {
+ return (string)$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)
+ {
+ return (string)$this->GetEditValue($sValue, $oHostObj);
+ }
+
+ /**
+ * Helper to get a value that will be JSON encoded
+ *
+ * @see FromJSONToValue for the reverse operation
+ *
+ * @param mixed $value field value
+ *
+ * @return string|array PHP struct that can be properly encoded
+ *
+ */
+ public function GetForJSON($value)
+ {
+ // In most of the cases, that will be the expected behavior...
+ return $this->GetEditValue($value);
+ }
+
+ /**
+ * Helper to form a value, given JSON decoded data. This way the attribute itself handles the transformation from the JSON structure to the expected data (the one that
+ * needs to be used in the {@see \DBObject::Set()} method).
+ *
+ * Note that for CSV and XML this isn't done yet (no delegation to the attribute but switch/case inside controllers) :/
+ *
+ * @see GetForJSON for the reverse operation
+ *
+ * @param string $json JSON encoded value
+ *
+ * @return mixed JSON decoded data, depending on the attribute type
+ *
+ */
+ public function FromJSONToValue($json)
+ {
+ // Pass-through in most of the cases
+ return $json;
+ }
+
+ /**
+ * Override to display the value in the GUI
+ *
+ * @param string $sValue
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string
+ */
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ return Str::pure2html((string)$sValue);
+ }
+
+ /**
+ * Override to export the value in XML
+ *
+ * @param string $sValue
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return mixed
+ */
+ public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ return Str::pure2xml((string)$sValue);
+ }
+
+ /**
+ * Override to escape the value when read by DBObject::GetAsCSV()
+ *
+ * @param string $sValue
+ * @param string $sSeparator
+ * @param string $sTextQualifier
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ * @param bool $bConvertToPlainText
+ *
+ * @return string
+ */
+ public function GetAsCSV(
+ $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ ) {
+ return (string)$sValue;
+ }
+
+ /**
+ * Override to differentiate a value displayed in the UI or in the history
+ *
+ * @param string $sValue
+ * @param \DBObject $oHostObject
+ * @param bool $bLocalize
+ *
+ * @return string
+ */
+ public function GetAsHTMLForHistory($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ return $this->GetAsHTML($sValue, $oHostObject, $bLocalize);
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\StringField';
+ }
+
+ /**
+ * Override to specify Field class
+ *
+ * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the
+ * $oFormField is passed, MakeFormField behave more like a Prepare.
+ *
+ * @param \DBObject $oObject
+ * @param \Combodo\iTop\Form\Field\Field|null $oFormField
+ *
+ * @return \Combodo\iTop\Form\Field\Field
+ * @throws \CoreException
+ * @throws \Exception
+ *
+ * @noinspection PhpMissingReturnTypeInspection
+ * @noinspection PhpMissingParamTypeInspection
+ * @noinspection ReturnTypeCanBeDeclaredInspection
+ */
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ // This is a fallback in case the AttributeDefinition subclass has no overloading of this function.
+ if ($oFormField === null) {
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ //$oFormField->SetReadOnly(true);
+ }
+
+ $oFormField->SetLabel($this->GetLabel());
+
+ // Attributes flags
+ // - Retrieving flags for the current object
+ if ($oObject->IsNew()) {
+ $iFlags = $oObject->GetInitialStateAttributeFlags($this->GetCode());
+ } else {
+ $iFlags = $oObject->GetAttributeFlags($this->GetCode());
+ }
+
+ // - Comparing flags
+ if ($this->IsWritable() && (!$this->IsNullAllowed() || (($iFlags & OPT_ATT_MANDATORY) === OPT_ATT_MANDATORY))) {
+ $oFormField->SetMandatory(true);
+ }
+ if ((!$oObject->IsNew() || !$oFormField->GetMandatory()) && (($iFlags & OPT_ATT_READONLY) === OPT_ATT_READONLY)) {
+ $oFormField->SetReadOnly(true);
+ }
+
+ // CurrentValue
+ $oFormField->SetCurrentValue($oObject->Get($this->GetCode()));
+
+ // Validation pattern
+ if ($this->GetValidationPattern() !== '') {
+ $oFormField->AddValidator(new CustomRegexpValidator($this->GetValidationPattern()));
+ }
+
+ // Description
+ $sAttDescription = $this->GetDescription();
+ if (!empty($sAttDescription)) {
+ $oFormField->SetDescription($this->GetDescription());
+ }
+
+ // Metadata
+ $oFormField->AddMetadata('attribute-code', $this->GetCode());
+ $oFormField->AddMetadata('attribute-type', get_class($this));
+ $oFormField->AddMetadata('attribute-label', $this->GetLabel());
+ // - Attribute flags
+ $aPossibleAttFlags = MetaModel::EnumPossibleAttributeFlags();
+ foreach ($aPossibleAttFlags as $sFlagCode => $iFlagValue) {
+ // Note: Skip normal flag as we don't need it.
+ if ($sFlagCode === 'normal') {
+ continue;
+ }
+ $sFormattedFlagCode = str_ireplace('_', '-', $sFlagCode);
+ $sFormattedFlagValue = (($iFlags & $iFlagValue) === $iFlagValue) ? 'true' : 'false';
+ $oFormField->AddMetadata('attribute-flag-'.$sFormattedFlagCode, $sFormattedFlagValue);
+ }
+ // - Value raw
+ if ($this::IsScalar()) {
+ $oFormField->AddMetadata('value-raw', (string)$oObject->Get($this->GetCode()));
+ }
+
+ // We don't want to invalidate field because of old untouched values that are no longer valid
+ $aModifiedAttCodes = $oObject->ListChanges();
+ $bAttributeHasBeenModified = array_key_exists($this->GetCode(), $aModifiedAttCodes);
+ if (false === $bAttributeHasBeenModified) {
+ $oFormField->SetValidationDisabled(true);
+ }
+
+ return $oFormField;
+ }
+
+ /**
+ * List the available verbs for 'GetForTemplate'
+ */
+ public function EnumTemplateVerbs()
+ {
+ return array(
+ '' => 'Plain text (unlocalized) representation',
+ 'html' => 'HTML representation',
+ 'label' => 'Localized representation',
+ 'text' => 'Plain text representation (without any markup)',
+ );
+ }
+
+ /**
+ * 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
+ * @param bool $bLocalize Whether or not to localize the value
+ *
+ * @return mixed|null|string
+ *
+ * @throws \Exception
+ */
+ public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
+ {
+ if ($this->IsScalar())
+ {
+ switch ($sVerb)
+ {
+ case '':
+ return $value;
+
+ case 'html':
+ return $this->GetAsHtml($value, $oHostObject, $bLocalize);
+
+ case 'label':
+ return $this->GetEditValue($value);
+
+ case 'text':
+ return $this->GetAsPlainText($value);
+ break;
+
+ default:
+ throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param array $aArgs
+ * @param string $sContains
+ *
+ * @return array|null
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ $oValSetDef = $this->GetValuesDef();
+ if (!$oValSetDef)
+ {
+ return null;
+ }
+
+ return $oValSetDef->GetValues($aArgs, $sContains);
+ }
+
+ /**
+ * GetAllowedValuesForSelect is the same as GetAllowedValues except for field with obsolescence flag
+ * @param array $aArgs
+ * @param string $sContains
+ *
+ * @return array|null
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function GetAllowedValuesForSelect($aArgs = array(), $sContains = '')
+ {
+ return $this->GetAllowedValues($aArgs, $sContains);
+ }
+
+ /**
+ * Explain the change of the attribute (history)
+ *
+ * @param string $sOldValue
+ * @param string $sNewValue
+ * @param string $sLabel
+ *
+ * @return string
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ * @throws \OQLException
+ * @throws \Exception
+ */
+ public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null)
+ {
+ if (is_null($sLabel))
+ {
+ $sLabel = $this->GetLabel();
+ }
+
+ $sNewValueHtml = $this->GetAsHTMLForHistory($sNewValue);
+ $sOldValueHtml = $this->GetAsHTMLForHistory($sOldValue);
+
+ if ($this->IsExternalKey())
+ {
+ /** @var \AttributeExternalKey $this */
+ $sTargetClass = $this->GetTargetClass();
+ $sOldValueHtml = (int)$sOldValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sOldValue) : null;
+ $sNewValueHtml = (int)$sNewValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sNewValue) : null;
+ }
+ if ((($this->GetType() == 'String') || ($this->GetType() == 'Text')) &&
+ (strlen($sNewValue) > strlen($sOldValue)))
+ {
+ // Check if some text was not appended to the field
+ if (substr($sNewValue, 0, strlen($sOldValue)) == $sOldValue) // Text added at the end
+ {
+ $sDelta = $this->GetAsHTML(substr($sNewValue, strlen($sOldValue)));
+ $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel);
+ }
+ else
+ {
+ if (substr($sNewValue, -strlen($sOldValue)) == $sOldValue) // Text added at the beginning
+ {
+ $sDelta = $this->GetAsHTML(substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue)));
+ $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel);
+ }
+ else
+ {
+ if (strlen($sOldValue) == 0)
+ {
+ $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml);
+ }
+ else
+ {
+ if (is_null($sNewValue))
+ {
+ $sNewValueHtml = Dict::S('UI:UndefinedObject');
+ }
+ $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel,
+ $sNewValueHtml, $sOldValueHtml);
+ }
+ }
+ }
+ }
+ else
+ {
+ if (strlen($sOldValue) == 0)
+ {
+ $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml);
+ }
+ else
+ {
+ if (is_null($sNewValue))
+ {
+ $sNewValueHtml = Dict::S('UI:UndefinedObject');
+ }
+ $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml,
+ $sOldValueHtml);
+ }
+ }
+
+ return $sResult;
+ }
+
+ /**
+ * @param \DBObject $oObject
+ * @param mixed $original
+ * @param mixed $value
+ *
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException if cannot create object
+ * @throws \CoreUnexpectedValue
+ * @throws \CoreWarning
+ * @throws \MySQLException
+ * @throws \OQLException
+ *
+ * @uses GetChangeRecordAdditionalData
+ * @uses GetChangeRecordClassName
+ *
+ * @since 3.1.0 N°6042
+ */
+ public function RecordAttChange(DBObject $oObject, $original, $value): void
+ {
+ /** @var CMDBChangeOp $oMyChangeOp */
+ $oMyChangeOp = MetaModel::NewObject($this->GetChangeRecordClassName());
+ $oMyChangeOp->Set("objclass", get_class($oObject));
+ $oMyChangeOp->Set("objkey", $oObject->GetKey());
+ $oMyChangeOp->Set("attcode", $this->GetCode());
+
+ $this->GetChangeRecordAdditionalData($oMyChangeOp, $oObject, $original, $value);
+
+ $oMyChangeOp->DBInsertNoReload();
+ }
+
+ /**
+ * Add attribute specific information in the {@link \CMDBChangeOp} instance
+ *
+ * @param \CMDBChangeOp $oMyChangeOp
+ * @param \DBObject $oObject
+ * @param $original
+ * @param $value
+ *
+ * @return void
+ * @used-by RecordAttChange
+ */
+ protected function GetChangeRecordAdditionalData(CMDBChangeOp $oMyChangeOp, DBObject $oObject, $original, $value): void
+ {
+ $oMyChangeOp->Set("oldvalue", $original);
+ $oMyChangeOp->Set("newvalue", $value);
+ }
+
+ /**
+ * @return string name of the children of {@link \CMDBChangeOp} class to use for the history record
+ * @used-by RecordAttChange
+ */
+ protected function GetChangeRecordClassName(): string
+ {
+ return CMDBChangeOpSetAttributeScalar::class;
+ }
+
+ /**
+ * Parses a string to find some smart search patterns and build the corresponding search/OQL condition
+ * Each derived class is reponsible for defining and processing their own smart patterns, the base class
+ * does nothing special, and just calls the default (loose) operator
+ *
+ * @param string $sSearchText The search string to analyze for smart patterns
+ * @param \FieldExpression $oField
+ * @param array $aParams Values of the query parameters
+ *
+ * @return \Expression The search condition to be added (AND) to the current search
+ *
+ * @throws \CoreException
+ */
+ public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams)
+ {
+ $sParamName = $oField->GetParent().'_'.$oField->GetName();
+ $oRightExpr = new VariableExpression($sParamName);
+ $sOperator = $this->GetBasicFilterLooseOperator();
+ switch ($sOperator)
+ {
+ case 'Contains':
+ $aParams[$sParamName] = "%$sSearchText%";
+ $sSQLOperator = 'LIKE';
+ break;
+
+ default:
+ $sSQLOperator = $sOperator;
+ $aParams[$sParamName] = $sSearchText;
+ }
+ $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr);
+
+ return $oNewCondition;
+ }
+
+ /**
+ * Tells if an attribute is part of the unique fingerprint of the object (used for comparing two objects)
+ * All attributes which value is not based on a value from the object itself (like ExternalFields or LinkedSet)
+ * must be excluded from the object's signature
+ *
+ * @return boolean
+ */
+ public function IsPartOfFingerprint()
+ {
+ return true;
+ }
+
+ /**
+ * 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)
+ {
+ return (string)$value;
+ }
+
+ /*
+ * return string
+ */
+ public function GetRenderForDataTable(string $sClassAlias) :string
+ {
+ $sRenderFunction = "return data;";
+ return $sRenderFunction;
+ }
+}
+
+class AttributeDashboard extends AttributeDefinition
+{
+ /**
+ * 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 static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(),
+ array("definition_file", "is_user_editable"));
+ }
+
+ public function GetDashboard()
+ {
+ $sAttCode = $this->GetCode();
+ $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode);
+ $sFilePath = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$this->Get('definition_file');
+ return RuntimeDashboard::GetDashboard($sFilePath, $sClass.'__'.$sAttCode);
+ }
+
+ public function IsUserEditable()
+ {
+ return $this->Get('is_user_editable');
+ }
+
+ public function IsWritable()
+ {
+ return false;
+ }
+
+ public function GetEditClass()
+ {
+ return "";
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return null;
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array();
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return '=';
+ }
+
+ public function GetBasicFilterSQLExpr($sOpCode, $value)
+ {
+ return '';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ return null;
+ }
+
+ // if this verb returns false, then GetValue must be implemented
+ public static function LoadInObject()
+ {
+ return false;
+ }
+
+ public function GetValue($oHostObject)
+ {
+ return '';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function HasAValue($proposedValue): bool
+ {
+ // Always return false for now, we don't consider a custom version of a dashboard
+ return false;
+ }
+}
+
+/**
+ * Set of objects directly linked to an object, and being part of its definition
+ *
+ * @package iTopORM
+ */
+class AttributeLinkedSet extends AttributeDefinition
+{
+ /**
+ * 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);
+ $this->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 '.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 BlockLinkSetDisplayAsProperty($this->GetCode(), $this, $sValue);
+
+ return 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 '
';
+
+ 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;
+ }
+ }
+}
+
+/**
+ * Set of objects linked to an object (n-n), and being part of its definition
+ *
+ * @package iTopORM
+ */
+class AttributeLinkedSetIndirect extends AttributeLinkedSet
+{
+ /**
+ * 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 static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array("ext_key_to_remote"));
+ }
+
+ public function IsIndirect()
+ {
+ return true;
+ }
+
+ public function GetExtKeyToRemote()
+ {
+ return $this->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;
+ }
+
+}
+
+/**
+ * Abstract class implementing default filters for a DB column
+ *
+ * @package iTopORM
+ */
+class AttributeDBFieldVoid extends AttributeDefinition
+{
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "sql"));
+ }
+
+ // To be overriden, used in GetSQLColumns
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return 'VARCHAR(255)'
+ .CMDBSource::GetSqlStringColumnDefinition()
+ .($bFullSpec ? $this->GetSQLColSpec() : '');
+ }
+
+ protected function GetSQLColSpec()
+ {
+ $default = $this->ScalarToSQL($this->GetDefaultValue());
+ if (is_null($default))
+ {
+ $sRet = '';
+ }
+ else
+ {
+ if (is_numeric($default))
+ {
+ // Though it is a string in PHP, it will be considered as a numeric value in MySQL
+ // Then it must not be quoted here, to preserve the compatibility with the value returned by CMDBSource::GetFieldSpec
+ $sRet = " DEFAULT $default";
+ }
+ else
+ {
+ $sRet = " DEFAULT ".CMDBSource::Quote($default);
+ }
+ }
+
+ return $sRet;
+ }
+
+ public function GetEditClass()
+ {
+ return "String";
+ }
+
+ public function GetValuesDef()
+ {
+ return $this->Get("allowed_values");
+ }
+
+ public function GetPrerequisiteAttributes($sClass = null)
+ {
+ return $this->Get("depends_on");
+ }
+
+ public static function IsBasedOnDBColumns()
+ {
+ return true;
+ }
+
+ public static function IsScalar()
+ {
+ return true;
+ }
+
+ public function IsWritable()
+ {
+ return !$this->IsMagic();
+ }
+
+ public function GetSQLExpr()
+ {
+ return $this->Get("sql");
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return $this->MakeRealValue("", $oHostObject);
+ }
+
+ public function IsNullAllowed()
+ {
+ return false;
+ }
+
+ //
+ protected function ScalarToSQL($value)
+ {
+ return $value;
+ } // format value as a valuable SQL literal (quoted outside)
+
+ public function GetSQLExpressions($sPrefix = '')
+ {
+ $aColumns = array();
+ // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
+ $aColumns[''] = $this->Get("sql");
+
+ return $aColumns;
+ }
+
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ $value = $this->MakeRealValue($aCols[$sPrefix.''], null);
+
+ return $value;
+ }
+
+ public function GetSQLValues($value)
+ {
+ $aValues = array();
+ $aValues[$this->Get("sql")] = $this->ScalarToSQL($value);
+
+ return $aValues;
+ }
+
+ public function GetSQLColumns($bFullSpec = false)
+ {
+ $aColumns = array();
+ $aColumns[$this->Get("sql")] = $this->GetSQLCol($bFullSpec);
+
+ return $aColumns;
+ }
+
+ public function GetBasicFilterOperators()
+ {
+ return array("=" => "equals", "!=" => "differs from");
+ }
+
+ 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";
+ }
+ }
+}
+
+/**
+ * Base class for all kind of DB attributes, with the exception of external keys
+ *
+ * @package iTopORM
+ */
+class AttributeDBField extends AttributeDBFieldVoid
+{
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed"));
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return $this->MakeRealValue($this->Get("default_value"), $oHostObject);
+ }
+
+ public function IsNullAllowed()
+ {
+ return $this->Get("is_null_allowed");
+ }
+}
+
+/**
+ * Map an integer column to an attribute
+ *
+ * @package iTopORM
+ */
+class AttributeInteger extends AttributeDBField
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
+
+ /**
+ * 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 static function ListExpectedParams()
+ {
+ return parent::ListExpectedParams();
+ //return array_merge(parent::ListExpectedParams(), array());
+ }
+
+ public function GetEditClass()
+ {
+ return "String";
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "INT(11)".($bFullSpec ? $this->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
+ }
+}
+
+/**
+ * An external key for which the class is defined as the value of another attribute
+ *
+ * @package iTopORM
+ */
+class AttributeObjectKey extends AttributeDBFieldVoid
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY;
+
+ /**
+ * 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 static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array('class_attcode', 'is_null_allowed'));
+ }
+
+ public function GetEditClass()
+ {
+ return "String";
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ return 0;
+ }
+
+ public function IsNullAllowed()
+ {
+ 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);
+ }
+
+ 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;
+ }
+}
+
+/**
+ * Display an integer between 0 and 100 as a percentage / horizontal bar graph
+ *
+ * @package iTopORM
+ */
+class AttributePercentage extends AttributeInteger
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
+
+ /**
+ * 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 GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ $iWidth = 5; // Total width of the percentage bar graph, in em...
+ $iValue = (int)$sValue;
+ if ($iValue > 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 %";
+ }
+}
+
+/**
+ * Map a decimal value column (suitable for financial computations) to an attribute
+ * internally in PHP such numbers are represented as string. Should you want to perform
+ * a calculation on them, it is recommended to use the BC Math functions in order to
+ * retain the precision
+ *
+ * @package iTopORM
+ */
+class AttributeDecimal extends AttributeDBField
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
+
+ /**
+ * 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 static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */));
+ }
+
+ public function GetEditClass()
+ {
+ return "String";
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "DECIMAL(".$this->Get('digits').",".$this->Get('decimals').")".($bFullSpec ? $this->GetSQLColSpec() : '');
+ }
+
+ public function GetValidationPattern()
+ {
+ $iNbDigits = $this->Get('digits');
+ $iPrecision = $this->Get('decimals');
+ $iNbIntegerDigits = $iNbDigits - $iPrecision;
+
+ return "^[\-\+]?\d{1,$iNbIntegerDigits}(\.\d{0,$iPrecision})?$";
+ }
+
+ /**
+ * @inheritDoc
+ * @since 3.2.0
+ */
+ public function CheckFormat($value)
+ {
+ $sRegExp = $this->GetValidationPattern();
+ return preg_match("/$sRegExp/", $value);
+ }
+
+ 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;
+ }
+
+ return $this->ScalarToSQL($proposedValue);
+ }
+
+ public function ScalarToSQL($value)
+ {
+ assert(is_null($value) || preg_match('/'.$this->GetValidationPattern().'/', $value));
+
+ if (!is_null($value) && ($value !== ''))
+ {
+ $value = sprintf("%1.".$this->Get('decimals')."F", $value);
+ }
+ return $value; // null or string
+ }
+}
+
+/**
+ * Map a boolean column to an attribute
+ *
+ * @package iTopORM
+ */
+class AttributeBoolean extends AttributeInteger
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
+
+ /**
+ * 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 static function ListExpectedParams()
+ {
+ return parent::ListExpectedParams();
+ //return array_merge(parent::ListExpectedParams(), array());
+ }
+
+ public function GetEditClass()
+ {
+ return "Integer";
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "TINYINT(1)".($bFullSpec ? $this->GetSQLColSpec() : '');
+ }
+
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (is_null($proposedValue))
+ {
+ return null;
+ }
+ if ($proposedValue === '')
+ {
+ return null;
+ }
+ if ((int)$proposedValue)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function ScalarToSQL($value)
+ {
+ if ($value)
+ {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ public function GetValueLabel($bValue)
+ {
+ if (is_null($bValue))
+ {
+ $sLabel = Dict::S('Core:'.get_class($this).'/Value:null');
+ }
+ else
+ {
+ $sValue = $bValue ? 'yes' : 'no';
+ $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue);
+ $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, true /*user lang*/);
+ }
+
+ return $sLabel;
+ }
+
+ public function GetValueDescription($bValue)
+ {
+ if (is_null($bValue))
+ {
+ $sDescription = Dict::S('Core:'.get_class($this).'/Value:null+');
+ }
+ else
+ {
+ $sValue = $bValue ? 'yes' : 'no';
+ $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue.'+');
+ $sDescription = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue.'+', $sDefault,
+ true /*user lang*/);
+ }
+
+ return $sDescription;
+ }
+
+ public function GetAsHTML($bValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (is_null($bValue))
+ {
+ $sRes = '';
+ }
+ elseif ($bLocalize)
+ {
+ $sLabel = $this->GetValueLabel($bValue);
+ $sDescription = $this->GetValueDescription($bValue);
+ // later, we could imagine a detailed description in the title
+ $sRes = "".parent::GetAsHtml($sLabel)."";
+ }
+ else
+ {
+ $sRes = $bValue ? 'yes' : 'no';
+ }
+
+ return $sRes;
+ }
+
+ public function GetAsXML($bValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (is_null($bValue))
+ {
+ $sFinalValue = '';
+ }
+ elseif ($bLocalize)
+ {
+ $sFinalValue = $this->GetValueLabel($bValue);
+ }
+ else
+ {
+ $sFinalValue = $bValue ? 'yes' : 'no';
+ }
+ $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize);
+
+ return $sRes;
+ }
+
+ public function GetAsCSV(
+ $bValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
+ $bConvertToPlainText = false
+ ) {
+ if (is_null($bValue))
+ {
+ $sFinalValue = '';
+ }
+ elseif ($bLocalize)
+ {
+ $sFinalValue = $this->GetValueLabel($bValue);
+ }
+ else
+ {
+ $sFinalValue = $bValue ? 'yes' : 'no';
+ }
+ $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize);
+
+ return $sRes;
+ }
+
+ public static function GetFormFieldClass()
+ {
+ return '\\Combodo\\iTop\\Form\\Field\\SelectField';
+ }
+
+ /**
+ * @param \DBObject $oObject
+ * @param \Combodo\iTop\Form\Field\SelectField $oFormField
+ *
+ * @return \Combodo\iTop\Form\Field\SelectField
+ * @throws \CoreException
+ */
+ public function MakeFormField(DBObject $oObject, $oFormField = null)
+ {
+ if ($oFormField === null)
+ {
+ $sFormFieldClass = static::GetFormFieldClass();
+ $oFormField = new $sFormFieldClass($this->GetCode());
+ }
+
+ $oFormField->SetChoices(array('yes' => $this->GetValueLabel(true), 'no' => $this->GetValueLabel(false)));
+ parent::MakeFormField($oObject, $oFormField);
+
+ return $oFormField;
+ }
+
+ public function GetEditValue($value, $oHostObj = null)
+ {
+ if (is_null($value))
+ {
+ return '';
+ }
+ else
+ {
+ return $this->GetValueLabel($value);
+ }
+ }
+
+ public function GetForJSON($value)
+ {
+ return (bool)$value;
+ }
+
+ public function MakeValueFromString(
+ $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
+ $sAttributeQualifier = null
+ ) {
+ $sInput = mb_strtolower(trim($sProposedValue));
+ if ($bLocalizedValue)
+ {
+ switch ($sInput)
+ {
+ case '1': // backward compatibility
+ case $this->GetValueLabel(true):
+ $value = true;
+ break;
+ case '0': // backward compatibility
+ case 'no':
+ case $this->GetValueLabel(false):
+ $value = false;
+ break;
+ default:
+ $value = null;
+ }
+ }
+ else
+ {
+ switch ($sInput)
+ {
+ case '1': // backward compatibility
+ case 'yes':
+ $value = true;
+ break;
+ case '0': // backward compatibility
+ case 'no':
+ $value = false;
+ break;
+ default:
+ $value = null;
+ }
+ }
+
+ return $value;
+ }
+
+ public function RecordAttChange(DBObject $oObject, $original, $value): void
+ {
+ parent::RecordAttChange($oObject, $original ? 1 : 0, $value ? 1 : 0);
+ }
+
+ protected function GetChangeRecordClassName(): string
+ {
+ return CMDBChangeOpSetAttributeScalar::class;
+ }
+
+ public function GetAllowedValues($aArgs = array(), $sContains = '') : array
+ {
+ return [
+ 0 => $this->GetValueLabel(false),
+ 1 => $this->GetValueLabel(true)
+ ];
+ }
+
+ public function GetDisplayStyle()
+ {
+ return $this->GetOptional('display_style', 'select');
+ }
+}
+
+/**
+ * Map a varchar column (size < ?) to an attribute
+ *
+ * @package iTopORM
+ */
+class AttributeString extends AttributeDBField
+{
+ 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 static function ListExpectedParams()
+ {
+ return parent::ListExpectedParams();
+ //return array_merge(parent::ListExpectedParams(), array());
+ }
+
+ public function GetEditClass()
+ {
+ return "String";
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return 'VARCHAR(255)'
+ .CMDBSource::GetSqlStringColumnDefinition()
+ .($bFullSpec ? $this->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;
+ }
+
+}
+
+/**
+ * An attribute that matches an object class
+ *
+ * @package iTopORM
+ */
+class AttributeClass extends AttributeString
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM;
+
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array('class_category', 'more_values'));
+ }
+
+ public function __construct($sCode, $aParams)
+ {
+ $this->m_sCode = $sCode;
+ $aParams["allowed_values"] = new ValueSetEnumClasses($aParams['class_category'], $aParams['more_values']);
+ parent::__construct($sCode, $aParams);
+ }
+
+ public function GetDefaultValue(DBObject $oHostObject = null)
+ {
+ $sDefault = parent::GetDefaultValue($oHostObject);
+ if (!$this->IsNullAllowed() && $this->IsNull($sDefault))
+ {
+ // For this kind of attribute specifying null as default value
+ // is authorized even if null is not allowed
+
+ // Pick the first one...
+ $aClasses = $this->GetAllowedValues();
+ $sDefault = key($aClasses);
+ }
+
+ return $sDefault;
+ }
+
+ /**
+ * @param array $aArgs
+ * @param string $sContains
+ *
+ * @return array|null
+ * @throws \CoreException
+ */
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ $oValSetDef = $this->GetValuesDef();
+ if (!$oValSetDef) {
+ return null;
+ }
+
+ $aListClass = $oValSetDef->GetValues($aArgs, $sContains);
+ /* @since 3.3.0 remove elements in class_exclusion_list*/
+ $sClassExclusionList = $this->GetOptional('class_exclusion_list',null);
+ if (!empty($sClassExclusionList)) {
+ foreach (explode(',', $sClassExclusionList) as $sClassName) {
+ unset($aListClass[trim($sClassName)]);
+ }
+ }
+
+ return $aListClass;
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (empty($sValue)) {
+ return '';
+ }
+
+ return MetaModel::GetName($sValue);
+ }
+
+ public function RequiresIndex()
+ {
+ return true;
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return '=';
+ }
+
+}
+
+
+/**
+ * An attribute that matches a class state
+ *
+ * @package iTopORM
+ */
+class AttributeClassState 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);
+ }
+
+ public static function ListExpectedParams()
+ {
+ return array_merge(parent::ListExpectedParams(), array('class_field'));
+ }
+
+ public function GetAllowedValues($aArgs = array(), $sContains = '')
+ {
+ if (isset($aArgs['this']))
+ {
+ $oHostObj = $aArgs['this'];
+ $sTargetClass = $this->Get('class_field');
+ $sClass = $oHostObj->Get($sTargetClass);
+
+ $aAllowedStates = array();
+ foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass)
+ {
+ $aValues = MetaModel::EnumStates($sChildClass);
+ foreach (array_keys($aValues) as $sState)
+ {
+ $aAllowedStates[$sState] = $sState.' ('.MetaModel::GetStateLabel($sChildClass, $sState).')';
+ }
+ }
+ return $aAllowedStates;
+ }
+
+ return null;
+ }
+
+ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
+ {
+ if (empty($sValue))
+ {
+ return '';
+ }
+
+ if (!empty($oHostObject))
+ {
+ $sTargetClass = $this->Get('class_field');
+ $sClass = $oHostObject->Get($sTargetClass);
+ foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass)
+ {
+ $aValues = MetaModel::EnumStates($sChildClass);
+ if (in_array($sValue, $aValues))
+ {
+ $sLabelForHtmlAttribute = utils::EscapeHtml($sValue.' ('.MetaModel::GetStateLabel($sChildClass, $sValue).')');
+ $sHTML = ''.$sValue.'';
+
+ return $sHTML;
+ }
+ }
+ }
+
+ return $sValue;
+ }
+
+}
+
+/**
+ * An attibute that matches one of the language codes availables in the dictionnary
+ *
+ * @package iTopORM
+ */
+class AttributeApplicationLanguage extends AttributeString
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
+
+ public static function ListExpectedParams()
+ {
+ return parent::ListExpectedParams();
+ }
+
+ public function __construct($sCode, $aParams)
+ {
+ $this->m_sCode = $sCode;
+ $aAvailableLanguages = Dict::GetLanguages();
+ $aLanguageCodes = array();
+ foreach($aAvailableLanguages as $sLangCode => $aInfo)
+ {
+ $aLanguageCodes[$sLangCode] = $aInfo['description'].' ('.$aInfo['localized_description'].')';
+ }
+
+ // N°6462 This should be sorted directly in \Dict during the compilation but we can't for 2 reasons:
+ // - Additional languages can be added on the fly even though it is not recommended
+ // - Formatting is done at run time (just above)
+ natcasesort($aLanguageCodes);
+
+ $aParams["allowed_values"] = new ValueSetEnum($aLanguageCodes);
+ parent::__construct($sCode, $aParams);
+ }
+
+ public function RequiresIndex()
+ {
+ return true;
+ }
+
+ public function GetBasicFilterLooseOperator()
+ {
+ return '=';
+ }
+}
+
+/**
+ * The attribute dedicated to the finalclass automatic attribute
+ *
+ * @package iTopORM
+ */
+class AttributeFinalClass extends AttributeString
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
+ public $m_sValue;
+
+ public function __construct($sCode, $aParams)
+ {
+ $this->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;
+ }
+}
+
+
+/**
+ * Map a varchar column (size < ?) to an attribute that must never be shown to the user
+ *
+ * @package iTopORM
+ */
+class AttributePassword extends AttributeString implements iAttributeNoGroupBy
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
+
+ /**
+ * 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 static function ListExpectedParams()
+ {
+ return parent::ListExpectedParams();
+ //return array_merge(parent::ListExpectedParams(), array());
+ }
+
+ public function GetEditClass()
+ {
+ return "Password";
+ }
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "VARCHAR(64)"
+ .CMDBSource::GetSqlStringColumnDefinition()
+ .($bFullSpec ? $this->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'
+}
+
+/**
+ * Map a text column (size < 255) to an attribute that is encrypted in the database
+ * The encryption is based on a key set per iTop instance. Thus if you export your
+ * database (in SQL) to someone else without providing the key at the same time
+ * the encrypted fields will remain encrypted
+ *
+ * @package iTopORM
+ */
+class AttributeEncryptedString extends AttributeString implements iAttributeNoGroupBy
+{
+ const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
+
+ protected function GetSQLCol($bFullSpec = false)
+ {
+ return "TINYBLOB";
+ }
+
+ public function GetMaxSize()
+ {
+ return 255;
+ }
+
+ public function MakeRealValue($proposedValue, $oHostObj)
+ {
+ if (is_null($proposedValue))
+ {
+ return null;
+ }
+
+ return (string)$proposedValue;
+ }
+
+ /**
+ * Decrypt the value when reading from the database
+ *
+ * @param array $aCols
+ * @param string $sPrefix
+ *
+ * @return string
+ * @throws \Exception
+ */
+ public function FromSQLToValue($aCols, $sPrefix = '')
+ {
+ $oSimpleCrypt = new SimpleCrypt(MetaModel::GetConfig()->GetEncryptionLibrary());
+ $sValue = $oSimpleCrypt->Decrypt(MetaModel::GetConfig()->GetEncryptionKey(), $aCols[$sPrefix]);
+
+ return $sValue;
+ }
+
+ /**
+ * Encrypt the value before storing it in the database
+ *
+ * @param $value
+ *
+ * @return array
+ * @throws \Exception
+ */
+ public function GetSQLValues($value)
+ {
+ $oSimpleCrypt = new SimpleCrypt(MetaModel::GetConfig()->GetEncryptionLibrary());
+ $encryptedValue = $oSimpleCrypt->Encrypt(MetaModel::GetConfig()->GetEncryptionKey(), $value);
+
+ $aValues = array();
+ $aValues[$this->Get("sql")] = $encryptedValue;
+
+ return $aValues;
+ }
+
+ protected function GetChangeRecordAdditionalData(CMDBChangeOp $oMyChangeOp, DBObject $oObject, $original, $value): void
+ {
+ if (is_null($original)) {
+ $original = '';
+ }
+ $oMyChangeOp->Set("prevstring", $original);
+ }
+
+ protected function GetChangeRecordClassName(): string
+ {
+ return CMDBChangeOpSetAttributeEncrypted::class;
+ }
+
+
+}
+
+
+/**
+ * Wiki formatting - experimental
+ *
+ * [[:|