From 7dbbb1c2990702347780cd38130600f39f088d0c Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Mon, 8 Oct 2012 12:17:56 +0000 Subject: [PATCH] #439 Record and display changes in the link sets (ex: Members of a team) #439 Make sure that changes made by a plugin get recorded + simplified the change tracking for the plugins. Simply call DBObject::DBInsert (resp. Update and Delete) and the change will be recorded for the current page. This is compatible with the old (not mandatory anymore) way that was requiring DBInsertTracked APIs (resp. Update, Delete). SVN:trunk[2236] --- .../userrightsprofile.class.inc.php | 8 +- application/cmdbabstract.class.inc.php | 14 +- application/portalwebpage.class.inc.php | 7 +- application/ui.extkeywidget.class.inc.php | 7 +- core/attributedef.class.inc.php | 22 ++ core/cmdbchangeop.class.inc.php | 186 +++++++++++++++++ core/cmdbobject.class.inc.php | 194 +++++++----------- core/dbobject.class.php | 178 +++++++++++++++- core/dbobjectsearch.class.php | 9 +- core/metamodel.class.php | 48 ++++- core/ormstopwatch.class.inc.php | 9 +- css/light-grey.css | 3 + .../datamodel.itop-config-mgmt.xml | 32 +-- dictionaries/dictionary.itop.core.php | 9 + dictionaries/fr.dictionary.itop.core.php | 7 + pages/UI.php | 70 +------ pages/csvimport.php | 11 +- pages/preferences.php | 7 +- pages/schema.php | 7 + portal/index.php | 7 +- setup/ajax.dataloader.php | 13 +- setup/applicationinstaller.class.inc.php | 23 +-- setup/compiler.class.inc.php | 33 ++- webservices/import.php | 12 +- 24 files changed, 622 insertions(+), 294 deletions(-) diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php index 3ba09e7a0..6574cac74 100644 --- a/addons/userrights/userrightsprofile.class.inc.php +++ b/addons/userrights/userrightsprofile.class.inc.php @@ -311,11 +311,9 @@ class UserRightsProfile extends UserRightsAddOnAPI // Installation: create the very first user public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') { - // Create a change to record the history of the User object - $oChange = MetaModel::NewObject("CMDBChange"); - $oChange->Set("date", time()); - $oChange->Set("userinfo", "Initialization"); - $iChangeId = $oChange->DBInsert(); + CMDBObject::SetTrackInfo('Initialization'); + + $oChange = CMDBObject::GetCurrentChange(); $iContactId = 0; // Support drastic data model changes: no organization class (or not writable)! diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 2a58b16ec..ec4a04412 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -2487,7 +2487,7 @@ EOF // Invoke extensions after insertion (the object must exist, have an id, etc.) foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { - $oExtensionInstance->OnDBInsert($this, self::$m_oCurrChange); + $oExtensionInstance->OnDBInsert($this, self::GetCurrentChange()); } return $res; @@ -2500,7 +2500,7 @@ EOF // Invoke extensions after insertion (the object must exist, have an id, etc.) foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { - $oExtensionInstance->OnDBInsert($oNewObj, self::$m_oCurrChange); + $oExtensionInstance->OnDBInsert($oNewObj, self::GetCurrentChange()); } return $oNewObj; } @@ -2512,7 +2512,7 @@ EOF // Invoke extensions after the update (could be before) foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { - $oExtensionInstance->OnDBUpdate($this, self::$m_oCurrChange); + $oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange()); } return $res; } @@ -2528,18 +2528,12 @@ EOF // Invoke extensions before the deletion (the deletion will do some cleanup and we might loose some information foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { - $oExtensionInstance->OnDBDelete($this, self::$m_oCurrChange); + $oExtensionInstance->OnDBDelete($this, self::GetCurrentChange()); } return parent::DBDeleteTracked_Internal($oDeletionPlan); } - protected static function BulkDeleteTracked_Internal(DBObjectSearch $oFilter) - { - // Todo - invoke the extension - return parent::BulkDeleteTracked_Internal($oFilter); - } - public function IsModified() { if (parent::IsModified()) diff --git a/application/portalwebpage.class.inc.php b/application/portalwebpage.class.inc.php index 6027b23c4..91b2d3a0f 100644 --- a/application/portalwebpage.class.inc.php +++ b/application/portalwebpage.class.inc.php @@ -658,12 +658,7 @@ EOF // Record the change // - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oObj->DBUpdateTracked($oMyChange); + $oObj->DBUpdate(); // Trigger ? // diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php index ec538ca83..d5704461f 100644 --- a/application/ui.extkeywidget.class.inc.php +++ b/application/ui.extkeywidget.class.inc.php @@ -485,12 +485,7 @@ EOF $aErrors = $oObj->UpdateObjectFromPostedForm($this->iId); if (count($aErrors) == 0) { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oObj->DBInsertTracked($oMyChange); + $oObj->DBInsert(); return array('name' => $oObj->GetName(), 'id' => $oObj->GetKey()); } else diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 03011643c..fecac2f81 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -75,6 +75,18 @@ define('DEL_SILENT', 2); define('DEL_MOVEUP', 3); +/** + * For Link sets: tracking_level + * + * @package iTopORM + */ +define('LINKSET_TRACKING_NONE', 0); // Do not track changes in the link set +define('LINKSET_TRACKING_LIST', 1); // Do track added/removed items +define('LINKSET_TRACKING_DETAILS', 2); // Do track modified items +define('LINKSET_TRACKING_ALL', 3); // Do track added/removed/modified items + + + /** * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.) * @@ -567,6 +579,11 @@ class AttributeLinkedSet extends AttributeDefinition } } + public function GetTrackingLevel() + { + return $this->GetOptional('tracking_level', LINKSET_TRACKING_LIST); + } + public function GetLinkedClass() {return $this->Get('linked_class');} public function GetExtKeyToMe() {return $this->Get('ext_key_to_me');} @@ -855,6 +872,11 @@ class AttributeLinkedSetIndirect extends AttributeLinkedSet 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', LINKSET_TRACKING_ALL); + } } /** diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php index aa2117699..8c0820838 100644 --- a/core/cmdbchangeop.class.inc.php +++ b/core/cmdbchangeop.class.inc.php @@ -64,6 +64,19 @@ class CMDBChangeOp extends DBObject { return ''; } + + /** + * Safety net: in case the change is not given, let's guarantee that it will + * be set to the current ongoing change (or create a new one) + */ + protected function OnInsert() + { + if ($this->Get('change') <= 0) + { + $this->Set('change', CMDBObject::GetCurrentChange()); + } + parent::OnInsert(); + } } @@ -124,6 +137,11 @@ class CMDBChangeOpDelete extends CMDBChangeOp ); MetaModel::Init_Params($aParams); MetaModel::Init_InheritAttributes(); + + // Final class of the object (objclass must be set to the root class for efficiency purposes) + MetaModel::Init_AddAttribute(new AttributeString("fclass", array("allowed_values"=>null, "sql"=>"fclass", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + // Last friendly name of the object + MetaModel::Init_AddAttribute(new AttributeString("fname", array("allowed_values"=>null, "sql"=>"fname", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); } /** * Describe (as a text string) the modifications corresponding to this change @@ -535,4 +553,172 @@ class CMDBChangeOpPlugin extends CMDBChangeOp return $this->Get('description'); } } + +/** + * Record added/removed objects from within a link set + * + * @package iTopORM + */ +abstract class CMDBChangeOpSetAttributeLinks 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_links", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Note: item class/id points to the link class itself in case of a direct link set (e.g. Server::interface_list => Interface) + // item class/id points to the remote class in case of a indirect link set (e.g. Server::contract_list => Contract) + MetaModel::Init_AddAttribute(new AttributeString("item_class", array("allowed_values"=>null, "sql"=>"item_class", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("item_id", array("allowed_values"=>null, "sql"=>"item_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + } +} + +/** + * Record added/removed objects from within a link set + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeLinksAddRemove extends CMDBChangeOpSetAttributeLinks +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_links_addremove", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeEnum("type", array("allowed_values"=>new ValueSetEnum('added,removed'), "sql"=>"type", "default_value"=>"added", "is_null_allowed"=>false, "depends_on"=>array()))); + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... + + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + + $sItemDesc = MetaModel::GetHyperLink($this->Get('item_class'), $this->Get('item_id')); + + $sResult = $sAttName.' - '; + switch ($this->Get('type')) + { + case 'added': + $sResult .= Dict::Format('Change:LinkSet:Added', $sItemDesc); + break; + + case 'removed': + $sResult .= Dict::Format('Change:LinkSet:Removed', $sItemDesc); + break; + } + } + return $sResult; + } +} + +/** + * Record attribute changes from within a link set + * A single record redirects to the modifications made within the same change + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeLinksTune extends CMDBChangeOpSetAttributeLinks +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_links_tune", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeInteger("link_id", array("allowed_values"=>null, "sql"=>"link_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... + + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + + $sLinkClass = $oAttDef->GetLinkedClass(); + + // Search for changes on the corresponding link + // + $oSearch = new DBObjectSearch('CMDBChangeOpSetAttribute'); + $oSearch->AddCondition('change', $this->Get('change'), '='); + $oSearch->AddCondition('objclass', $sLinkClass, '='); + $oSearch->AddCondition('objkey', $this->Get('link_id'), '='); + $oSet = new DBObjectSet($oSearch); + $aChanges = array(); + while ($oChangeOp = $oSet->Fetch()) + { + $aChanges[] = $oChangeOp->GetDescription(); + } + if (count($aChanges) == 0) + { + return ''; + } + + $sItemDesc = MetaModel::GetHyperLink($this->Get('item_class'), $this->Get('item_id')); + + $sResult = $sAttName.' - '; + $sResult .= Dict::Format('Change:LinkSet:Modified', $sItemDesc); + $sResult .= ' : '.implode(', ', $aChanges); + } + return $sResult; + } +} ?> diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index 1994d904f..8b6308351 100644 --- a/core/cmdbobject.class.inc.php +++ b/core/cmdbobject.class.inc.php @@ -91,8 +91,11 @@ abstract class CMDBObject extends DBObject protected $m_datUpdated; // Note: this value is static, but that could be changed because it is sometimes a real issue (see update of interfaces / connected_to protected static $m_oCurrChange = null; + protected static $m_sInfo = null; // null => the information is built in a standard way - + /** + * Specify another change (this is mainly for backward compatibility) + */ public static function SetCurrentChange(CMDBChange $oChange) { self::$m_oCurrChange = $oChange; @@ -100,34 +103,86 @@ abstract class CMDBObject extends DBObject // // Todo: simplify the APIs and do not pass the current change as an argument anymore - // SetCurrentChange to be invoked in very few cases (UI.php, CSV import, Data synchro) + // SetTrackInfo to be invoked in very few cases (UI.php, CSV import, Data synchro) + // SetCurrentChange is an alternative to SetTrackInfo (csv ?) // GetCurrentChange to be called ONCE (!) by CMDBChangeOp::OnInsert ($this->Set('change', ..GetCurrentChange()) // GetCurrentChange to create a default change if not already done in the current context // - public static function GetCurrentChange() + /** + * Get a change record (create it if not existing) + */ + public static function GetCurrentChange($bAutoCreate = true) { + if ($bAutoCreate && is_null(self::$m_oCurrChange)) + { + self::CreateChange(); + } return self::$m_oCurrChange; } - - private function RecordObjCreation(CMDBChange $oChange) + /** + * Override the additional information (defaulting to user name) + * A call to this verb should replace every occurence of + * $oMyChange = MetaModel::NewObject("CMDBChange"); + * $oMyChange->Set("date", time()); + * $oMyChange->Set("userinfo", 'this is done by ... for ...'); + * $iChangeId = $oMyChange->DBInsert(); + */ + public static function SetTrackInfo($sInfo) { + self::$m_sInfo = $sInfo; + } + + /** + * Get the additional information (defaulting to user name) + */ + protected static function GetTrackInfo() + { + if (is_null(self::$m_sInfo)) + { + return CMDBChange::GetCurrentUserName(); + } + else + { + return self::$m_sInfo; + } + } + + /** + * Create a standard change record (done here 99% of the time, and nearly once per page) + */ + protected static function CreateChange() + { + self::$m_oCurrChange = MetaModel::NewObject("CMDBChange"); + self::$m_oCurrChange->Set("date", time()); + self::$m_oCurrChange->Set("userinfo", self::GetTrackInfo()); + self::$m_oCurrChange->DBInsert(); + } + + protected function RecordObjCreation() + { + parent::RecordObjCreation(); $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpCreate"); - $oMyChangeOp->Set("change", $oChange->GetKey()); $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $iId = $oMyChangeOp->DBInsertNoReload(); } - private function RecordObjDeletion(CMDBChange $oChange, $objkey) + + protected function RecordObjDeletion($objkey) { + parent::RecordObjDeletion($objkey); $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpDelete"); - $oMyChangeOp->Set("change", $oChange->GetKey()); - $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objclass", MetaModel::GetRootClass(get_class($this))); $oMyChangeOp->Set("objkey", $objkey); + $oMyChangeOp->Set("fclass", get_class($this)); + $oMyChangeOp->Set("fname", $this->GetRawName()); $iId = $oMyChangeOp->DBInsertNoReload(); } - private function RecordAttChanges(CMDBChange $oChange, array $aValues, array $aOrigValues) + + protected function RecordAttChanges(array $aValues, array $aOrigValues) { + parent::RecordAttChanges($aValues, $aOrigValues); + // $aValues is an array of $sAttCode => $value // foreach ($aValues as $sAttCode=> $value) @@ -149,7 +204,6 @@ abstract class CMDBObject extends DBObject { // One Way encrypted passwords' history is stored -one way- encrypted $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeOneWayPassword"); - $oMyChangeOp->Set("change", $oChange->GetKey()); $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sAttCode); @@ -165,7 +219,6 @@ abstract class CMDBObject extends DBObject { // Encrypted string history is stored encrypted $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeEncrypted"); - $oMyChangeOp->Set("change", $oChange->GetKey()); $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sAttCode); @@ -181,7 +234,6 @@ abstract class CMDBObject extends DBObject { // Data blobs $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob"); - $oMyChangeOp->Set("change", $oChange->GetKey()); $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sAttCode); @@ -209,7 +261,6 @@ abstract class CMDBObject extends DBObject if ($item_value != $item_original) { $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); - $oMyChangeOp->Set("change", $oChange->GetKey()); $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sSubItemAttCode); @@ -223,7 +274,6 @@ abstract class CMDBObject extends DBObject elseif ($oAttDef instanceOf AttributeCaseLog) { $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCaseLog"); - $oMyChangeOp->Set("change", $oChange->GetKey()); $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sAttCode); @@ -235,7 +285,6 @@ abstract class CMDBObject extends DBObject { // Data blobs $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText"); - $oMyChangeOp->Set("change", $oChange->GetKey()); $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sAttCode); @@ -252,7 +301,6 @@ abstract class CMDBObject extends DBObject // Scalars // $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); - $oMyChangeOp->Set("change", $oChange->GetKey()); $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sAttCode); @@ -294,32 +342,24 @@ abstract class CMDBObject extends DBObject public function DBInsert() { - if(!is_object(self::$m_oCurrChange)) - { - throw new CoreException("DBInsert() could not be used here, please use DBInsertTracked() instead"); - } return $this->DBInsertTracked_Internal(); } public function DBInsertTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) { + self::SetCurrentChange($oChange); $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); - $oPreviousChange = self::$m_oCurrChange; - self::$m_oCurrChange = $oChange; $ret = $this->DBInsertTracked_Internal(); - self::$m_oCurrChange = $oPreviousChange; return $ret; } public function DBInsertTrackedNoReload(CMDBChange $oChange, $bSkipStrongSecurity = null) { + self::SetCurrentChange($oChange); $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); - $oPreviousChange = self::$m_oCurrChange; - self::$m_oCurrChange = $oChange; $ret = $this->DBInsertTracked_Internal(true); - self::$m_oCurrChange = $oPreviousChange; return $ret; } @@ -333,25 +373,18 @@ abstract class CMDBObject extends DBObject { $ret = parent::DBInsert(); } - $this->RecordObjCreation(self::$m_oCurrChange); return $ret; } public function DBClone($newKey = null) { - if(!self::$m_oCurrChange) - { - throw new CoreException("DBClone() could not be used here, please use DBCloneTracked() instead"); - } return $this->DBCloneTracked_Internal(); } public function DBCloneTracked(CMDBChange $oChange, $newKey = null) { - $oPreviousChange = self::$m_oCurrChange; - self::$m_oCurrChange = $oChange; + self::SetCurrentChange($oChange); $this->DBCloneTracked_Internal($newKey); - self::$m_oCurrChange = $oPreviousChange; } protected function DBCloneTracked_Internal($newKey = null) @@ -359,128 +392,57 @@ abstract class CMDBObject extends DBObject $newKey = parent::DBClone($newKey); $oClone = MetaModel::GetObject(get_class($this), $newKey); - $oClone->RecordObjCreation(self::$m_oCurrChange); return $newKey; } public function DBUpdate() - { - if(!self::$m_oCurrChange) - { - throw new CoreException("DBUpdate() could not be used here, please use DBUpdateTracked() instead"); - } - return $this->DBUpdateTracked_internal(); - } - - public function DBUpdateTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) - { - $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); - - $oPreviousChange = self::$m_oCurrChange; - self::$m_oCurrChange = $oChange; - $this->DBUpdateTracked_Internal(); - self::$m_oCurrChange = $oPreviousChange; - } - - protected function DBUpdateTracked_Internal() { // Copy the changes list before the update (the list should be reset afterwards) $aChanges = $this->ListChanges(); if (count($aChanges) == 0) { - //throw new CoreWarning("Attempting to update an unchanged object"); return; } - // Save the original values (will be reset to the new values when the object get written to the DB) - $aOriginalValues = $this->m_aOrigValues; $ret = parent::DBUpdate(); - $this->RecordAttChanges(self::$m_oCurrChange, $aChanges, $aOriginalValues); return $ret; } + public function DBUpdateTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + self::SetCurrentChange($oChange); + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); + $this->DBUpdate(); + } + public function DBDelete(&$oDeletionPlan = null) { - if(!self::$m_oCurrChange) - { - throw new CoreException("DBDelete() could not be used here, please use DBDeleteTracked() instead"); - } return $this->DBDeleteTracked_Internal($oDeletionPlan); } public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null) { + self::SetCurrentChange($oChange); $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_DELETE); - - $oPreviousChange = self::$m_oCurrChange; - self::$m_oCurrChange = $oChange; $this->DBDeleteTracked_Internal($oDeletionPlan); - self::$m_oCurrChange = $oPreviousChange; } protected function DBDeleteTracked_Internal(&$oDeletionPlan = null) { $prevkey = $this->GetKey(); $ret = parent::DBDelete($oDeletionPlan); - $this->RecordObjDeletion(self::$m_oCurrChange, $prevkey); - return $ret; - } - - public static function BulkDelete(DBObjectSearch $oFilter) - { - if(!self::$m_oCurrChange) - { - throw new CoreException("BulkDelete() could not be used here, please use BulkDeleteTracked() instead"); - } - return $this->BulkDeleteTracked_Internal($oFilter); - } - - public static function BulkDeleteTracked(CMDBChange $oChange, DBObjectSearch $oFilter) - { - $oPreviousChange = self::$m_oCurrChange; - self::$m_oCurrChange = $oChange; - $this->BulkDeleteTracked_Internal($oFilter); - self::$m_oCurrChange = $oPreviousChange; - } - - protected static function BulkDeleteTracked_Internal(DBObjectSearch $oFilter) - { - throw new CoreWarning("Change tracking not tested for bulk operations"); - - // Get the list of objects to delete (and record data before deleting the DB records) - $oObjSet = new CMDBObjectSet($oFilter); - $aObjAndKeys = array(); // array of id=>object - while ($oItem = $oObjSet->Fetch()) - { - $aObjAndKeys[$oItem->GetKey()] = $oItem; - } - $oObjSet->FreeResult(); - - // Delete in one single efficient query - $ret = parent::BulkDelete($oFilter); - // Record... in many queries !!! - foreach($aObjAndKeys as $prevkey=>$oItem) - { - $oItem->RecordObjDeletion(self::$m_oCurrChange, $prevkey); - } return $ret; } public static function BulkUpdate(DBObjectSearch $oFilter, array $aValues) { - if(!self::$m_oCurrChange) - { - throw new CoreException("BulkUpdate() could not be used here, please use BulkUpdateTracked() instead"); - } return $this->BulkUpdateTracked_Internal($oFilter, $aValues); } public static function BulkUpdateTracked(CMDBChange $oChange, DBObjectSearch $oFilter, array $aValues) { - $oPreviousChange = self::$m_oCurrChange; - self::$m_oCurrChange = $oChange; + self::SetCurrentChange($oChange); $this->BulkUpdateTracked_Internal($oFilter, $aValues); - self::$m_oCurrChange = $oPreviousChange; } protected static function BulkUpdateTracked_Internal(DBObjectSearch $oFilter, array $aValues) @@ -507,7 +469,7 @@ abstract class CMDBObject extends DBObject while ($oItem = $oObjSet->Fetch()) { $aChangedValues = $oItem->ListChangedValues($aValues); - $oItem->RecordAttChanges(self::$m_oCurrChange, $aChangedValues, $aOriginalValues[$oItem->GetKey()]); + $oItem->RecordAttChanges($aChangedValues, $aOriginalValues[$oItem->GetKey()]); } return $ret; } diff --git a/core/dbobject.class.php b/core/dbobject.class.php index dc217bccc..c8c228ac3 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -1424,6 +1424,8 @@ abstract class DBObject $oTrigger->DoActivate($this->ToArgs('this')); } + $this->RecordObjCreation(); + return $this->m_iKey; } @@ -1434,13 +1436,15 @@ abstract class DBObject return $this->m_iKey; } - public function DBInsertTracked(CMDBChange $oVoid) + public function DBInsertTracked(CMDBChange $oChange) { + CMDBObject::SetCurrentChange($oChange); return $this->DBInsert(); } - public function DBInsertTrackedNoReload(CMDBChange $oVoid) + public function DBInsertTrackedNoReload(CMDBChange $oChange) { + CMDBObject::SetCurrentChange($oChange); return $this->DBInsertNoReload(); } @@ -1450,7 +1454,9 @@ abstract class DBObject { $this->m_bIsInDB = false; $this->m_iKey = $iNewKey; - return $this->DBInsert(); + $ret = $this->DBInsert(); + $this->RecordObjCreation(); + return $ret; } /** @@ -1498,7 +1504,7 @@ abstract class DBObject $aChanges = $this->ListChanges(); if (count($aChanges) == 0) { - //throw new CoreWarning("Attempting to update an unchanged object"); + // Attempting to update an unchanged object return; } @@ -1510,6 +1516,9 @@ abstract class DBObject throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey())); } + // Save the original values (will be reset to the new values when the object get written to the DB) + $aOriginalValues = $this->m_aOrigValues; + $bHasANewExternalKeyValue = false; $aHierarchicalKeys = array(); foreach($aChanges as $sAttCode => $valuecurr) @@ -1588,11 +1597,17 @@ abstract class DBObject $this->Reload(); } + if (count($aChanges) != 0) + { + $this->RecordAttChanges($aChanges, $aOriginalValues); + } + return $this->m_iKey; } - public function DBUpdateTracked(CMDBChange $oVoid) + public function DBUpdateTracked(CMDBChange $oChange) { + CMDBObject::SetCurrentChange($oChange); return $this->DBUpdate(); } @@ -1664,6 +1679,8 @@ abstract class DBObject $this->AfterDelete(); + $this->RecordObjDeletion($this->m_iKey); + $this->m_bIsInDB = false; $this->m_iKey = null; } @@ -1719,8 +1736,9 @@ abstract class DBObject return $oDeletionPlan; } - public function DBDeleteTracked(CMDBChange $oVoid, $bSkipStrongSecurity = null, &$oDeletionPlan = null) + public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null) { + CMDBObject::SetCurrentChange($oChange); $this->DBDelete($oDeletionPlan); } @@ -1899,6 +1917,152 @@ abstract class DBObject { } + + /** + * Common to the recording of link set changes (add/remove/modify) + */ + private function PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, $sChangeOpClass, $aOriginalValues = null) + { + if ($iLinkSetOwnerId <= 0) + { + return null; + } + + if (!is_subclass_of($oLinkSet->GetHostClass(), 'CMDBObject')) + { + // The link set owner class does not keep track of its history + return null; + } + + // Determine the linked item class and id + // + if ($oLinkSet->IsIndirect()) + { + // The "item" is on the other end (N-N links) + $sExtKeyToRemote = $oLinkSet->GetExtKeyToRemote(); + $oExtKeyToRemote = MetaModel::GetAttributeDef(get_class($this), $sExtKeyToRemote); + $sItemClass = $oExtKeyToRemote->GetTargetClass(); + if ($aOriginalValues) + { + // Get the value from the original values + $iItemId = $aOriginalValues[$sExtKeyToRemote]; + } + else + { + $iItemId = $this->Get($sExtKeyToRemote); + } + } + else + { + // I am the "item" (1-N links) + $sItemClass = get_class($this); + $iItemId = $this->GetKey(); + } + + // Get the remote object, to determine its exact class + // Possible optimization: implement a tool in MetaModel, to get the final class of an object (not always querying + query reduced to a select on the root table! + $oOwner = MetaModel::GetObject($oLinkSet->GetHostClass(), $iLinkSetOwnerId, false); + if ($oOwner) + { + $sLinkSetOwnerClass = get_class($oOwner); + + $oMyChangeOp = MetaModel::NewObject($sChangeOpClass); + $oMyChangeOp->Set("objclass", $sLinkSetOwnerClass); + $oMyChangeOp->Set("objkey", $iLinkSetOwnerId); + $oMyChangeOp->Set("attcode", $oLinkSet->GetCode()); + $oMyChangeOp->Set("item_class", $sItemClass); + $oMyChangeOp->Set("item_id", $iItemId); + return $oMyChangeOp; + } + else + { + // Depending on the deletion order, it may happen that the id is already invalid... ignore + return null; + } + } + + /** + * This object has been created/deleted, record that as a change in link sets pointing to this (if any) + */ + private function RecordLinkSetListChange($bAdd = true) + { + $aForwardChangeTracking = MetaModel::GetTrackForwardExternalKeys(get_class($this)); + foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet) + { + if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue; + + $iLinkSetOwnerId = $this->Get($sExtKeyAttCode); + $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove'); + if ($oMyChangeOp) + { + if ($bAdd) + { + $oMyChangeOp->Set("type", "added"); + } + else + { + $oMyChangeOp->Set("type", "removed"); + } + $iId = $oMyChangeOp->DBInsertNoReload(); + } + } + } + + protected function RecordObjCreation() + { + $this->RecordLinkSetListChange(true); + } + + protected function RecordObjDeletion($objkey) + { + $this->RecordLinkSetListChange(false); + } + + protected function RecordAttChanges(array $aValues, array $aOrigValues) + { + $aForwardChangeTracking = MetaModel::GetTrackForwardExternalKeys(get_class($this)); + foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet) + { + + if (array_key_exists($sExtKeyAttCode, $aValues)) + { + if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue; + + // Keep track of link added/removed + // + $iLinkSetOwnerNext = $aValues[$sExtKeyAttCode]; + $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerNext, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove'); + if ($oMyChangeOp) + { + $oMyChangeOp->Set("type", "added"); + $oMyChangeOp->DBInsertNoReload(); + } + + $iLinkSetOwnerPrevious = $aOrigValues[$sExtKeyAttCode]; + $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerPrevious, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove', $aOrigValues); + if ($oMyChangeOp) + { + $oMyChangeOp->Set("type", "removed"); + $oMyChangeOp->DBInsertNoReload(); + } + } + else + { + // Keep track of link changes + // + if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_DETAILS) == 0) continue; + + $iLinkSetOwnerId = $this->Get($sExtKeyAttCode); + $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksTune'); + if ($oMyChangeOp) + { + $oMyChangeOp->Set("link_id", $this->GetKey()); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + } + } + } + // Return an empty set for the parent of all public static function GetRelationQueries($sRelCode) { @@ -2103,7 +2267,7 @@ abstract class DBObject } // to be continued... } + } - ?> diff --git a/core/dbobjectsearch.class.php b/core/dbobjectsearch.class.php index abf4ae396..2a8c2891e 100644 --- a/core/dbobjectsearch.class.php +++ b/core/dbobjectsearch.class.php @@ -1126,7 +1126,12 @@ class DBObjectSearch if ($bOQLCacheEnabled && array_key_exists($sQuery, self::$m_aOQLQueries)) { // hit! - return clone self::$m_aOQLQueries[$sQuery]; + $oClone = clone self::$m_aOQLQueries[$sQuery]; + if (!is_null($aParams)) + { + $oClone->m_aParams = $aParams; + } + return $oClone; } $oOql = new OqlInterpreter($sQuery); @@ -1288,4 +1293,4 @@ class DBObjectSearch } -?> +?> diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 0aeee31bf..bc7d7c285 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -879,6 +879,34 @@ abstract class MetaModel } } + protected static $m_aTrackForwardCache = array(); + /** + * List external keys for which there is a LinkSet (direct or indirect) on the other end + * For those external keys, a change will have a special meaning on the other end + * in term of change tracking + */ + final static public function GetTrackForwardExternalKeys($sClass) + { + if (!isset(self::$m_aTrackForwardCache[$sClass])) + { + $aRes = array(); + foreach (MetaModel::GetExternalKeys($sClass) as $sAttCode => $oAttDef) + { + $sRemoteClass = $oAttDef->GetTargetClass(); + foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) + { + if (!$oRemoteAttDef->IsLinkSet()) continue; + if ($oRemoteAttDef->GetLinkedClass() != $sClass) continue; + if ($oRemoteAttDef->GetExtKeyToMe() != $sAttCode) continue; + $aRes[$sAttCode] = $oRemoteAttDef; + } + } + self::$m_aTrackForwardCache[$sClass] = $aRes; + } + return self::$m_aTrackForwardCache[$sClass]; + } + + public static function GetLabel($sClass, $sAttCode) { $oAttDef = self::GetAttributeDef($sClass, $sAttCode); @@ -4730,7 +4758,23 @@ abstract class MetaModel $oObj = self::GetObject($sTargetClass, $iKey, false); if (is_null($oObj)) { - return "$sTargetClass: $iKey (not found)"; + // Whatever we are looking for, the root class is the key to search for + $sRootClass = self::GetRootClass($sTargetClass); + $oSearch = DBObjectSearch::FromOQL('SELECT CMDBChangeOpDelete WHERE objclass = :objclass AND objkey = :objkey', array('objclass' => $sRootClass, 'objkey' => $iKey)); + $oSet = new DBObjectSet($oSearch); + $oRecord = $oSet->Fetch(); + // An empty fname is obtained with iTop < 2.0 + if (is_null($oRecord) || (strlen(trim($oRecord->Get('fname'))) == 0)) + { + $sName = Dict::Format('Core:UnknownObjectLabel', $sTargetClass, $iKey); + $sTitle = Dict::S('Core:UnknownObjectTip'); + } + else + { + $sName = $oRecord->Get('fname'); + $sTitle = Dict::Format('Core:DeletedObjectTip', $oRecord->Get('date'), $oRecord->Get('userinfo')); + } + return ''.htmlentities($sName, ENT_QUOTES, 'UTF-8').''; } return $oObj->GetHyperLink(); } @@ -4751,6 +4795,8 @@ abstract class MetaModel public static function BulkDelete(DBObjectSearch $oFilter) { + throw new Exception("Bulk deletion cannot be done this way: it will not work with hierarchical keys - implementation to be reviewed!"); + $sSQL = self::MakeDeleteQuery($oFilter); if (!self::DBIsReadOnly()) { diff --git a/core/ormstopwatch.class.inc.php b/core/ormstopwatch.class.inc.php index 333641d36..bfad787ad 100644 --- a/core/ormstopwatch.class.inc.php +++ b/core/ormstopwatch.class.inc.php @@ -429,12 +429,9 @@ class CheckStopWatchThresholds implements iBackgroundProcess if($oObj->IsModified()) { - // Todo - factorize so that only one single change will be instantiated - $oMyChange = new CMDBChange(); - $oMyChange->Set("date", time()); - $oMyChange->Set("userinfo", "Automatic - threshold triggered"); - $iChangeId = $oMyChange->DBInsertNoReload(); - + CMDBObject::SetTrackInfo("Automatic - threshold triggered"); + + $oMyChange = CMDBObject::GetCurrentChange(); $oObj->DBUpdateTracked($oMyChange, true /*skip security*/); } diff --git a/css/light-grey.css b/css/light-grey.css index 982f2192c..ed539ae15 100644 --- a/css/light-grey.css +++ b/css/light-grey.css @@ -1368,4 +1368,7 @@ a.summary, a.summary:hover { } .sortable_field_list > li.selected { background: #F6A828; +} +.itop-deleted-object { + text-decoration: line-through; } \ No newline at end of file diff --git a/datamodel/itop-config-mgmt-1.0.0/datamodel.itop-config-mgmt.xml b/datamodel/itop-config-mgmt-1.0.0/datamodel.itop-config-mgmt.xml index d3bc4c36c..ca6369dfe 100644 --- a/datamodel/itop-config-mgmt-1.0.0/datamodel.itop-config-mgmt.xml +++ b/datamodel/itop-config-mgmt-1.0.0/datamodel.itop-config-mgmt.xml @@ -2621,6 +2621,10 @@ m_aOrigValues['connected_if']; // The interface this interface was connected to + if ($iPrevTargetIf == $this->Get('connected_if')) + { + return; + } if ($iPrevTargetIf != 0) { @@ -2628,11 +2632,11 @@ $oPrevConnectedIf = MetaModel::GetObject('NetworkInterface', $iPrevTargetIf, false); if (!is_null($oPrevConnectedIf)) { - $oPrevConnectedIf->Set('connected_if', 0); - // Need to backup the current change, because it is reset when DBUpdateTracked is complete - $oCurrChange = self::$m_oCurrChange; - $oPrevConnectedIf->DBUpdateTracked($oCurrChange); - self::$m_oCurrChange = $oCurrChange; + if ($oPrevConnectedIf->Get('connected_if') == $this->GetKey()) // protection against reentrance + { + $oPrevConnectedIf->Set('connected_if', 0); + $oPrevConnectedIf->DBUpdate(); + } } } @@ -2644,28 +2648,12 @@ if (($oConnIf->Get('connected_if') != $this->GetKey()) || ($sConnLink != $oConnIf->Get('link_type'))) { - // Something has to be changed on the connected interface... - if ($oConnIf->Get('connected_if') != $this->GetKey()) - { - // It is connected to another interface: reset that third one... - $oThirdIf = MetaModel::GetObject('NetworkInterface', $oConnIf->Get('connected_if'), false); - if (!is_null($oThirdIf)) - { - $oThirdIf->Set('connected_if', 0); - // Need to backup the current change, because it is reset when DBUpdateTracked is complete - $oCurrChange = self::$m_oCurrChange; - $oThirdIf->DBUpdateTracked($oCurrChange); - self::$m_oCurrChange = $oCurrChange; - } - } // Connect the remote interface to the current one $oConnIf->Set('connected_if', $this->GetKey()); $oConnIf->Set('link_type', $sConnLink); // Need to backup the current change, because it is reset when DBUpdateTracked is complete - $oCurrChange = self::$m_oCurrChange; - $oConnIf->DBUpdateTracked($oCurrChange); - self::$m_oCurrChange = $oCurrChange; + $oConnIf->DBUpdate(); } } }]]> diff --git a/dictionaries/dictionary.itop.core.php b/dictionaries/dictionary.itop.core.php index d889dc927..27b0cccc4 100644 --- a/dictionaries/dictionary.itop.core.php +++ b/dictionaries/dictionary.itop.core.php @@ -24,6 +24,12 @@ */ Dict::Add('EN US', 'English', 'English', array( + 'Core:DeletedObjectLabel' => '%1s (deleted)', + 'Core:DeletedObjectTip' => 'The object has been deleted on %1$s (%2$s)', + + 'Core:UnknownObjectLabel' => 'Object not found (class: %1$s, id: %2$d)', + 'Core:UnknownObjectTip' => 'The object could not be found. It may have been deleted some time ago and the log has been purged since.', + 'Core:AttributeLinkedSet' => 'Array of objects', 'Core:AttributeLinkedSet+' => 'Any kind of objects of the same class or subclass', @@ -240,6 +246,9 @@ Dict::Add('EN US', 'English', 'English', array( 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modified, previous value: %2$s', 'Change:AttName_Changed' => '%1$s modified', 'Change:AttName_EntryAdded' => '%1$s modified, new entry added.', + 'Change:LinkSet:Added' => 'added %1$s', + 'Change:LinkSet:Removed' => 'removed %1$s', + 'Change:LinkSet:Modified' => 'modified %1$s', )); // diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index c09d16c40..fcc796d2b 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -22,6 +22,10 @@ */ Dict::Add('FR FR', 'French', 'Français', array( + 'Core:DeletedObjectTip' => 'L\'objet a été effacé le %1$s (%2$s)', + 'Core:UnknownObjectLabel' => 'Classe: %1$s, Identifiant: %2$d', + 'Core:UnknownObjectTip' => 'L\'objet n\'a pu être trouvé. Il se peut que les archives aient été purgées après son effacement.', + 'Class:ActionEmail' => 'email notification', 'Class:ActionEmail+' => 'Action: Email notification', 'Class:ActionEmail/Attribute:test_recipient' => 'Destinataire de test', @@ -503,6 +507,9 @@ Opérateurs :
'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modifié, ancienne valeur: %2$s', 'Change:AttName_Changed' => '%1$s modifié', 'Change:AttName_EntryAdded' => '%1$s champ modifié, une nouvelle entrée a été ajoutée', + 'Change:LinkSet:Added' => 'ajout de %1$s', + 'Change:LinkSet:Removed' => 'suppression de %1$s', + 'Change:LinkSet:Modified' => 'modification de %1$s', 'Class:Action' => 'Action', 'Class:Action+' => 'Action spécifique', 'Class:Action/Attribute:name' => 'Nom', diff --git a/pages/UI.php b/pages/UI.php index d1e2e2f42..c46f2750c 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -34,15 +34,7 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed) { if ($bDeleteConfirmed) { - // Prepare the change reporting - // - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $oMyChange->DBInsert(); - - $oObj->DBDeleteTracked($oMyChange, null, $oDeletionPlan); + $oObj->DBDeleteTracked(CMDBObject::GetCurrentChange(), null, $oDeletionPlan); } else { @@ -363,9 +355,8 @@ EOF * @param $oP WebPage The page for the output * @param $oObj CMDBObject The object to process * @param $sNextAction string The code of the stimulus for the 'action' (i.e. Transition) to apply - * @param $oMyChange CMDBChange The change used to log the modifications or null is none is available (a new one will be created) */ -function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction, $oMyChange) +function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction) { // Here handle the apply stimulus $aTransitions = $oObj->EnumTransitions(); @@ -383,15 +374,7 @@ function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction, $oMyChange // If all the mandatory fields are already present, just apply the transition silently... if ($oObj->ApplyStimulus($sNextAction)) { - if ($oMyChange == null) - { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - } - $oObj->DBUpdateTracked($oMyChange); + $oObj->DBUpdate(); } $oObj->Reload(); $oObj->DisplayDetails($oP); @@ -1013,11 +996,6 @@ EOF { throw new Exception(Dict::S('UI:Error:ObjectAlreadyUpdated')); } - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); utils::RemoveTransaction($sTransactionId); } foreach($aSelectedObj as $iId) @@ -1049,7 +1027,7 @@ EOF ); if ($bResult && (!$bPreview)) { - $oObj->DBUpdateTracked($oMyChange); + $oObj->DBUpdate(); } } $oP->Table($aHeaders, $aRows); @@ -1224,7 +1202,6 @@ EOF } else { - $oMyChange = null; $oObj->UpdateObjectFromPostedForm(); if (!$oObj->IsModified()) @@ -1240,12 +1217,7 @@ EOF $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding $oP->add("

".Dict::Format('UI:ModificationTitle_Class_Object', $sClassLabel, $oObj->GetName())."

\n"); - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oObj->DBUpdateTracked($oMyChange); + $oObj->DBUpdate(); utils::RemoveTransaction($sTransactionId); $oP->p(Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName())); @@ -1273,7 +1245,7 @@ EOF $sNextAction = utils::ReadPostedParam('next_action', ''); if (!empty($sNextAction)) { - ApplyNextAction($oP, $oObj, $sNextAction, $oMyChange); + ApplyNextAction($oP, $oObj, $sNextAction); } else { @@ -1386,12 +1358,7 @@ EOF list($bRes, $aIssues) = $oObj->CheckToWrite(); if ($bRes) { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oObj->DBInsertTracked($oMyChange); + $oObj->DBInsert(); utils::RemoveTransaction($sTransactionId); $oP->set_title(Dict::S('UI:PageTitle:ObjectCreated')); $oP->add("

".Dict::Format('UI:Title:Object_Of_Class_Created', $oObj->GetName(), $sClassLabel)."

\n"); @@ -1400,7 +1367,7 @@ EOF $sNextAction = utils::ReadPostedParam('next_action', ''); if (!empty($sNextAction)) { - ApplyNextAction($oP, $oObj, $sNextAction, $oMyChange); + ApplyNextAction($oP, $oObj, $sNextAction); } else { @@ -1440,12 +1407,7 @@ EOF { $sClass = get_class($oObj); $sClassLabel = MetaModel::GetName($sClass); - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oObj->DBInsertTracked($oMyChange); + $oObj->DBInsert(); $oP->set_title(Dict::S('UI:PageTitle:ObjectCreated')); $oP->add("

".Dict::Format('UI:Title:Object_Of_Class_Created', $oObj->GetName(), $sClassLabel)."

\n"); $oObj->DisplayDetails($oP); @@ -1683,11 +1645,6 @@ EOF $oP->add(''); $oSet = DBObjectSet::FromArray($sClass, $aObjects); - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); // For reporting $aHeaders = array( @@ -1743,7 +1700,7 @@ EOF $sStatus = $bResult ? Dict::S('UI:BulkModifyStatusModified') : Dict::S('UI:BulkModifyStatusSkipped'); if ($bResult) { - $oObj->DBUpdateTracked($oMyChange); + $oObj->DBUpdate(); } else { @@ -1982,12 +1939,7 @@ EOF { if ($oObj->ApplyStimulus($sStimulus)) { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oObj->DBUpdateTracked($oMyChange); + $oObj->DBUpdate(); $oP->p(Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName())); } else diff --git a/pages/csvimport.php b/pages/csvimport.php index 3264fb168..441bff18c 100644 --- a/pages/csvimport.php +++ b/pages/csvimport.php @@ -298,15 +298,10 @@ try if (!$bSimulate) { // We're doing it for real, let's create a change - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $sUserString .= ' (CSV)'; - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); + $sUserString = CMDBChange::GetCurrentUserName().' (CSV)'; + CMDBObject::SetTrackInfo($sUserString); - // Todo - simplify that when reworking the change tracking - CMDBObject::SetCurrentChange($oMyChange); + $oMyChange = CMDBObject::GetCurrentChange(); } $oBulk = new BulkChange( diff --git a/pages/preferences.php b/pages/preferences.php index bd96c1d67..56ea5971d 100644 --- a/pages/preferences.php +++ b/pages/preferences.php @@ -232,12 +232,7 @@ try $sLangCode = utils::ReadParam('language', 'EN US'); $oUser = UserRights::GetUserObject(); $oUser->Set('language', $sLangCode); - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oUser->DBUpdateTracked($oMyChange); + $oUser->DBUpdate(); // Redirect to force a reload/display of the page with the new language $oAppContext = new ApplicationContext(); $sURL = utils::GetAbsoluteUrlAppRoot().'pages/preferences.php?'.$oAppContext->GetForLink(); diff --git a/pages/schema.php b/pages/schema.php index ee717836e..50d4dd55e 100644 --- a/pages/schema.php +++ b/pages/schema.php @@ -378,12 +378,19 @@ function DisplayClassDetails($oPage, $sClass, $sContext) $oPage->AddTabContainer('details'); $oPage->SetCurrentTabContainer('details'); // List the attributes of the object + $aForwardChangeTracking = MetaModel::GetTrackForwardExternalKeys($sClass); $aDetails = array(); foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) { if ($oAttDef->IsExternalKey()) { $sValue = Dict::Format('UI:Schema:ExternalKey_To',MakeClassHLink($oAttDef->GetTargetClass(), $sContext)); + if (array_key_exists($sAttCode, $aForwardChangeTracking)) + { + $oLinkSet = $aForwardChangeTracking[$sAttCode]; + $sRemoteClass = $oLinkSet->GetHostClass(); + $sValue = $sValue."*"; + } } elseif ($oAttDef->IsLinkSet()) { diff --git a/portal/index.php b/portal/index.php index 2409ccb84..f03c5a9f3 100644 --- a/portal/index.php +++ b/portal/index.php @@ -356,12 +356,7 @@ function DoCreateRequest($oP, $oUserOrg) list($bRes, $aIssues) = $oRequest->CheckToWrite(); if ($bRes) { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oRequest->DBInsertTracked($oMyChange); + $oRequest->DBInsert(); $oP->add("

".Dict::Format('UI:Title:Object_Of_Class_Created', $oRequest->GetName(), MetaModel::GetName(get_class($oRequest)))."

\n"); //DisplayObject($oP, $oRequest, $oUserOrg); diff --git a/setup/ajax.dataloader.php b/setup/ajax.dataloader.php index fb0c518f2..3ea53ac7c 100644 --- a/setup/ajax.dataloader.php +++ b/setup/ajax.dataloader.php @@ -303,13 +303,6 @@ try $aPredefinedObjects = call_user_func(array($sClass, 'GetPredefinedObjects')); if ($aPredefinedObjects != null) { - // Temporary... until this get really encapsulated as the default and transparent behavior - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - // Create/Delete/Update objects of this class, // according to the given constant values // @@ -324,12 +317,12 @@ try { $oObj->Set($sAttCode, $value); } - $oObj->DBUpdateTracked($oMyChange); + $oObj->DBUpdate(); $aDBIds[$oObj->GetKey()] = true; } else { - $oObj->DBDeleteTracked($oMyChange); + $oObj->DBDelete(); } } foreach ($aPredefinedObjects as $iRefId => $aObjValues) @@ -342,7 +335,7 @@ try { $oNewObj->Set($sAttCode, $value); } - $oNewObj->DBInsertTracked($oMyChange); + $oNewObj->DBInsert(); } } } diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index 4b430eb93..ccb7a7d32 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -516,13 +516,6 @@ class ApplicationInstaller if ($aPredefinedObjects != null) { SetupPage::log_info("$sClass::GetPredefinedObjects() returned ".count($aPredefinedObjects)." elements."); - - // Temporary... until this get really encapsulated as the default and transparent behavior - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); // Create/Delete/Update objects of this class, // according to the given constant values @@ -538,12 +531,12 @@ class ApplicationInstaller { $oObj->Set($sAttCode, $value); } - $oObj->DBUpdateTracked($oMyChange); + $oObj->DBUpdate(); $aDBIds[$oObj->GetKey()] = true; } else { - $oObj->DBDeleteTracked($oMyChange); + $oObj->DBDelete(); } } foreach ($aPredefinedObjects as $iRefId => $aObjValues) @@ -556,7 +549,7 @@ class ApplicationInstaller { $oNewObj->Set($sAttCode, $value); } - $oNewObj->DBInsertTracked($oMyChange); + $oNewObj->DBInsert(); } } } @@ -622,12 +615,12 @@ class ApplicationInstaller $oDataLoader = new XMLDataLoader(); - $oChange = MetaModel::NewObject("CMDBChange"); - $oChange->Set("date", time()); - $oChange->Set("userinfo", "Initialization"); - $iChangeId = $oChange->DBInsert(); + + CMDBObject::SetTrackInfo("Initialization"); + $oMyChange = CMDBObject::GetCurrentChange(); + SetupPage::log_info("starting data load session"); - $oDataLoader->StartSession($oChange); + $oDataLoader->StartSession($oMyChange); $aFiles = array(); $oProductionEnv = new RunTimeEnvironment(); diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 87f4af56d..2f9a88a35 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -288,6 +288,27 @@ EOF; return $sRes; } + /** + * Helper to format the tracking level for linkset (direct or indirect attributes) + * @param string $sTrackingLevel Value set from within the XML + * Returns string PHP flag + */ + protected function TrackingLevelToPHP($sTrackingLevel) + { + static $aXmlToPHP = array( + 'none' => 'LINKSET_TRACKING_NONE', + 'list' => 'LINKSET_TRACKING_LIST', + 'details' => 'LINKSET_TRACKING_DETAILS', + 'all' => 'LINKSET_TRACKING_ALL', + ); + + if (!array_key_exists($sTrackingLevel, $aXmlToPHP)) + { + throw new exception("Tracking level: unknown value '$sTrackingLevel'"); + } + return $aXmlToPHP[$sTrackingLevel]; + } + /** * Format a path (file or url) as an absolute path or relative to the module or the app @@ -542,21 +563,29 @@ EOF; $aParameters['linked_class'] = $this->GetPropString($oField, 'linked_class', ''); $aParameters['ext_key_to_me'] = $this->GetPropString($oField, 'ext_key_to_me', ''); $aParameters['ext_key_to_remote'] = $this->GetPropString($oField, 'ext_key_to_remote', ''); - // todo - utile ? $aParameters['allowed_values'] = 'null'; $aParameters['count_min'] = $this->GetPropNumber($oField, 'count_min', 0); $aParameters['count_max'] = $this->GetPropNumber($oField, 'count_max', 0); $aParameters['duplicates'] = $this->GetPropBoolean($oField, 'duplicates', false); + $sTrackingLevel = $oField->GetChildText('tracking_level'); + if (!is_null($sTrackingLevel)) + { + $aParameters['tracking_level'] = $this->TrackingLevelToPHP($sTrackingLevel); + } $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeLinkedSet') { $aParameters['linked_class'] = $this->GetPropString($oField, 'linked_class', ''); $aParameters['ext_key_to_me'] = $this->GetPropString($oField, 'ext_key_to_me', ''); - // todo - utile ? $aParameters['allowed_values'] = 'null'; $aParameters['count_min'] = $this->GetPropNumber($oField, 'count_min', 0); $aParameters['count_max'] = $this->GetPropNumber($oField, 'count_max', 0); + $sTrackingLevel = $oField->GetChildText('tracking_level'); + if (!is_null($sTrackingLevel)) + { + $aParameters['tracking_level'] = $this->TrackingLevelToPHP($sTrackingLevel); + } $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeExternalKey') diff --git a/webservices/import.php b/webservices/import.php index 0c1aec625..9760d8e82 100644 --- a/webservices/import.php +++ b/webservices/import.php @@ -636,19 +636,17 @@ try } else { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); if (strlen($sComment) > 0) { - $sMoreInfo = 'Web Service (CSV) - '.$sComment; + $sMoreInfo = CMDBChange::GetCurrentUserName().', Web Service (CSV) - '.$sComment; } else { - $sMoreInfo = 'Web Service (CSV)'; + $sMoreInfo = CMDBChange::GetCurrentUserName().', Web Service (CSV)'; } - $oMyChange->Set("userinfo", $sUserString.', '.$sMoreInfo); - $iChangeId = $oMyChange->DBInsert(); + CMDBChange::SetTrackInfo($sMoreInfo); + + $oMyChange = CMDBObject::GetCurrentChange(); } $aRes = $oBulk->Process($oMyChange);