From 847c1d27366d5022dd4e779573363c7a761f75cf Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Thu, 24 Mar 2016 10:49:04 +0000 Subject: [PATCH] Custom fields: track the changes and improve the robustness with regards to the Exception thrown by the handler. Also fixed an issue with DBObject, causing the custom fields to be written several times if invoking DBUpdate more than once. Theoretically, this issue affects any type of attribute. SVN:trunk[3966] --- core/attributedef.class.inc.php | 108 +++++++++++++++++++----- core/cmdbchangeop.class.inc.php | 68 ++++++++++++++- core/cmdbobject.class.inc.php | 15 +++- core/customfieldshandler.class.inc.php | 16 ++-- core/dbobject.class.php | 24 ++++-- core/ormcustomfieldsvalue.class.inc.php | 10 +-- 6 files changed, 194 insertions(+), 47 deletions(-) diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 746bac4f3..23f95c86f 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -6503,10 +6503,10 @@ class AttributeCustomFields extends AttributeDefinition * @param array|null $aValues * @return CustomFieldsHandler */ - public function GetHandler(DBObject $oHostObject, $aValues = null) + public function GetHandler($aValues = null) { $sHandlerClass = $this->Get('handler_class'); - $oHandler = new $sHandlerClass($oHostObject, $this->GetCode()); + $oHandler = new $sHandlerClass($this->GetCode()); if (!is_null($aValues)) { $oHandler->SetCurrentValues($aValues); @@ -6581,11 +6581,23 @@ class AttributeCustomFields extends AttributeDefinition */ public function GetForm(DBObject $oHostObject, $sFormPrefix = null) { - $oValue = $oHostObject->Get($this->GetCode()); - $oHandler = $this->GetHandler($oHostObject, $oValue->GetValues()); - $sFormId = is_null($sFormPrefix) ? 'cf_'.$this->GetCode() : $sFormPrefix.'_cf_'.$this->GetCode(); - $oHandler->BuildForm($sFormId); - return $oHandler->GetForm(); + try + { + $oValue = $oHostObject->Get($this->GetCode()); + $oHandler = $this->GetHandler($oValue->GetValues()); + $sFormId = is_null($sFormPrefix) ? 'cf_'.$this->GetCode() : $sFormPrefix.'_cf_'.$this->GetCode(); + $oHandler->BuildForm($oHostObject, $sFormId); + $oForm = $oHandler->GetForm(); + } + catch (Exception $e) + { + $oForm = new \Combodo\iTop\Form\Form(''); + $oField = new \Combodo\iTop\Form\Field\LabelField(''); + $oField->SetLabel('Custom field error: '.$e->getMessage()); + $oForm->AddField($oField); + $oForm->Finalize(); + } + return $oForm; } /** @@ -6595,9 +6607,16 @@ class AttributeCustomFields extends AttributeDefinition */ public function ReadValue($oHostObject) { - $oHandler = $this->GetHandler($oHostObject); - $aValues = $oHandler->ReadValues(); - $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); + try + { + $oHandler = $this->GetHandler(); + $aValues = $oHandler->ReadValues($oHostObject); + $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); + } + catch (Exception $e) + { + $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode()); + } return $oRet; } @@ -6611,18 +6630,18 @@ class AttributeCustomFields extends AttributeDefinition { if (is_null($oValue)) { - $oHandler = $this->GetHandler($oHostObject); + $oHandler = $this->GetHandler(); $aValues = array(); } else { // Pass the values through the form to make sure that they are correct - $oHandler = $this->GetHandler($oHostObject, $oValue->GetValues()); - $oHandler->BuildForm(''); + $oHandler = $this->GetHandler($oValue->GetValues()); + $oHandler->BuildForm($oHostObject, ''); $oForm = $oHandler->GetForm(); $aValues = $oForm->GetCurrentValues(); } - return $oHandler->WriteValues($aValues); + return $oHandler->WriteValues($oHostObject, $aValues); } /** @@ -6635,8 +6654,8 @@ class AttributeCustomFields extends AttributeDefinition { try { - $oHandler = $this->GetHandler($oHostObject, $value->GetValues()); - $oHandler->BuildForm(''); + $oHandler = $this->GetHandler($value->GetValues()); + $oHandler->BuildForm($oHostObject, ''); $oForm = $oHandler->GetForm(); $oForm->Validate(); if ($oForm->GetValid()) @@ -6667,23 +6686,50 @@ class AttributeCustomFields extends AttributeDefinition public function DeleteValue(DBObject $oHostObject) { $oValue = $oHostObject->Get($this->GetCode()); - $oHandler = $this->GetHandler($oHostObject, $oValue->GetValues()); - return $oHandler->DeleteValues(); + $oHandler = $this->GetHandler($oValue->GetValues()); + return $oHandler->DeleteValues($oHostObject); } public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { - return $value->GetAsHTML($bLocalize); + try + { + $sRet = $value->GetAsHTML($bLocalize); + } + catch (Exception $e) + { + $sRet = 'Custom field error: '.htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8'); + } + return $sRet; } public function GetAsXML($value, $oHostObject = null, $bLocalize = true) { - return $value->GetAsXML($bLocalize); + try + { + $sRet = $value->GetAsXML($bLocalize); + } + catch (Exception $e) + { + $sRet = Str::pure2xml('Custom field error: '.$e->getMessage()); + } + return $sRet; } public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { - return $value->GetAsCSV($sSeparator, $sTextQualifier, $bLocalize, $bConvertToPlainText); + try + { + $sRet = $value->GetAsCSV($sSeparator, $sTextQualifier, $bLocalize, $bConvertToPlainText); + } + catch (Exception $e) + { + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, 'Custom field error: '.$e->getMessage()); + $sRet = $sTextQualifier.$sEscaped.$sTextQualifier; + } + return $sRet; } /** @@ -6704,7 +6750,15 @@ class AttributeCustomFields extends AttributeDefinition */ public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) { - return $value->GetForTemplate($sVerb, $bLocalize); + try + { + $sRet = $value->GetForTemplate($sVerb, $bLocalize); + } + catch (Exception $e) + { + $sRet = 'Custom field error: '.$e->getMessage(); + } + return $sRet; } public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) @@ -6732,7 +6786,15 @@ class AttributeCustomFields extends AttributeDefinition public function Equals($val1, $val2) { - return $val1->Equals($val2); + try + { + $bEquals = $val1->Equals($val2); + } + catch (Exception $e) + { + false; + } + return $bEquals; } } diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php index 2937df71c..38b23eeb0 100644 --- a/core/cmdbchangeop.class.inc.php +++ b/core/cmdbchangeop.class.inc.php @@ -910,4 +910,70 @@ class CMDBChangeOpSetAttributeLinksTune extends CMDBChangeOpSetAttributeLinks return $sResult; } } -?> + +/** + * Record the modification of custom fields + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeCustomFields extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_custfields", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeLongText("prevdata", array("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + $aValues = json_decode($this->Get('prevdata'), true); + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + + try + { + $oHandler = $oAttDef->GetHandler($aValues); + $sValueDesc = $oHandler->GetAsHTML($aValues); + } + catch (Exception $e) + { + $sValueDesc = 'Custom field error: '.htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8'); + } + $sTextView = '
'.$sValueDesc.'
'; + + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); + } + } + return $sResult; + } +} diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index c9e424d65..78aedbd72 100644 --- a/core/cmdbobject.class.inc.php +++ b/core/cmdbobject.class.inc.php @@ -396,6 +396,17 @@ abstract class CMDBObject extends DBObject $oMyChangeOp->Set("newvalue", $value[$sAttCode]); $iId = $oMyChangeOp->DBInsertNoReload(); } + elseif ($oAttDef instanceOf AttributeCustomFields) + { + // Custom fields + // + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCustomFields"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + $oMyChangeOp->Set("prevdata", json_encode($original->GetValues())); + $iId = $oMyChangeOp->DBInsertNoReload(); + } else { // Scalars @@ -536,13 +547,13 @@ abstract class CMDBObject extends DBObject public static function BulkUpdate(DBSearch $oFilter, array $aValues) { - return $this->BulkUpdateTracked_Internal($oFilter, $aValues); + return static::BulkUpdateTracked_Internal($oFilter, $aValues); } public static function BulkUpdateTracked(CMDBChange $oChange, DBSearch $oFilter, array $aValues) { self::SetCurrentChange($oChange); - $this->BulkUpdateTracked_Internal($oFilter, $aValues); + static::BulkUpdateTracked_Internal($oFilter, $aValues); } protected static function BulkUpdateTracked_Internal(DBSearch $oFilter, array $aValues) diff --git a/core/customfieldshandler.class.inc.php b/core/customfieldshandler.class.inc.php index 2a4110bda..1befb9c1d 100644 --- a/core/customfieldshandler.class.inc.php +++ b/core/customfieldshandler.class.inc.php @@ -28,7 +28,6 @@ use Combodo\iTop\Form\FormManager; abstract class CustomFieldsHandler { - protected $oHostObject; protected $sAttCode; protected $aValues; protected $oForm; @@ -37,17 +36,15 @@ abstract class CustomFieldsHandler * This constructor's prototype must be frozen. * Any specific behavior must be implemented in BuildForm() * - * @param DBObject $oHostObject * @param $sAttCode */ - final public function __construct(DBObject $oHostObject, $sAttCode) + final public function __construct($sAttCode) { - $this->oHostObject = $oHostObject; $this->sAttCode = $sAttCode; $this->aValues = null; } - abstract public function BuildForm($sFormId); + abstract public function BuildForm(DBObject $oHostObject, $sFormId); /** * @@ -109,21 +106,24 @@ abstract class CustomFieldsHandler abstract public function GetAsCSV($aValues, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true); /** + * @param DBObject $oHostObject * @return array Associative array id => value */ - abstract public function ReadValues(); + abstract public function ReadValues(DBObject $oHostObject); /** * Record the data (currently in the processing of recording the host object) * It is assumed that the data has been checked prior to calling Write() + * @param DBObject $oHostObject * @param array Associative array id => value */ - abstract public function WriteValues($aValues); + abstract public function WriteValues(DBObject $oHostObject, $aValues); /** * Cleanup data upon object deletion (object id still available here) + * @param DBObject $oHostObject */ - abstract public function DeleteValues(); + abstract public function DeleteValues(DBObject $oHostObject); /** * @param $aValuesA diff --git a/core/dbobject.class.php b/core/dbobject.class.php index c0ed8e287..35a079e16 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -1428,6 +1428,8 @@ abstract class DBObject implements iDisplay { if (!$oAttDef->LoadInObject()) continue; if ($oAttDef->LoadFromDB()) continue; + if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue; + if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue; $oAttDef->WriteValue($this, $this->m_aCurrValues[$sAttCode]); } } @@ -1841,11 +1843,15 @@ abstract class DBObject implements iDisplay $bHasANewExternalKeyValue = false; $aHierarchicalKeys = array(); + $aDBChanges = array(); foreach($aChanges as $sAttCode => $valuecurr) { $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true; - if (!$oAttDef->IsDirectField()) unset($aChanges[$sAttCode]); + if ($oAttDef->IsDirectField()) + { + $aDBChanges[$sAttCode] = $aChanges[$sAttCode]; + } if ($oAttDef->IsHierarchicalKey()) { $aHierarchicalKeys[$sAttCode] = $oAttDef; @@ -1865,7 +1871,7 @@ abstract class DBObject implements iDisplay $iDelta =$iMyRight - $iMyLeft + 1; MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable); - if ($aChanges[$sAttCode] == 0) + if ($aDBChanges[$sAttCode] == 0) { // No new parent, insert completely at the right of the tree $sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`"; @@ -1882,33 +1888,36 @@ abstract class DBObject implements iDisplay else { // Insert at the right of the specified parent - $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aChanges[$sAttCode]); + $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aDBChanges[$sAttCode]); $iNewLeft = CMDBSource::QueryToScalar($sSQL); } MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable); $aHKChanges = array(); - $aHKChanges[$sAttCode] = $aChanges[$sAttCode]; + $aHKChanges[$sAttCode] = $aDBChanges[$sAttCode]; $aHKChanges[$oAttDef->GetSQLLeft()] = $iNewLeft; $aHKChanges[$oAttDef->GetSQLRight()] = $iNewLeft + $iDelta - 1; - $aChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below + $aDBChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below } // Update scalar attributes - if (count($aChanges) != 0) + if (count($aDBChanges) != 0) { $oFilter = new DBObjectSearch(get_class($this)); $oFilter->AddCondition('id', $this->m_iKey, '='); - $sSQL = $oFilter->MakeUpdateQuery($aChanges); + $sSQL = $oFilter->MakeUpdateQuery($aDBChanges); CMDBSource::Query($sSQL); } } $this->DBWriteLinks(); $this->WriteExternalAttributes(); + $this->m_bDirty = false; + $this->m_aTouchedAtt = array(); + $this->m_aModifiedAtt = array(); $this->AfterUpdate(); @@ -1928,7 +1937,6 @@ abstract class DBObject implements iDisplay $this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value; } } - } if (count($aChanges) != 0) diff --git a/core/ormcustomfieldsvalue.class.inc.php b/core/ormcustomfieldsvalue.class.inc.php index 42a1d686f..ad81da24d 100644 --- a/core/ormcustomfieldsvalue.class.inc.php +++ b/core/ormcustomfieldsvalue.class.inc.php @@ -59,21 +59,21 @@ class ormCustomFieldsValue public function GetAsHTML($bLocalize = true) { $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->oHostObject, $this->GetValues()); + $oHandler = $oAttDef->GetHandler($this->GetValues()); return $oHandler->GetAsHTML($this->aCurrentValues, $bLocalize); } public function GetAsXML($bLocalize = true) { $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->oHostObject, $this->GetValues()); + $oHandler = $oAttDef->GetHandler($this->GetValues()); return $oHandler->GetAsXML($this->aCurrentValues, $bLocalize); } public function GetAsCSV($sSeparator = ',', $sTextQualifier = '"', $bLocalize = true) { $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->oHostObject, $this->GetValues()); + $oHandler = $oAttDef->GetHandler($this->GetValues()); return $oHandler->GetAsCSV($this->aCurrentValues, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true); } @@ -86,7 +86,7 @@ class ormCustomFieldsValue public function GetForTemplate($sVerb, $bLocalize = true) { $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->oHostObject, $this->GetValues()); + $oHandler = $oAttDef->GetHandler($this->GetValues()); return 'template...verb='.$sVerb.' sur "'.json_encode($this->aCurrentValues).'"'; } @@ -97,7 +97,7 @@ class ormCustomFieldsValue public function Equals(ormCustomFieldsValue $oReference) { $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->oHostObject, $this->GetValues()); + $oHandler = $oAttDef->GetHandler($this->GetValues()); return $oHandler->CompareValues($this->aCurrentValues, $oReference->aCurrentValues); } }