diff --git a/core/attributedef/AttributeDefinition.php b/core/attributedef/AttributeDefinition.php new file mode 100644 index 0000000000..6d4ec03e2c --- /dev/null +++ b/core/attributedef/AttributeDefinition.php @@ -0,0 +1,1300 @@ +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 + * + * @param mixed $value field value + * + * @return string|array PHP struct that can be properly encoded + * + * @see FromJSONToValue for the reverse operation + * + */ + 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) :/ + * + * @param string $json JSON encoded value + * + * @return mixed JSON decoded data, depending on the attribute type + * + * @see GetForJSON for the reverse operation + * + */ + 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 \Combodo\iTop\Form\Validator\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; + } +} \ No newline at end of file