mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-29 21:48:45 +02:00
Deletion: entirely reviewed to take into account the objects being synchronized by an external data source, or the plugins constraints.
SVN:trunk[1194]
This commit is contained in:
@@ -24,6 +24,8 @@
|
||||
*/
|
||||
|
||||
require_once('metamodel.class.php');
|
||||
require_once('deletionplan.class.inc.php');
|
||||
|
||||
|
||||
/**
|
||||
* A persistent object, as defined by the metamodel
|
||||
@@ -49,6 +51,7 @@ abstract class DBObject
|
||||
// The object may have incorrect external keys, then any attempt of reload must be avoided
|
||||
private $m_bCheckStatus = null; // Means: the object has been verified and is consistent with integrity rules
|
||||
// if null, then the check has to be performed again to know the status
|
||||
protected $m_bSecurityIssue = null;
|
||||
protected $m_aCheckIssues = null;
|
||||
protected $m_aDeleteIssues = null;
|
||||
|
||||
@@ -153,7 +156,7 @@ abstract class DBObject
|
||||
protected function Reload()
|
||||
{
|
||||
assert($this->m_bIsInDB);
|
||||
$aRow = MetaModel::MakeSingleRow(get_class($this), $this->m_iKey);
|
||||
$aRow = MetaModel::MakeSingleRow(get_class($this), $this->m_iKey, false/*, $this->m_bAllowAllData*/);
|
||||
if (empty($aRow))
|
||||
{
|
||||
throw new CoreException("Failed to reload object of class '".get_class($this)."', id = ".$this->m_iKey);
|
||||
@@ -671,7 +674,7 @@ abstract class DBObject
|
||||
* @param string $sAttCode The code of the attribute
|
||||
* @return integer Flags: the binary combination of the flags applicable to this attribute
|
||||
*/
|
||||
public function GetAttributeFlags($sAttCode)
|
||||
public function GetAttributeFlags($sAttCode, &$aReasons = array())
|
||||
{
|
||||
$iFlags = 0; // By default (if no life cycle) no flag at all
|
||||
$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
|
||||
@@ -765,6 +768,7 @@ abstract class DBObject
|
||||
$this->DoComputeValues();
|
||||
|
||||
$this->m_aCheckIssues = array();
|
||||
$aChanges = $this->ListChanges();
|
||||
|
||||
foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
|
||||
{
|
||||
@@ -787,6 +791,27 @@ abstract class DBObject
|
||||
// $res contains the error description
|
||||
$this->m_aCheckIssues[] = "Consistency rules not followed: $res";
|
||||
}
|
||||
|
||||
// Synchronization: are we attempting to modify an attribute for which an external source is master?
|
||||
//
|
||||
if ($this->m_bIsInDB && $this->InSyncScope() && (count($aChanges) > 0))
|
||||
{
|
||||
foreach($aChanges as $sAttCode => $value)
|
||||
{
|
||||
$iFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
|
||||
if ($iFlags & OPT_ATT_SLAVE)
|
||||
{
|
||||
// Note: $aReasonInfo['name'] could be reported (the task owning the attribute)
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
|
||||
$sAttLabel = $oAttDef->GetLabel();
|
||||
foreach($aReasons as $aReasonInfo)
|
||||
{
|
||||
// Todo: associate the attribute code with the error
|
||||
$this->m_aCheckIssues[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $sAttLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final public function CheckToWrite()
|
||||
@@ -809,7 +834,7 @@ abstract class DBObject
|
||||
$this->m_bCheckStatus = false;
|
||||
}
|
||||
}
|
||||
return array($this->m_bCheckStatus, $this->m_aCheckIssues);
|
||||
return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue);
|
||||
}
|
||||
|
||||
// check if it is allowed to delete the existing object from the database
|
||||
@@ -817,10 +842,57 @@ abstract class DBObject
|
||||
protected function DoCheckToDelete()
|
||||
{
|
||||
$this->m_aDeleteIssues = array(); // Ok
|
||||
|
||||
if ($this->InSyncScope())
|
||||
{
|
||||
$oReplicaSet = $this->GetMasterReplica();
|
||||
if ($oReplicaSet->Count() > 0)
|
||||
{
|
||||
while($aData = $oReplicaSet->FetchAssoc())
|
||||
{
|
||||
if ($aData['datasource']->GetKey() == SynchroDataSource::GetCurrentTaskId())
|
||||
{
|
||||
// The current task has the right to delete the object
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($aData['replica']->Get('status_dest_creator') != 1)
|
||||
{
|
||||
// The object is not owned by the task
|
||||
continue;
|
||||
}
|
||||
|
||||
$sLink = $aData['datasource']->GetName();
|
||||
$sUserDeletePolicy = $aData['datasource']->Get('user_delete_policy');
|
||||
switch($sUserDeletePolicy)
|
||||
{
|
||||
case 'nobody':
|
||||
$this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink);
|
||||
break;
|
||||
|
||||
case 'administrators':
|
||||
if (!UserRights::IsAdministrator())
|
||||
{
|
||||
$this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'everybody':
|
||||
default:
|
||||
// Ok
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// final public function CheckToDelete() - THE EQUIVALENT OF CheckToWrite IS NOT AVAILABLE
|
||||
// Todo - split the "DeleteObject()" function (UI.php) and move the generic part in cmdbAbstractObject, etc.
|
||||
final public function CheckToDelete(&$oDeletionPlan)
|
||||
{
|
||||
$this->MakeDeletionPlan($oDeletionPlan);
|
||||
$oDeletionPlan->ComputeResults();
|
||||
return (!$oDeletionPlan->FoundStopper());
|
||||
}
|
||||
|
||||
protected function ListChangedValues(array $aProposal)
|
||||
{
|
||||
@@ -1079,7 +1151,8 @@ abstract class DBObject
|
||||
list($bRes, $aIssues) = $this->CheckToWrite();
|
||||
if (!$bRes)
|
||||
{
|
||||
throw new CoreException("Object not following integrity rules - it will not be written into the DB", array('class' => $sClass, 'id' => $this->GetKey(), 'issues' => $aIssues));
|
||||
$sIssues = implode(', ', $aIssues);
|
||||
throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
|
||||
}
|
||||
|
||||
// First query built upon on the root class, because the ID must be created first
|
||||
@@ -1176,7 +1249,8 @@ abstract class DBObject
|
||||
list($bRes, $aIssues) = $this->CheckToWrite();
|
||||
if (!$bRes)
|
||||
{
|
||||
throw new CoreException("Object not following integrity rules - it will not be written into the DB", array('class' => get_class($this), 'id' => $this->GetKey(), 'issues' => $aIssues));
|
||||
$sIssues = implode(', ', $aIssues);
|
||||
throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
|
||||
}
|
||||
|
||||
$bHasANewExternalKeyValue = false;
|
||||
@@ -1245,24 +1319,16 @@ abstract class DBObject
|
||||
CMDBSource::DeleteFrom($sDeleteSQL);
|
||||
}
|
||||
|
||||
private function DBDeleteInternal()
|
||||
{
|
||||
$sClass = get_class($this);
|
||||
|
||||
foreach(MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL) as $sParentClass)
|
||||
{
|
||||
$this->DBDeleteSingleTable($sParentClass);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a record
|
||||
public function DBDelete()
|
||||
private function DBDeleteSingleObject()
|
||||
{
|
||||
$this->OnDelete();
|
||||
|
||||
if (!MetaModel::DBIsReadOnly())
|
||||
{
|
||||
$this->DBDeleteInternal();
|
||||
foreach(MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL) as $sParentClass)
|
||||
{
|
||||
$this->DBDeleteSingleTable($sParentClass);
|
||||
}
|
||||
}
|
||||
|
||||
$this->AfterDelete();
|
||||
@@ -1271,9 +1337,53 @@ abstract class DBObject
|
||||
$this->m_iKey = null;
|
||||
}
|
||||
|
||||
public function DBDeleteTracked(CMDBChange $oVoid)
|
||||
// Delete an object... and guarantee data integrity
|
||||
//
|
||||
public function DBDelete(&$oDeletionPlan = null)
|
||||
{
|
||||
$this->DBDelete();
|
||||
if (is_null($oDeletionPlan))
|
||||
{
|
||||
$oDeletionPlan = new DeletionPlan();
|
||||
}
|
||||
$this->MakeDeletionPlan($oDeletionPlan);
|
||||
$oDeletionPlan->ComputeResults();
|
||||
|
||||
if ($oDeletionPlan->FoundStopper())
|
||||
{
|
||||
$aIssues = $oDeletionPlan->GetIssues();
|
||||
throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues)));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete)
|
||||
{
|
||||
foreach ($aToDelete as $iId => $aData)
|
||||
{
|
||||
$oToDelete = $aData['to_delete'];
|
||||
$oToDelete->DBDeleteSingleObject();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate)
|
||||
{
|
||||
foreach ($aToUpdate as $iId => $aData)
|
||||
{
|
||||
$oToUpdate = $aData['to_reset'];
|
||||
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef)
|
||||
{
|
||||
$oToUpdate->Set($sRemoteExtKey, 0);
|
||||
$oToUpdate->DBUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $oDeletionPlan;
|
||||
}
|
||||
|
||||
public function DBDeleteTracked(CMDBChange $oVoid, $bSkipStrongSecurity = null, &$oDeletionPlan = null)
|
||||
{
|
||||
$this->DBDelete($oDeletionPlan);
|
||||
}
|
||||
|
||||
public function EnumTransitions()
|
||||
@@ -1447,7 +1557,7 @@ abstract class DBObject
|
||||
return $aResults;
|
||||
}
|
||||
|
||||
public function GetReferencingObjects()
|
||||
public function GetReferencingObjects($bAllowAllData = false)
|
||||
{
|
||||
$aDependentObjects = array();
|
||||
$aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this));
|
||||
@@ -1460,6 +1570,10 @@ abstract class DBObject
|
||||
|
||||
$oSearch = new DBObjectSearch($sRemoteClass);
|
||||
$oSearch->AddCondition($sExtKeyAttCode, $this->GetKey(), '=');
|
||||
if ($bAllowAllData)
|
||||
{
|
||||
$oSearch->AllowAllData();
|
||||
}
|
||||
$oSet = new CMDBObjectSet($oSearch);
|
||||
if ($oSet->Count() > 0)
|
||||
{
|
||||
@@ -1473,15 +1587,13 @@ abstract class DBObject
|
||||
return $aDependentObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* $aDeletedObjs = array(); // [class][key] => structure
|
||||
* $aResetedObjs = array(); // [class][key] => object
|
||||
*/
|
||||
public function GetDeletionScheme(&$aDeletedObjs, &$aResetedObjs, $aVisited = array())
|
||||
private function MakeDeletionPlan(&$oDeletionPlan, $aVisited = array(), $iDeleteOption = null)
|
||||
{
|
||||
$sClass = get_class($this);
|
||||
$iThisId = $this->GetKey();
|
||||
|
||||
$iDeleteOption = $oDeletionPlan->AddToDelete($this, $iDeleteOption);
|
||||
|
||||
if (array_key_exists($sClass, $aVisited))
|
||||
{
|
||||
if (in_array($iThisId, $aVisited[$sClass]))
|
||||
@@ -1491,13 +1603,16 @@ abstract class DBObject
|
||||
}
|
||||
$aVisited[$sClass] = $iThisId;
|
||||
|
||||
$aDeletedObjs[$sClass][$iThisId]['to_delete'] = $this;
|
||||
$aDeletedObjs[$sClass][$iThisId]['auto_delete'] = true;
|
||||
if ($iDeleteOption == DEL_MANUAL)
|
||||
{
|
||||
// Stop the recursion here
|
||||
return;
|
||||
}
|
||||
// Check the node itself
|
||||
$this->DoCheckToDelete();
|
||||
$aDeletedObjs[$sClass][$iThisId]['issues'] = $this->m_aDeleteIssues;
|
||||
$oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue);
|
||||
|
||||
$aDependentObjects = $this->GetReferencingObjects();
|
||||
$aDependentObjects = $this->GetReferencingObjects(true /* allow all data */);
|
||||
foreach ($aDependentObjects as $sRemoteClass => $aPotentialDeletes)
|
||||
{
|
||||
foreach ($aPotentialDeletes as $sRemoteExtKey => $aData)
|
||||
@@ -1512,44 +1627,12 @@ abstract class DBObject
|
||||
if ($oAttDef->IsNullAllowed())
|
||||
{
|
||||
// Optional external key, list to reset
|
||||
if (!array_key_exists($sRemoteClass, $aResetedObjs) || !array_key_exists($iId, $aResetedObjs[$sRemoteClass]))
|
||||
{
|
||||
$aResetedObjs[$sRemoteClass][$iId]['to_reset'] = $oDependentObj;
|
||||
}
|
||||
$aResetedObjs[$sRemoteClass][$iId]['attributes'][$sRemoteExtKey] = $oAttDef;
|
||||
$oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mandatory external key, list to delete
|
||||
if (array_key_exists($sRemoteClass, $aDeletedObjs) && array_key_exists($iId, $aDeletedObjs[$sRemoteClass]))
|
||||
{
|
||||
$iCurrentOption = $aDeletedObjs[$sRemoteClass][$iId];
|
||||
if ($iCurrentOption == DEL_AUTO)
|
||||
{
|
||||
// be conservative, take the new option
|
||||
// (DEL_MANUAL has precedence over DEL_AUTO)
|
||||
$aDeletedObjs[$sRemoteClass][$iId]['auto_delete'] = ($iDeletePropagationOption == DEL_AUTO);
|
||||
}
|
||||
else
|
||||
{
|
||||
// DEL_MANUAL... leave it as is, it HAS to be verified anyway
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// First time we find the given object in the list
|
||||
// (and most likely case is that no other occurence will be found)
|
||||
if ($iDeletePropagationOption == DEL_AUTO)
|
||||
{
|
||||
// Recursively inspect this object
|
||||
$oDependentObj->GetDeletionScheme($aDeletedObjs, $aResetedObjs, $aVisited);
|
||||
}
|
||||
else
|
||||
{
|
||||
$aDeletedObjs[$sRemoteClass][$iId]['to_delete'] = $oDependentObj;
|
||||
$aDeletedObjs[$sRemoteClass][$iId]['auto_delete'] = false;
|
||||
}
|
||||
}
|
||||
$oDependentObj->MakeDeletionPlan($oDeletionPlan, $aVisited, $iDeletePropagationOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1565,10 +1648,10 @@ abstract class DBObject
|
||||
{
|
||||
if ($this->m_oMasterReplicaSet == null)
|
||||
{
|
||||
$aParentClasses = MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL);
|
||||
$sClassesList = "'".implode("','", $aParentClasses)."'";
|
||||
$sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE datasource.scope_class IN ($sClassesList) AND replica.dest_id = :dest_id";
|
||||
$oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_id' => $this->GetKey()));
|
||||
//$aParentClasses = MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL);
|
||||
//$sClassesList = "'".implode("','", $aParentClasses)."'";
|
||||
$sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id";
|
||||
$oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey()));
|
||||
$this->m_oMasterReplicaSet = $oReplicaSet;
|
||||
}
|
||||
else
|
||||
@@ -1584,6 +1667,11 @@ abstract class DBObject
|
||||
$oSet = $this->GetMasterReplica();
|
||||
while($aData = $oSet->FetchAssoc())
|
||||
{
|
||||
if ($aData['datasource']->GetKey() == SynchroDataSource::GetCurrentTaskId())
|
||||
{
|
||||
// Ignore the current task (check to write => ok)
|
||||
continue;
|
||||
}
|
||||
// Assumption: $aData['datasource'] will not be null because the data source id is always set...
|
||||
$oReplica = $aData['replica'];
|
||||
$oSource = $aData['datasource'];
|
||||
@@ -1600,6 +1688,29 @@ abstract class DBObject
|
||||
}
|
||||
return $iFlags;
|
||||
}
|
||||
|
||||
public function InSyncScope()
|
||||
{
|
||||
return true;
|
||||
|
||||
// TODO - FINALIZE THIS OPTIMIZATION
|
||||
//
|
||||
// Optimization: cache the list of Data Sources and classes candidates for synchro
|
||||
//
|
||||
static $aSynchroClasses = null;
|
||||
if (is_null($aSynchroClasses))
|
||||
{
|
||||
$aSynchroClasses = array();
|
||||
$sOQL = "SELECT SynchroDataSource AS datasource";
|
||||
$oSourceSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array());
|
||||
while($oSource = $oSourceSet->Fetch())
|
||||
{
|
||||
$sTarget = $oSource->Get('scope_class');
|
||||
$aSynchroClasses[] = $oSource;
|
||||
}
|
||||
}
|
||||
// to be continued...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user