Преглед изворни кода

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

+ 4 - 10
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())

+ 1 - 6
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 ?
 		//

+ 1 - 6
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

+ 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.) 
  *
  * @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 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);
+	}
 }
 
 /**

+ 186 - 0
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;
+	}
+}
 ?>

+ 77 - 115
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;
 	}
 
+	/**
+	 * 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->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;
 	}

+ 171 - 7
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...
 	}
-}
 
+}
 
 ?>

+ 7 - 2
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
 }
 
 
-?>
+?>

+ 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)
 	{
 		$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 '<span class="itop-deleted-object" title="'.htmlentities($sTitle, ENT_QUOTES, 'UTF-8').'">'.htmlentities($sName, ENT_QUOTES, 'UTF-8').'</span>';
 		}
 		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())
 		{

+ 3 - 6
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*/);
 							}
 

+ 3 - 0
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;
 }

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

@@ -2621,6 +2621,10 @@
           <code><![CDATA[	protected function UpdateConnectedInterface()
 	{
 		$iPrevTargetIf = $this->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();
 			}
 		}
 	}]]></code>

+ 9 - 0
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',
 ));
 
 //

+ 7 - 0
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 :<br/>
 	'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',

+ 11 - 59
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("<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);
 			
 						$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("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oObj->GetName(), $sClassLabel)."</h1>\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("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oObj->GetName(), $sClassLabel)."</h1>\n");
 				$oObj->DisplayDetails($oP);
@@ -1683,11 +1645,6 @@ EOF
 			$oP->add('</div>');
 			
 			$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

+ 3 - 8
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(

+ 1 - 6
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();

+ 7 - 0
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."<span title=\"Forward changes to $sRemoteClass\">*</span>";
+			}
 		}
 		elseif ($oAttDef->IsLinkSet())
 		{

+ 1 - 6
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("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oRequest->GetName(), MetaModel::GetName(get_class($oRequest)))."</h1>\n");
 
 		//DisplayObject($oP, $oRequest, $oUserOrg);

+ 3 - 10
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();
 					}
 				}
 			}

+ 8 - 15
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();

+ 31 - 2
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')

+ 5 - 7
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);