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:
Romain Quetiez
2011-04-08 11:29:25 +00:00
parent 037d6cc4ba
commit 8ff40fb4b8
10 changed files with 649 additions and 244 deletions

View File

@@ -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...
}
}