From 6ddaa82eab1794e845da70e3ce7dac78cebd09f1 Mon Sep 17 00:00:00 2001 From: Benjamin Dalsass Date: Wed, 10 Sep 2025 15:45:54 +0200 Subject: [PATCH 1/3] =?UTF-8?q?N=C2=B08699=20-=20attributedef.class.inc.ph?= =?UTF-8?q?p=20to=20PSR4=20[1-DUPLICATE]=20-=20Duplicates=20attributedef.c?= =?UTF-8?q?lass.inc.php=20as=20attributedefrequires.class.inc.php=20(will?= =?UTF-8?q?=20contains=20the=20require=5Fonce=20directives)=20-=20Duplicat?= =?UTF-8?q?es=20attributedef.class.inc.php=20for=20each=20final=20class=20?= =?UTF-8?q?files=20AttributeApplicationLanguage.php,=20AttributeArchiveDat?= =?UTF-8?q?e.php...=20to=20keep=20VCS=20=20history=20-=20Remove=20attribut?= =?UTF-8?q?edef.class.inc.php=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...php => attributedefrequires.class.inc.php} | 0 .../AttributeApplicationLanguage.php | 13835 ++++++++++++++++ .../AttributeArchiveDate.php | 13835 ++++++++++++++++ .../AttributeArchiveFlag.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeBlob.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeBoolean.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeCaseLog.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeClass.php | 13835 ++++++++++++++++ .../AttributeClassAttCodeSet.php | 13835 ++++++++++++++++ .../AttributeClassState.php | 13835 ++++++++++++++++ .../AttributeCustomFields.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeDBField.php | 13835 ++++++++++++++++ .../AttributeDBFieldVoid.php | 13835 ++++++++++++++++ .../AttributeDashboard.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeDate.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeDateTime.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeDeadline.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeDecimal.php | 13835 ++++++++++++++++ .../AttributeDefinition.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeDuration.php | 13835 ++++++++++++++++ .../AttributeEmailAddress.php | 13835 ++++++++++++++++ .../AttributeEncryptedString.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeEnum.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeEnumSet.php | 13835 ++++++++++++++++ .../AttributeExternalField.php | 13835 ++++++++++++++++ .../AttributeExternalKey.php | 13835 ++++++++++++++++ .../AttributeFinalClass.php | 13835 ++++++++++++++++ .../AttributeFriendlyName.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeHTML.php | 13835 ++++++++++++++++ .../AttributeHierarchicalKey.php | 13835 ++++++++++++++++ .../AttributeIPAddress.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeImage.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeInteger.php | 13835 ++++++++++++++++ .../AttributeLinkedSet.php | 13835 ++++++++++++++++ .../AttributeLinkedSetIndirect.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeLongText.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeMetaEnum.php | 13835 ++++++++++++++++ .../Core/AttributeDefinition/AttributeOQL.php | 13835 ++++++++++++++++ .../AttributeObjectKey.php | 13835 ++++++++++++++++ .../AttributeObsolescenceDate.php | 13835 ++++++++++++++++ .../AttributeObsolescenceFlag.php | 13835 ++++++++++++++++ .../AttributeOneWayPassword.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributePassword.php | 13835 ++++++++++++++++ .../AttributePercentage.php | 13835 ++++++++++++++++ .../AttributePhoneNumber.php | 13835 ++++++++++++++++ .../AttributePropertySet.php | 13835 ++++++++++++++++ .../AttributeQueryAttCodeSet.php | 13835 ++++++++++++++++ .../AttributeRedundancySettings.php | 13835 ++++++++++++++++ .../Core/AttributeDefinition/AttributeSet.php | 13835 ++++++++++++++++ .../AttributeStopWatch.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeString.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeSubItem.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeTable.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeTagSet.php | 13835 ++++++++++++++++ .../AttributeTemplateHTML.php | 13835 ++++++++++++++++ .../AttributeTemplateString.php | 13835 ++++++++++++++++ .../AttributeTemplateText.php | 13835 ++++++++++++++++ .../AttributeDefinition/AttributeText.php | 13835 ++++++++++++++++ .../Core/AttributeDefinition/AttributeURL.php | 13835 ++++++++++++++++ .../MissingColumnException.php | 13835 ++++++++++++++++ .../iAttributeNoGroupBy.php | 13835 ++++++++++++++++ 61 files changed, 830100 insertions(+) rename core/{attributedef.class.inc.php => attributedefrequires.class.inc.php} (100%) create mode 100644 sources/Core/AttributeDefinition/AttributeApplicationLanguage.php create mode 100644 sources/Core/AttributeDefinition/AttributeArchiveDate.php create mode 100644 sources/Core/AttributeDefinition/AttributeArchiveFlag.php create mode 100644 sources/Core/AttributeDefinition/AttributeBlob.php create mode 100644 sources/Core/AttributeDefinition/AttributeBoolean.php create mode 100644 sources/Core/AttributeDefinition/AttributeCaseLog.php create mode 100644 sources/Core/AttributeDefinition/AttributeClass.php create mode 100644 sources/Core/AttributeDefinition/AttributeClassAttCodeSet.php create mode 100644 sources/Core/AttributeDefinition/AttributeClassState.php create mode 100644 sources/Core/AttributeDefinition/AttributeCustomFields.php create mode 100644 sources/Core/AttributeDefinition/AttributeDBField.php create mode 100644 sources/Core/AttributeDefinition/AttributeDBFieldVoid.php create mode 100644 sources/Core/AttributeDefinition/AttributeDashboard.php create mode 100644 sources/Core/AttributeDefinition/AttributeDate.php create mode 100644 sources/Core/AttributeDefinition/AttributeDateTime.php create mode 100644 sources/Core/AttributeDefinition/AttributeDeadline.php create mode 100644 sources/Core/AttributeDefinition/AttributeDecimal.php create mode 100644 sources/Core/AttributeDefinition/AttributeDefinition.php create mode 100644 sources/Core/AttributeDefinition/AttributeDuration.php create mode 100644 sources/Core/AttributeDefinition/AttributeEmailAddress.php create mode 100644 sources/Core/AttributeDefinition/AttributeEncryptedString.php create mode 100644 sources/Core/AttributeDefinition/AttributeEnum.php create mode 100644 sources/Core/AttributeDefinition/AttributeEnumSet.php create mode 100644 sources/Core/AttributeDefinition/AttributeExternalField.php create mode 100644 sources/Core/AttributeDefinition/AttributeExternalKey.php create mode 100644 sources/Core/AttributeDefinition/AttributeFinalClass.php create mode 100644 sources/Core/AttributeDefinition/AttributeFriendlyName.php create mode 100644 sources/Core/AttributeDefinition/AttributeHTML.php create mode 100644 sources/Core/AttributeDefinition/AttributeHierarchicalKey.php create mode 100644 sources/Core/AttributeDefinition/AttributeIPAddress.php create mode 100644 sources/Core/AttributeDefinition/AttributeImage.php create mode 100644 sources/Core/AttributeDefinition/AttributeInteger.php create mode 100644 sources/Core/AttributeDefinition/AttributeLinkedSet.php create mode 100644 sources/Core/AttributeDefinition/AttributeLinkedSetIndirect.php create mode 100644 sources/Core/AttributeDefinition/AttributeLongText.php create mode 100644 sources/Core/AttributeDefinition/AttributeMetaEnum.php create mode 100644 sources/Core/AttributeDefinition/AttributeOQL.php create mode 100644 sources/Core/AttributeDefinition/AttributeObjectKey.php create mode 100644 sources/Core/AttributeDefinition/AttributeObsolescenceDate.php create mode 100644 sources/Core/AttributeDefinition/AttributeObsolescenceFlag.php create mode 100644 sources/Core/AttributeDefinition/AttributeOneWayPassword.php create mode 100644 sources/Core/AttributeDefinition/AttributePassword.php create mode 100644 sources/Core/AttributeDefinition/AttributePercentage.php create mode 100644 sources/Core/AttributeDefinition/AttributePhoneNumber.php create mode 100644 sources/Core/AttributeDefinition/AttributePropertySet.php create mode 100644 sources/Core/AttributeDefinition/AttributeQueryAttCodeSet.php create mode 100644 sources/Core/AttributeDefinition/AttributeRedundancySettings.php create mode 100644 sources/Core/AttributeDefinition/AttributeSet.php create mode 100644 sources/Core/AttributeDefinition/AttributeStopWatch.php create mode 100644 sources/Core/AttributeDefinition/AttributeString.php create mode 100644 sources/Core/AttributeDefinition/AttributeSubItem.php create mode 100644 sources/Core/AttributeDefinition/AttributeTable.php create mode 100644 sources/Core/AttributeDefinition/AttributeTagSet.php create mode 100644 sources/Core/AttributeDefinition/AttributeTemplateHTML.php create mode 100644 sources/Core/AttributeDefinition/AttributeTemplateString.php create mode 100644 sources/Core/AttributeDefinition/AttributeTemplateText.php create mode 100644 sources/Core/AttributeDefinition/AttributeText.php create mode 100644 sources/Core/AttributeDefinition/AttributeURL.php create mode 100644 sources/Core/AttributeDefinition/MissingColumnException.php create mode 100644 sources/Core/AttributeDefinition/iAttributeNoGroupBy.php 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 0000000000..b5103b12da --- /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\n"; + } + $sRes .= "\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 + * + * [[:|