浏览代码

#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).

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2236 a333f486-631f-4898-b8df-5754b55c2be0

romainq 12 年之前
父节点
当前提交
60da6859a4

+ 3 - 5
addons/userrights/userrightsprofile.class.inc.php

@@ -311,11 +311,9 @@ class UserRightsProfile extends UserRightsAddOnAPI
 	// Installation: create the very first user
 	// Installation: create the very first user
 	public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US')
 	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;
 		$iContactId = 0;
 		// Support drastic data model changes: no organization class (or not writable)!
 		// Support drastic data model changes: no organization class (or not writable)!

+ 4 - 10
application/cmdbabstract.class.inc.php

@@ -2487,7 +2487,7 @@ EOF
 		// Invoke extensions after insertion (the object must exist, have an id, etc.)
 		// Invoke extensions after insertion (the object must exist, have an id, etc.)
 		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
 		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
 		{
 		{
-			$oExtensionInstance->OnDBInsert($this, self::$m_oCurrChange);
+			$oExtensionInstance->OnDBInsert($this, self::GetCurrentChange());
 		}
 		}
 
 
 		return $res;
 		return $res;
@@ -2500,7 +2500,7 @@ EOF
 		// Invoke extensions after insertion (the object must exist, have an id, etc.)
 		// Invoke extensions after insertion (the object must exist, have an id, etc.)
 		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
 		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
 		{
 		{
-			$oExtensionInstance->OnDBInsert($oNewObj, self::$m_oCurrChange);
+			$oExtensionInstance->OnDBInsert($oNewObj, self::GetCurrentChange());
 		}
 		}
 		return $oNewObj;
 		return $oNewObj;
 	}
 	}
@@ -2512,7 +2512,7 @@ EOF
 		// Invoke extensions after the update (could be before)
 		// Invoke extensions after the update (could be before)
 		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
 		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
 		{
 		{
-			$oExtensionInstance->OnDBUpdate($this, self::$m_oCurrChange);
+			$oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
 		}
 		}
 		return $res;
 		return $res;
 	}
 	}
@@ -2528,18 +2528,12 @@ EOF
 		// Invoke extensions before the deletion (the deletion will do some cleanup and we might loose some information
 		// Invoke extensions before the deletion (the deletion will do some cleanup and we might loose some information
 		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
 		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
 		{
 		{
-			$oExtensionInstance->OnDBDelete($this, self::$m_oCurrChange);
+			$oExtensionInstance->OnDBDelete($this, self::GetCurrentChange());
 		}
 		}
 
 
 		return parent::DBDeleteTracked_Internal($oDeletionPlan);
 		return parent::DBDeleteTracked_Internal($oDeletionPlan);
 	}
 	}
 
 
-	protected static function BulkDeleteTracked_Internal(DBObjectSearch $oFilter)
-	{
-		// Todo - invoke the extension
-		return parent::BulkDeleteTracked_Internal($oFilter);
-	}
-
 	public function IsModified()
 	public function IsModified()
 	{
 	{
 		if (parent::IsModified())
 		if (parent::IsModified())

+ 1 - 6
application/portalwebpage.class.inc.php

@@ -658,12 +658,7 @@ EOF
 		
 		
 		// Record the change
 		// 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 ?
 		// Trigger ?
 		//
 		//

+ 1 - 6
application/ui.extkeywidget.class.inc.php

@@ -485,12 +485,7 @@ EOF
 		$aErrors = $oObj->UpdateObjectFromPostedForm($this->iId);
 		$aErrors = $oObj->UpdateObjectFromPostedForm($this->iId);
 		if (count($aErrors) == 0)
 		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());
 			return array('name' => $oObj->GetName(), 'id' => $oObj->GetKey());
 		}
 		}
 		else
 		else

+ 22 - 0
core/attributedef.class.inc.php

@@ -76,6 +76,18 @@ 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.) 
  * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.) 
  *
  *
  * @package     iTopORM
  * @package     iTopORM
@@ -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 GetLinkedClass() {return $this->Get('linked_class');}
 	public function GetExtKeyToMe() {return $this->Get('ext_key_to_me');}
 	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 GetExtKeyToRemote() { return $this->Get('ext_key_to_remote'); }
 	public function GetEditClass() {return "LinkedSet";}
 	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 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);
+	}
 }
 }
 
 
 /**
 /**

+ 186 - 0
core/cmdbchangeop.class.inc.php

@@ -64,6 +64,19 @@ class CMDBChangeOp extends DBObject
 	{
 	{
 		return '';
 		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_Params($aParams);
 		MetaModel::Init_InheritAttributes();
 		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
 	 * Describe (as a text string) the modifications corresponding to this change
@@ -535,4 +553,172 @@ class CMDBChangeOpPlugin extends CMDBChangeOp
 		return $this->Get('description');
 		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;
+	}
+}
 ?>
 ?>

+ 77 - 115
core/cmdbobject.class.inc.php

@@ -91,8 +91,11 @@ abstract class CMDBObject extends DBObject
 	protected $m_datUpdated;
 	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
 	// 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_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)
 	public static function SetCurrentChange(CMDBChange $oChange)
 	{
 	{
 		self::$m_oCurrChange = $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
 	// 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 be called ONCE (!) by CMDBChangeOp::OnInsert ($this->Set('change', ..GetCurrentChange())
 	//			GetCurrentChange to create a default change if not already done in the current context
 	//			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;
 		return self::$m_oCurrChange;
 	}
 	}
 
 
+	/**
+	 * 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();
+	}
 
 
-	private function RecordObjCreation(CMDBChange $oChange)
+	protected function RecordObjCreation()
 	{
 	{
+		parent::RecordObjCreation();
 		$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpCreate");
 		$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpCreate");
-		$oMyChangeOp->Set("change", $oChange->GetKey());
 		$oMyChangeOp->Set("objclass", get_class($this));
 		$oMyChangeOp->Set("objclass", get_class($this));
 		$oMyChangeOp->Set("objkey", $this->GetKey());
 		$oMyChangeOp->Set("objkey", $this->GetKey());
 		$iId = $oMyChangeOp->DBInsertNoReload();
 		$iId = $oMyChangeOp->DBInsertNoReload();
 	}
 	}
-	private function RecordObjDeletion(CMDBChange $oChange, $objkey)
+
+	protected function RecordObjDeletion($objkey)
 	{
 	{
+		parent::RecordObjDeletion($objkey);
 		$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpDelete");
 		$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("objkey", $objkey);
+		$oMyChangeOp->Set("fclass", get_class($this));
+		$oMyChangeOp->Set("fname", $this->GetRawName());
 		$iId = $oMyChangeOp->DBInsertNoReload();
 		$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
 		// $aValues is an array of $sAttCode => $value
 		//
 		//
 		foreach ($aValues as $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
 				// One Way encrypted passwords' history is stored -one way- encrypted
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeOneWayPassword");
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeOneWayPassword");
-				$oMyChangeOp->Set("change", $oChange->GetKey());
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
@@ -165,7 +219,6 @@ abstract class CMDBObject extends DBObject
 			{
 			{
 				// Encrypted string history is stored encrypted
 				// Encrypted string history is stored encrypted
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeEncrypted");
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeEncrypted");
-				$oMyChangeOp->Set("change", $oChange->GetKey());
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
@@ -181,7 +234,6 @@ abstract class CMDBObject extends DBObject
 			{
 			{
 				// Data blobs
 				// Data blobs
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob");
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob");
-				$oMyChangeOp->Set("change", $oChange->GetKey());
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
@@ -209,7 +261,6 @@ abstract class CMDBObject extends DBObject
 					if ($item_value != $item_original)
 					if ($item_value != $item_original)
 					{
 					{
 						$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
 						$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
-						$oMyChangeOp->Set("change", $oChange->GetKey());
 						$oMyChangeOp->Set("objclass", get_class($this));
 						$oMyChangeOp->Set("objclass", get_class($this));
 						$oMyChangeOp->Set("objkey", $this->GetKey());
 						$oMyChangeOp->Set("objkey", $this->GetKey());
 						$oMyChangeOp->Set("attcode", $sSubItemAttCode);
 						$oMyChangeOp->Set("attcode", $sSubItemAttCode);
@@ -223,7 +274,6 @@ abstract class CMDBObject extends DBObject
 			elseif ($oAttDef instanceOf AttributeCaseLog)
 			elseif ($oAttDef instanceOf AttributeCaseLog)
 			{
 			{
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCaseLog");
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCaseLog");
-				$oMyChangeOp->Set("change", $oChange->GetKey());
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
@@ -235,7 +285,6 @@ abstract class CMDBObject extends DBObject
 			{
 			{
 				// Data blobs
 				// Data blobs
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText");
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText");
-				$oMyChangeOp->Set("change", $oChange->GetKey());
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
@@ -252,7 +301,6 @@ abstract class CMDBObject extends DBObject
 				// Scalars
 				// Scalars
 				//
 				//
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
 				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
-				$oMyChangeOp->Set("change", $oChange->GetKey());
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
@@ -294,32 +342,24 @@ abstract class CMDBObject extends DBObject
 
 
 	public function DBInsert()
 	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();
 		return $this->DBInsertTracked_Internal();
 	}
 	}
 
 
 	public function DBInsertTracked(CMDBChange $oChange, $bSkipStrongSecurity = null)
 	public function DBInsertTracked(CMDBChange $oChange, $bSkipStrongSecurity = null)
 	{
 	{
+		self::SetCurrentChange($oChange);
 		$this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY);
 		$this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY);
 
 
-		$oPreviousChange = self::$m_oCurrChange;
-		self::$m_oCurrChange = $oChange;
 		$ret = $this->DBInsertTracked_Internal();
 		$ret = $this->DBInsertTracked_Internal();
-		self::$m_oCurrChange = $oPreviousChange;
 		return $ret;
 		return $ret;
 	}
 	}
 
 
 	public function DBInsertTrackedNoReload(CMDBChange $oChange, $bSkipStrongSecurity = null)
 	public function DBInsertTrackedNoReload(CMDBChange $oChange, $bSkipStrongSecurity = null)
 	{
 	{
+		self::SetCurrentChange($oChange);
 		$this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY);
 		$this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY);
 
 
-		$oPreviousChange = self::$m_oCurrChange;
-		self::$m_oCurrChange = $oChange;
 		$ret = $this->DBInsertTracked_Internal(true);
 		$ret = $this->DBInsertTracked_Internal(true);
-		self::$m_oCurrChange = $oPreviousChange;
 		return $ret;
 		return $ret;
 	}
 	}
 
 
@@ -333,25 +373,18 @@ abstract class CMDBObject extends DBObject
 		{
 		{
 			$ret = parent::DBInsert();
 			$ret = parent::DBInsert();
 		}
 		}
-		$this->RecordObjCreation(self::$m_oCurrChange);
 		return $ret;
 		return $ret;
 	}
 	}
 
 
 	public function DBClone($newKey = null)
 	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();
 		return $this->DBCloneTracked_Internal();
 	}
 	}
 
 
 	public function DBCloneTracked(CMDBChange $oChange, $newKey = null)
 	public function DBCloneTracked(CMDBChange $oChange, $newKey = null)
 	{
 	{
-		$oPreviousChange = self::$m_oCurrChange;
-		self::$m_oCurrChange = $oChange;
+		self::SetCurrentChange($oChange);
 		$this->DBCloneTracked_Internal($newKey);
 		$this->DBCloneTracked_Internal($newKey);
-		self::$m_oCurrChange = $oPreviousChange;
 	}
 	}
 
 
 	protected function DBCloneTracked_Internal($newKey = null)
 	protected function DBCloneTracked_Internal($newKey = null)
@@ -359,128 +392,57 @@ abstract class CMDBObject extends DBObject
 		$newKey = parent::DBClone($newKey);
 		$newKey = parent::DBClone($newKey);
 		$oClone = MetaModel::GetObject(get_class($this), $newKey); 
 		$oClone = MetaModel::GetObject(get_class($this), $newKey); 
 
 
-		$oClone->RecordObjCreation(self::$m_oCurrChange);
 		return $newKey;
 		return $newKey;
 	}
 	}
 
 
 	public function DBUpdate()
 	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)
 		// Copy the changes list before the update (the list should be reset afterwards)
 		$aChanges = $this->ListChanges();
 		$aChanges = $this->ListChanges();
 		if (count($aChanges) == 0)
 		if (count($aChanges) == 0)
 		{
 		{
-			//throw new CoreWarning("Attempting to update an unchanged object");
 			return;
 			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();
 		$ret = parent::DBUpdate();
-		$this->RecordAttChanges(self::$m_oCurrChange, $aChanges, $aOriginalValues);
 		return $ret;
 		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)
 	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);
 		return $this->DBDeleteTracked_Internal($oDeletionPlan);
 	}
 	}
 
 
 	public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null)
 	public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null)
 	{
 	{
+		self::SetCurrentChange($oChange);
 		$this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_DELETE);
 		$this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_DELETE);
-
-		$oPreviousChange = self::$m_oCurrChange;
-		self::$m_oCurrChange = $oChange;
 		$this->DBDeleteTracked_Internal($oDeletionPlan);
 		$this->DBDeleteTracked_Internal($oDeletionPlan);
-		self::$m_oCurrChange = $oPreviousChange;
 	}
 	}
 
 
 	protected function DBDeleteTracked_Internal(&$oDeletionPlan = null)
 	protected function DBDeleteTracked_Internal(&$oDeletionPlan = null)
 	{
 	{
 		$prevkey = $this->GetKey();
 		$prevkey = $this->GetKey();
 		$ret = parent::DBDelete($oDeletionPlan);
 		$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;
 		return $ret;
 	}
 	}
 
 
 	public static function BulkUpdate(DBObjectSearch $oFilter, array $aValues)
 	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);
 		return $this->BulkUpdateTracked_Internal($oFilter, $aValues);
 	}
 	}
 
 
 	public static function BulkUpdateTracked(CMDBChange $oChange, DBObjectSearch $oFilter, array $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);
 		$this->BulkUpdateTracked_Internal($oFilter, $aValues);
-		self::$m_oCurrChange = $oPreviousChange;
 	}
 	}
 
 
 	protected static function BulkUpdateTracked_Internal(DBObjectSearch $oFilter, array $aValues)
 	protected static function BulkUpdateTracked_Internal(DBObjectSearch $oFilter, array $aValues)
@@ -507,7 +469,7 @@ abstract class CMDBObject extends DBObject
 		while ($oItem = $oObjSet->Fetch())
 		while ($oItem = $oObjSet->Fetch())
 		{
 		{
 			$aChangedValues = $oItem->ListChangedValues($aValues);
 			$aChangedValues = $oItem->ListChangedValues($aValues);
-			$oItem->RecordAttChanges(self::$m_oCurrChange, $aChangedValues, $aOriginalValues[$oItem->GetKey()]);
+			$oItem->RecordAttChanges($aChangedValues, $aOriginalValues[$oItem->GetKey()]);
 		}
 		}
 		return $ret;
 		return $ret;
 	}
 	}

+ 171 - 7
core/dbobject.class.php

@@ -1424,6 +1424,8 @@ abstract class DBObject
 			$oTrigger->DoActivate($this->ToArgs('this'));
 			$oTrigger->DoActivate($this->ToArgs('this'));
 		}
 		}
 
 
+		$this->RecordObjCreation();
+
 		return $this->m_iKey;
 		return $this->m_iKey;
 	}
 	}
 
 
@@ -1434,13 +1436,15 @@ abstract class DBObject
 		return $this->m_iKey;
 		return $this->m_iKey;
 	}
 	}
 	
 	
-	public function DBInsertTracked(CMDBChange $oVoid)
+	public function DBInsertTracked(CMDBChange $oChange)
 	{
 	{
+		CMDBObject::SetCurrentChange($oChange);
 		return $this->DBInsert();
 		return $this->DBInsert();
 	}
 	}
 
 
-	public function DBInsertTrackedNoReload(CMDBChange $oVoid)
+	public function DBInsertTrackedNoReload(CMDBChange $oChange)
 	{
 	{
+		CMDBObject::SetCurrentChange($oChange);
 		return $this->DBInsertNoReload();
 		return $this->DBInsertNoReload();
 	}
 	}
 
 
@@ -1450,7 +1454,9 @@ abstract class DBObject
 	{
 	{
 		$this->m_bIsInDB = false;
 		$this->m_bIsInDB = false;
 		$this->m_iKey = $iNewKey;
 		$this->m_iKey = $iNewKey;
-		return $this->DBInsert();
+		$ret = $this->DBInsert();
+		$this->RecordObjCreation();
+		return $ret;
 	}
 	}
 	
 	
 	/**
 	/**
@@ -1498,7 +1504,7 @@ abstract class DBObject
 		$aChanges = $this->ListChanges();
 		$aChanges = $this->ListChanges();
 		if (count($aChanges) == 0)
 		if (count($aChanges) == 0)
 		{
 		{
-			//throw new CoreWarning("Attempting to update an unchanged object");
+			// Attempting to update an unchanged object
 			return;
 			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()));
 			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;
 		$bHasANewExternalKeyValue = false;
 		$aHierarchicalKeys = array();
 		$aHierarchicalKeys = array();
 		foreach($aChanges as $sAttCode => $valuecurr)
 		foreach($aChanges as $sAttCode => $valuecurr)
@@ -1588,11 +1597,17 @@ abstract class DBObject
 			$this->Reload();
 			$this->Reload();
 		}
 		}
 
 
+		if (count($aChanges) != 0)
+		{
+			$this->RecordAttChanges($aChanges, $aOriginalValues);
+		}
+
 		return $this->m_iKey;
 		return $this->m_iKey;
 	}
 	}
 	
 	
-	public function DBUpdateTracked(CMDBChange $oVoid)
+	public function DBUpdateTracked(CMDBChange $oChange)
 	{
 	{
+		CMDBObject::SetCurrentChange($oChange);
 		return $this->DBUpdate();
 		return $this->DBUpdate();
 	}
 	}
 
 
@@ -1664,6 +1679,8 @@ abstract class DBObject
 
 
 		$this->AfterDelete();
 		$this->AfterDelete();
 
 
+		$this->RecordObjDeletion($this->m_iKey);
+
 		$this->m_bIsInDB = false;
 		$this->m_bIsInDB = false;
 		$this->m_iKey = null;
 		$this->m_iKey = null;
 	}
 	}
@@ -1719,8 +1736,9 @@ abstract class DBObject
 		return $oDeletionPlan;
 		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);
 		$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
 	// Return an empty set for the parent of all
 	public static function GetRelationQueries($sRelCode)
 	public static function GetRelationQueries($sRelCode)
 	{
 	{
@@ -2103,7 +2267,7 @@ abstract class DBObject
 		}
 		}
 		// to be continued...
 		// to be continued...
 	}
 	}
-}
 
 
+}
 
 
 ?>
 ?>

+ 7 - 2
core/dbobjectsearch.class.php

@@ -1126,7 +1126,12 @@ class DBObjectSearch
 		if ($bOQLCacheEnabled && array_key_exists($sQuery, self::$m_aOQLQueries))
 		if ($bOQLCacheEnabled && array_key_exists($sQuery, self::$m_aOQLQueries))
 		{
 		{
 			// hit!
 			// 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);
 		$oOql = new OqlInterpreter($sQuery);
@@ -1288,4 +1293,4 @@ class DBObjectSearch
 }
 }
 
 
 
 
-?>
+?>

+ 47 - 1
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)
 	public static function GetLabel($sClass, $sAttCode)
 	{
 	{
 		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
 		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
@@ -4730,7 +4758,23 @@ abstract class MetaModel
 		$oObj = self::GetObject($sTargetClass, $iKey, false);
 		$oObj = self::GetObject($sTargetClass, $iKey, false);
 		if (is_null($oObj))
 		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 '<span class="itop-deleted-object" title="'.htmlentities($sTitle, ENT_QUOTES, 'UTF-8').'">'.htmlentities($sName, ENT_QUOTES, 'UTF-8').'</span>';
 		}
 		}
 		return $oObj->GetHyperLink();
 		return $oObj->GetHyperLink();
 	}
 	}
@@ -4751,6 +4795,8 @@ abstract class MetaModel
 
 
 	public static function BulkDelete(DBObjectSearch $oFilter)
 	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);
 		$sSQL = self::MakeDeleteQuery($oFilter);
 		if (!self::DBIsReadOnly())
 		if (!self::DBIsReadOnly())
 		{
 		{

+ 3 - 6
core/ormstopwatch.class.inc.php

@@ -429,12 +429,9 @@ class CheckStopWatchThresholds implements iBackgroundProcess
 		
 		
 							if($oObj->IsModified())
 							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*/);
 								$oObj->DBUpdateTracked($oMyChange, true /*skip security*/);
 							}
 							}
 
 

+ 3 - 0
css/light-grey.css

@@ -1368,4 +1368,7 @@ a.summary,  a.summary:hover {
 }
 }
 .sortable_field_list > li.selected {
 .sortable_field_list > li.selected {
 	background: #F6A828;
 	background: #F6A828;
+}
+.itop-deleted-object {
+	text-decoration: line-through;
 }
 }

+ 10 - 22
datamodel/itop-config-mgmt-1.0.0/datamodel.itop-config-mgmt.xml

@@ -2621,6 +2621,10 @@
           <code><![CDATA[	protected function UpdateConnectedInterface()
           <code><![CDATA[	protected function UpdateConnectedInterface()
 	{
 	{
 		$iPrevTargetIf = $this->m_aOrigValues['connected_if']; // The interface this interface was connected to
 		$iPrevTargetIf = $this->m_aOrigValues['connected_if']; // The interface this interface was connected to
+		if ($iPrevTargetIf == $this->Get('connected_if'))
+		{
+			return;
+		}
  			
  			
 		if ($iPrevTargetIf != 0)
 		if ($iPrevTargetIf != 0)
 		{
 		{
@@ -2628,11 +2632,11 @@
 			$oPrevConnectedIf = MetaModel::GetObject('NetworkInterface', $iPrevTargetIf, false);
 			$oPrevConnectedIf = MetaModel::GetObject('NetworkInterface', $iPrevTargetIf, false);
 			if (!is_null($oPrevConnectedIf))
 			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')))
  			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
 				// Connect the remote interface to the current one
 				$oConnIf->Set('connected_if', $this->GetKey());
 				$oConnIf->Set('connected_if', $this->GetKey());
 				$oConnIf->Set('link_type', $sConnLink);
 				$oConnIf->Set('link_type', $sConnLink);
 
 
 				// Need to backup the current change, because it is reset when DBUpdateTracked is complete
 				// 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();
 			}
 			}
 		}
 		}
 	}]]></code>
 	}]]></code>

+ 9 - 0
dictionaries/dictionary.itop.core.php

@@ -24,6 +24,12 @@
  */
  */
 
 
 Dict::Add('EN US', 'English', 'English', array(
 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' => 'Array of objects',
 	'Core:AttributeLinkedSet+' => 'Any kind of objects of the same class or subclass',
 	'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_PreviousValue_OldValue' => '%1$s modified, previous value: %2$s',
 	'Change:AttName_Changed' => '%1$s modified',
 	'Change:AttName_Changed' => '%1$s modified',
 	'Change:AttName_EntryAdded' => '%1$s modified, new entry added.',
 	'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',
 ));
 ));
 
 
 //
 //

+ 7 - 0
dictionaries/fr.dictionary.itop.core.php

@@ -22,6 +22,10 @@
  */
  */
 
 
 Dict::Add('FR FR', 'French', 'Français', array(
 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' => 'email notification',
 	'Class:ActionEmail+' => 'Action: Email notification',
 	'Class:ActionEmail+' => 'Action: Email notification',
 	'Class:ActionEmail/Attribute:test_recipient' => 'Destinataire de test',
 	'Class:ActionEmail/Attribute:test_recipient' => 'Destinataire de test',
@@ -503,6 +507,9 @@ Opérateurs :<br/>
 	'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modifié, ancienne valeur: %2$s',
 	'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modifié, ancienne valeur: %2$s',
 	'Change:AttName_Changed' => '%1$s modifié',
 	'Change:AttName_Changed' => '%1$s modifié',
 	'Change:AttName_EntryAdded' => '%1$s champ modifié, une nouvelle entrée a été ajoutée',
 	'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',
 	'Class:Action+' => 'Action spécifique',
 	'Class:Action+' => 'Action spécifique',
 	'Class:Action/Attribute:name' => 'Nom',
 	'Class:Action/Attribute:name' => 'Nom',

+ 11 - 59
pages/UI.php

@@ -34,15 +34,7 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 	{
 	{
 		if ($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
 		else
 		{
 		{
@@ -363,9 +355,8 @@ EOF
  * @param $oP WebPage The page for the output
  * @param $oP WebPage The page for the output
  * @param $oObj CMDBObject The object to process
  * @param $oObj CMDBObject The object to process
  * @param $sNextAction string The code of the stimulus for the 'action' (i.e. Transition) to apply
  * @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
 	// Here handle the apply stimulus
 	$aTransitions = $oObj->EnumTransitions();
 	$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 all the mandatory fields are already present, just apply the transition silently...
 		if ($oObj->ApplyStimulus($sNextAction))
 		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->Reload();
 		$oObj->DisplayDetails($oP);
 		$oObj->DisplayDetails($oP);
@@ -1013,11 +996,6 @@ EOF
 			{
 			{
 				throw new Exception(Dict::S('UI:Error:ObjectAlreadyUpdated'));
 				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);
 			utils::RemoveTransaction($sTransactionId);
 		}
 		}
 		foreach($aSelectedObj as $iId)
 		foreach($aSelectedObj as $iId)
@@ -1049,7 +1027,7 @@ EOF
 			);
 			);
 			if ($bResult && (!$bPreview))
 			if ($bResult && (!$bPreview))
 			{
 			{
-				$oObj->DBUpdateTracked($oMyChange);
+				$oObj->DBUpdate();
 			}
 			}
 		}
 		}
 		$oP->Table($aHeaders, $aRows);
 		$oP->Table($aHeaders, $aRows);
@@ -1224,7 +1202,6 @@ EOF
 			}
 			}
 			else
 			else
 			{
 			{
-				$oMyChange = null;
 				$oObj->UpdateObjectFromPostedForm();
 				$oObj->UpdateObjectFromPostedForm();
 
 
 				if (!$oObj->IsModified())
 				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->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding
 						$oP->add("<h1>".Dict::Format('UI:ModificationTitle_Class_Object', $sClassLabel, $oObj->GetName())."</h1>\n");
 						$oP->add("<h1>".Dict::Format('UI:ModificationTitle_Class_Object', $sClassLabel, $oObj->GetName())."</h1>\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);
 						utils::RemoveTransaction($sTransactionId);
 			
 			
 						$oP->p(Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()));
 						$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', '');
 				$sNextAction = utils::ReadPostedParam('next_action', '');
 				if (!empty($sNextAction))
 				if (!empty($sNextAction))
 				{
 				{
-					ApplyNextAction($oP, $oObj, $sNextAction, $oMyChange);
+					ApplyNextAction($oP, $oObj, $sNextAction);
 				}
 				}
 				else
 				else
 				{
 				{
@@ -1386,12 +1358,7 @@ EOF
 			list($bRes, $aIssues) = $oObj->CheckToWrite();
 			list($bRes, $aIssues) = $oObj->CheckToWrite();
 			if ($bRes)
 			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);
 				utils::RemoveTransaction($sTransactionId);
 				$oP->set_title(Dict::S('UI:PageTitle:ObjectCreated'));
 				$oP->set_title(Dict::S('UI:PageTitle:ObjectCreated'));
 				$oP->add("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oObj->GetName(), $sClassLabel)."</h1>\n");
 				$oP->add("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oObj->GetName(), $sClassLabel)."</h1>\n");
@@ -1400,7 +1367,7 @@ EOF
 				$sNextAction = utils::ReadPostedParam('next_action', '');
 				$sNextAction = utils::ReadPostedParam('next_action', '');
 				if (!empty($sNextAction))
 				if (!empty($sNextAction))
 				{
 				{
-					ApplyNextAction($oP, $oObj, $sNextAction, $oMyChange);
+					ApplyNextAction($oP, $oObj, $sNextAction);
 				}
 				}
 				else
 				else
 				{
 				{
@@ -1440,12 +1407,7 @@ EOF
 			{
 			{
 				$sClass = get_class($oObj);
 				$sClass = get_class($oObj);
 				$sClassLabel = MetaModel::GetName($sClass);
 				$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->set_title(Dict::S('UI:PageTitle:ObjectCreated'));
 				$oP->add("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oObj->GetName(), $sClassLabel)."</h1>\n");
 				$oP->add("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oObj->GetName(), $sClassLabel)."</h1>\n");
 				$oObj->DisplayDetails($oP);
 				$oObj->DisplayDetails($oP);
@@ -1683,11 +1645,6 @@ EOF
 			$oP->add('</div>');
 			$oP->add('</div>');
 			
 			
 			$oSet = DBObjectSet::FromArray($sClass, $aObjects);
 			$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
 			// For reporting
 			$aHeaders = array(
 			$aHeaders = array(
@@ -1743,7 +1700,7 @@ EOF
 								$sStatus = $bResult ? Dict::S('UI:BulkModifyStatusModified') : Dict::S('UI:BulkModifyStatusSkipped');							
 								$sStatus = $bResult ? Dict::S('UI:BulkModifyStatusModified') : Dict::S('UI:BulkModifyStatusSkipped');							
 								if ($bResult)
 								if ($bResult)
 								{
 								{
-									$oObj->DBUpdateTracked($oMyChange);
+									$oObj->DBUpdate();
 								}
 								}
 								else
 								else
 								{
 								{
@@ -1982,12 +1939,7 @@ EOF
 				{
 				{
 					if ($oObj->ApplyStimulus($sStimulus))
 					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()));
 						$oP->p(Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()));
 					}
 					}
 					else
 					else

+ 3 - 8
pages/csvimport.php

@@ -298,15 +298,10 @@ try
 		if (!$bSimulate)
 		if (!$bSimulate)
 		{
 		{
 			// We're doing it for real, let's create a change
 			// 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(
 		$oBulk = new BulkChange(

+ 1 - 6
pages/preferences.php

@@ -232,12 +232,7 @@ try
 		$sLangCode = utils::ReadParam('language', 'EN US');
 		$sLangCode = utils::ReadParam('language', 'EN US');
 		$oUser = UserRights::GetUserObject();
 		$oUser = UserRights::GetUserObject();
 		$oUser->Set('language', $sLangCode);
 		$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
 		// Redirect to force a reload/display of the page with the new language
 		$oAppContext = new ApplicationContext();
 		$oAppContext = new ApplicationContext();
 		$sURL = utils::GetAbsoluteUrlAppRoot().'pages/preferences.php?'.$oAppContext->GetForLink();
 		$sURL = utils::GetAbsoluteUrlAppRoot().'pages/preferences.php?'.$oAppContext->GetForLink();

+ 7 - 0
pages/schema.php

@@ -378,12 +378,19 @@ function DisplayClassDetails($oPage, $sClass, $sContext)
 	$oPage->AddTabContainer('details');
 	$oPage->AddTabContainer('details');
 	$oPage->SetCurrentTabContainer('details');
 	$oPage->SetCurrentTabContainer('details');
 	// List the attributes of the object
 	// List the attributes of the object
+	$aForwardChangeTracking = MetaModel::GetTrackForwardExternalKeys($sClass);
 	$aDetails = array();
 	$aDetails = array();
 	foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef)
 	foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef)
 	{
 	{
 		if ($oAttDef->IsExternalKey())
 		if ($oAttDef->IsExternalKey())
 		{
 		{
 		   $sValue = Dict::Format('UI:Schema:ExternalKey_To',MakeClassHLink($oAttDef->GetTargetClass(), $sContext));
 		   $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."<span title=\"Forward changes to $sRemoteClass\">*</span>";
+			}
 		}
 		}
 		elseif ($oAttDef->IsLinkSet())
 		elseif ($oAttDef->IsLinkSet())
 		{
 		{

+ 1 - 6
portal/index.php

@@ -356,12 +356,7 @@ function DoCreateRequest($oP, $oUserOrg)
 	list($bRes, $aIssues) = $oRequest->CheckToWrite();
 	list($bRes, $aIssues) = $oRequest->CheckToWrite();
 	if ($bRes)
 	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("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oRequest->GetName(), MetaModel::GetName(get_class($oRequest)))."</h1>\n");
 		$oP->add("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oRequest->GetName(), MetaModel::GetName(get_class($oRequest)))."</h1>\n");
 
 
 		//DisplayObject($oP, $oRequest, $oUserOrg);
 		//DisplayObject($oP, $oRequest, $oUserOrg);

+ 3 - 10
setup/ajax.dataloader.php

@@ -303,13 +303,6 @@ try
 			$aPredefinedObjects = call_user_func(array($sClass, 'GetPredefinedObjects'));
 			$aPredefinedObjects = call_user_func(array($sClass, 'GetPredefinedObjects'));
 			if ($aPredefinedObjects != null)
 			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,
 				// Create/Delete/Update objects of this class,
 				// according to the given constant values
 				// according to the given constant values
 				//
 				//
@@ -324,12 +317,12 @@ try
 						{
 						{
 							$oObj->Set($sAttCode, $value);
 							$oObj->Set($sAttCode, $value);
 						}
 						}
-						$oObj->DBUpdateTracked($oMyChange);
+						$oObj->DBUpdate();
 						$aDBIds[$oObj->GetKey()] = true;
 						$aDBIds[$oObj->GetKey()] = true;
 					}
 					}
 					else
 					else
 					{
 					{
-						$oObj->DBDeleteTracked($oMyChange);
+						$oObj->DBDelete();
 					}
 					}
 				}
 				}
 				foreach ($aPredefinedObjects as $iRefId => $aObjValues)
 				foreach ($aPredefinedObjects as $iRefId => $aObjValues)
@@ -342,7 +335,7 @@ try
 						{
 						{
 							$oNewObj->Set($sAttCode, $value);
 							$oNewObj->Set($sAttCode, $value);
 						}
 						}
-						$oNewObj->DBInsertTracked($oMyChange);
+						$oNewObj->DBInsert();
 					}
 					}
 				}
 				}
 			}
 			}

+ 8 - 15
setup/applicationinstaller.class.inc.php

@@ -516,13 +516,6 @@ class ApplicationInstaller
 			if ($aPredefinedObjects != null)
 			if ($aPredefinedObjects != null)
 			{
 			{
 				SetupPage::log_info("$sClass::GetPredefinedObjects() returned ".count($aPredefinedObjects)." elements.");
 				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,
 				// Create/Delete/Update objects of this class,
 				// according to the given constant values
 				// according to the given constant values
@@ -538,12 +531,12 @@ class ApplicationInstaller
 						{
 						{
 							$oObj->Set($sAttCode, $value);
 							$oObj->Set($sAttCode, $value);
 						}
 						}
-						$oObj->DBUpdateTracked($oMyChange);
+						$oObj->DBUpdate();
 						$aDBIds[$oObj->GetKey()] = true;
 						$aDBIds[$oObj->GetKey()] = true;
 					}
 					}
 					else
 					else
 					{
 					{
-						$oObj->DBDeleteTracked($oMyChange);
+						$oObj->DBDelete();
 					}
 					}
 				}
 				}
 				foreach ($aPredefinedObjects as $iRefId => $aObjValues)
 				foreach ($aPredefinedObjects as $iRefId => $aObjValues)
@@ -556,7 +549,7 @@ class ApplicationInstaller
 						{
 						{
 							$oNewObj->Set($sAttCode, $value);
 							$oNewObj->Set($sAttCode, $value);
 						}
 						}
-						$oNewObj->DBInsertTracked($oMyChange);
+						$oNewObj->DBInsert();
 					}
 					}
 				}
 				}
 			}
 			}
@@ -622,12 +615,12 @@ class ApplicationInstaller
 		
 		
 		
 		
 		$oDataLoader = new XMLDataLoader(); 
 		$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");
 		SetupPage::log_info("starting data load session");
-		$oDataLoader->StartSession($oChange);
+		$oDataLoader->StartSession($oMyChange);
 
 
 		$aFiles = array();		
 		$aFiles = array();		
 		$oProductionEnv = new RunTimeEnvironment();
 		$oProductionEnv = new RunTimeEnvironment();

+ 31 - 2
setup/compiler.class.inc.php

@@ -288,6 +288,27 @@ EOF;
 		return $sRes;
 		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
 	 * 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['linked_class'] = $this->GetPropString($oField, 'linked_class', '');
 				$aParameters['ext_key_to_me'] = $this->GetPropString($oField, 'ext_key_to_me', '');
 				$aParameters['ext_key_to_me'] = $this->GetPropString($oField, 'ext_key_to_me', '');
 				$aParameters['ext_key_to_remote'] = $this->GetPropString($oField, 'ext_key_to_remote', '');
 				$aParameters['ext_key_to_remote'] = $this->GetPropString($oField, 'ext_key_to_remote', '');
-	// todo - utile ?
 				$aParameters['allowed_values'] = 'null';
 				$aParameters['allowed_values'] = 'null';
 				$aParameters['count_min'] = $this->GetPropNumber($oField, 'count_min', 0);
 				$aParameters['count_min'] = $this->GetPropNumber($oField, 'count_min', 0);
 				$aParameters['count_max'] = $this->GetPropNumber($oField, 'count_max', 0);
 				$aParameters['count_max'] = $this->GetPropNumber($oField, 'count_max', 0);
 				$aParameters['duplicates'] = $this->GetPropBoolean($oField, 'duplicates', false);
 				$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;
 				$aParameters['depends_on'] = $sDependencies;
 			}
 			}
 			elseif ($sAttType == 'AttributeLinkedSet')
 			elseif ($sAttType == 'AttributeLinkedSet')
 			{
 			{
 				$aParameters['linked_class'] = $this->GetPropString($oField, 'linked_class', '');
 				$aParameters['linked_class'] = $this->GetPropString($oField, 'linked_class', '');
 				$aParameters['ext_key_to_me'] = $this->GetPropString($oField, 'ext_key_to_me', '');
 				$aParameters['ext_key_to_me'] = $this->GetPropString($oField, 'ext_key_to_me', '');
-	// todo - utile ?
 				$aParameters['allowed_values'] = 'null';
 				$aParameters['allowed_values'] = 'null';
 				$aParameters['count_min'] = $this->GetPropNumber($oField, 'count_min', 0);
 				$aParameters['count_min'] = $this->GetPropNumber($oField, 'count_min', 0);
 				$aParameters['count_max'] = $this->GetPropNumber($oField, 'count_max', 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;
 				$aParameters['depends_on'] = $sDependencies;
 			}
 			}
 			elseif ($sAttType == 'AttributeExternalKey')
 			elseif ($sAttType == 'AttributeExternalKey')

+ 5 - 7
webservices/import.php

@@ -636,19 +636,17 @@ try
 	}
 	}
 	else
 	else
 	{
 	{
-		$oMyChange = MetaModel::NewObject("CMDBChange");
-		$oMyChange->Set("date", time());
-		$sUserString = CMDBChange::GetCurrentUserName();
 		if (strlen($sComment) > 0)
 		if (strlen($sComment) > 0)
 		{
 		{
-			$sMoreInfo = 'Web Service (CSV) - '.$sComment;
+			$sMoreInfo = CMDBChange::GetCurrentUserName().', Web Service (CSV) - '.$sComment;
 		}
 		}
 		else
 		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);
 	$aRes = $oBulk->Process($oMyChange);