diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php
index 6daccf162..eb52af98e 100644
--- a/application/cmdbabstract.class.inc.php
+++ b/application/cmdbabstract.class.inc.php
@@ -5354,7 +5354,7 @@ EOF
$aErrors = $oObj->UpdateObjectFromPostedForm('');
$bResult = (count($aErrors) == 0);
if ($bResult) {
- list($bResult, $aErrors) = $oObj->CheckToWrite();
+ [$bResult, $aErrors] = $oObj->CheckToWrite();
}
if ($bPreview) {
$sStatus = $bResult ? Dict::S('UI:BulkModifyStatusOk') : Dict::S('UI:BulkModifyStatusError');
@@ -5958,42 +5958,62 @@ JS
}
/**
- * If the passed object is an instance of a link class, then will register each remote object for modification using {@see static::RegisterObjectAwaitingEventDbLinksChanged()}
+ * Possibility for linked classes to be notified of current class modification
+ *
* If an external key was modified, register also the previous object that was linked previously.
*
- * @throws \ArchivedObjectException
- * @throws \CoreException
- * @throws \Exception
+ * @uses static::RegisterObjectAwaitingEventDbLinksChanged()
*
- * @since 3.1.0 N°5906
+ * @throws ArchivedObjectException
+ * @throws CoreException
+ * @throws Exception
+ *
+ * @since 3.1.0 N°5906 method creation
+ * @since 3.1.1 3.2.0 N°6228 now just notify attributes having `with_php_computation`
*/
final protected function NotifyAttachedObjectsOnLinkClassModification(): void
{
- $sClass = get_class($this);
- if (false === MetaModel::IsLinkClass($sClass)) {
- return;
- }
// previous values in case of link change
$aPreviousValues = $this->ListPreviousValuesForUpdatedAttributes();
+ $sClass = get_class($this);
+ $aClassExtKeyAttCodes = MetaModel::GetAttributesList($sClass, [AttributeExternalKey::class]);
+ foreach ($aClassExtKeyAttCodes as $sExternalKeyAttCode) {
+ /** @var AttributeExternalKey $oAttDef */
+ $oAttDef = MetaModel::GetAttributeDef($sClass, $sExternalKeyAttCode);
- $aLnkClassExternalKeys = MetaModel::GetAttributesList($sClass, [AttributeExternalKey::class]);
- foreach ($aLnkClassExternalKeys as $sExternalKeyAttCode) {
- /** @var \AttributeExternalKey $oExternalKeyAttDef */
- $oExternalKeyAttDef = MetaModel::GetAttributeDef($sClass, $sExternalKeyAttCode);
- $sRemoteClassName = $oExternalKeyAttDef->GetTargetClass();
+ if (false === $this->DoesRemoteObjectHavePhpComputation($oAttDef)) {
+ continue;
+ }
$sRemoteObjectId = $this->Get($sExternalKeyAttCode);
if ($sRemoteObjectId > 0) {
- self::RegisterObjectAwaitingEventDbLinksChanged($sRemoteClassName, $sRemoteObjectId);
+ self::RegisterObjectAwaitingEventDbLinksChanged($oAttDef->GetTargetClass(), $sRemoteObjectId);
}
$sPreviousRemoteObjectId = $aPreviousValues[$sExternalKeyAttCode] ?? 0;
if ($sPreviousRemoteObjectId > 0) {
- self::RegisterObjectAwaitingEventDbLinksChanged($sRemoteClassName, $sPreviousRemoteObjectId);
+ self::RegisterObjectAwaitingEventDbLinksChanged($oAttDef->GetTargetClass(), $sPreviousRemoteObjectId);
}
}
}
+ private function DoesRemoteObjectHavePhpComputation(AttributeExternalKey $oAttDef): bool
+ {
+ $sRemoteObjectClass = $oAttDef->GetTargetClass();
+
+ if (utils::IsNullOrEmptyString($sRemoteObjectClass)) {
+ return false;
+ }
+
+ /** @var AttributeLinkedSet $oAttDefMirrorLink */
+ $oAttDefMirrorLink = $oAttDef->GetMirrorLinkAttribute();
+ if (is_null($oAttDefMirrorLink) || false === $oAttDefMirrorLink->GetHasComputation()){
+ return false;
+ }
+
+ return true;
+ }
+
/**
* Register one object for later EVENT_DB_LINKS_CHANGED event.
*
diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php
index 40797d270..ed3495d78 100644
--- a/core/attributedef.class.inc.php
+++ b/core/attributedef.class.inc.php
@@ -1744,6 +1744,15 @@ class AttributeLinkedSet extends AttributeDefinition
return $this->GetOptional('with_php_constraint', false);
}
+ /**
+ * @return bool true if Attribute has computation (DB_LINKS_CHANGED event propagation, `with_php_computation` attribute xml property), false otherwise
+ * @since 3.1.1 3.2.0 N°6228
+ */
+ public function GetHasComputation()
+ {
+ return $this->GetOptional('with_php_computation', false);
+ }
+
public function GetLinkedClass()
{
return $this->Get('linked_class');
diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php
index b9df6d05f..c9ccb436f 100644
--- a/core/cmdbobject.class.inc.php
+++ b/core/cmdbobject.class.inc.php
@@ -474,7 +474,7 @@ abstract class CMDBObject extends DBObject
public function DBDelete(&$oDeletionPlan = null)
{
$this->LogCRUDEnter(__METHOD__);
- $oDeletionPlan = $this->DBDeleteTracked_Internal($oDeletionPlan);
+ $oDeletionPlan = parent::DBDelete($oDeletionPlan);
$this->LogCRUDExit(__METHOD__);
return $oDeletionPlan;
}
diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml
index a2c798165..112045683 100644
--- a/core/datamodel.core.xml
+++ b/core/datamodel.core.xml
@@ -502,6 +502,12 @@
boolean
false
+
+ with_php_computation
+ false
+ boolean
+ false
+
create_temporary_object
false
diff --git a/core/dbobject.class.php b/core/dbobject.class.php
index 2770566c3..898d8b33f 100644
--- a/core/dbobject.class.php
+++ b/core/dbobject.class.php
@@ -187,12 +187,13 @@ abstract class DBObject implements iDisplay
protected $m_oLinkHostObject = null;
/**
- * @var array List all the CRUD stack in progress
- *
- * The array contains instances of
- * ['type' => 'type of CRUD operation (INSERT, UPDATE, DELETE)',
- * 'class' => 'class of the object in the CRUD process',
- * 'id' => 'id of the object in the CRUD process']
+ * @var array{array{
+ * type: string,
+ * class: string,
+ * id: string,
+ * }} List all the CRUD stack in progress, with :
+ * - type: CRUD operation (INSERT, UPDATE, DELETE)',
+ * - class: class of the object in the CRUD process, leaf (object finalclass) if we have a hierarchy
*
* @since 3.1.0 N°5906
*/
@@ -2461,6 +2462,110 @@ abstract class DBObject implements iDisplay
}
}
+ /**
+ * @since 3.1.1 3.2.0 N°6228 method creation
+ */
+ final protected function CheckPhpConstraint(bool $bIsCheckToDelete = false): void
+ {
+ $aChanges = $this->ListChanges();
+
+ $aClassExtKeyAttCodes = MetaModel::GetAttributesList(get_class($this), [AttributeExternalKey::class]);
+ foreach ($aClassExtKeyAttCodes as $sExtKeyWithMirrorLinkAttCode) {
+ /** @var AttributeExternalKey $oExtKeyWithMirrorLinkAttDef */
+ $oExtKeyWithMirrorLinkAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyWithMirrorLinkAttCode);
+
+ $oRemoteObject = $this->GetRemoteObjectWithPhpConstraint($oExtKeyWithMirrorLinkAttDef, $this->Get($sExtKeyWithMirrorLinkAttCode));
+ if (is_null($oRemoteObject)) {
+ continue;
+ }
+
+ /** @var AttributeLinkedSet $oAttDefMirrorLink */
+ $oAttDefMirrorLink = $oExtKeyWithMirrorLinkAttDef->GetMirrorLinkAttribute();
+ if (is_null($oAttDefMirrorLink)) {
+ continue;
+ }
+ $sAttCodeMirrorLink = $oAttDefMirrorLink->GetCode();
+
+ if ($this->IsNew()) {
+ $this->CheckRemotePhpConstraintOnObject('add', $oRemoteObject, $sAttCodeMirrorLink, false);
+ } else if ($bIsCheckToDelete) {
+ $this->CheckRemotePhpConstraintOnObject('remove', $oRemoteObject, $sAttCodeMirrorLink, true);
+ } else {
+ if (array_key_exists($sExtKeyWithMirrorLinkAttCode, $aChanges)) {
+ // need to update remote old + new
+ $aPreviousValues = $this->ListPreviousValuesForUpdatedAttributes();
+ $sPreviousRemoteObjectKey = $aPreviousValues[$sExtKeyWithMirrorLinkAttCode];
+ $oPreviousRemoteObject = $this->GetRemoteObjectWithPhpConstraint($oExtKeyWithMirrorLinkAttDef, $sPreviousRemoteObjectKey);
+ if (false === is_null($oPreviousRemoteObject)) {
+ $this->CheckRemotePhpConstraintOnObject('remove', $oPreviousRemoteObject, $sAttCodeMirrorLink, false);
+ }
+ $this->CheckRemotePhpConstraintOnObject('add', $oRemoteObject, $sAttCodeMirrorLink, false);
+ } else {
+ $this->CheckRemotePhpConstraintOnObject('modify', $oRemoteObject, $sAttCodeMirrorLink, false); // we need to update remote with current lnk instance
+ }
+ }
+ }
+ }
+
+ private function CheckRemotePhpConstraintOnObject(string $sAction, DBObject $oRemoteObject, string $sAttCodeMirrorLink, bool $bIsCheckToDelete): void
+ {
+ $this->LogCRUDDebug(__METHOD__, "action: $sAction ".get_class($oRemoteObject).'::'.$oRemoteObject->GetKey()." ($sAttCodeMirrorLink)");
+
+ /** @var \ormLinkSet $oRemoteValue */
+ $oRemoteValue = $oRemoteObject->Get($sAttCodeMirrorLink);
+ switch ($sAction) {
+ case 'add':
+ $oRemoteValue->AddItem($this);
+ break;
+ case 'remove':
+ $oRemoteValue->RemoveItem($this->GetKey());
+ break;
+ case 'modify':
+ $oRemoteValue->ModifyItem($this);
+ break;
+ }
+ $oRemoteObject->Set($sAttCodeMirrorLink, $oRemoteValue);
+ [$bCheckStatus, $aCheckIssues, $bSecurityIssue] = $oRemoteObject->CheckToWrite();
+ if (false === $bCheckStatus) {
+ if ($bIsCheckToDelete) {
+ $this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues ?? [], $aCheckIssues);
+ } else {
+ $this->m_aCheckIssues = array_merge($this->m_aCheckIssues ?? [], $aCheckIssues);
+ }
+ $this->m_bSecurityIssue = $this->m_bSecurityIssue || $bSecurityIssue;
+ }
+ $aRemoteCheckWarnings = $oRemoteObject->GetCheckWarnings();
+ if (is_array($aRemoteCheckWarnings)) {
+ $this->m_aCheckWarnings = array_merge($this->m_aCheckWarnings ?? [], $aRemoteCheckWarnings);
+ }
+ }
+
+ private function GetRemoteObjectWithPhpConstraint(AttributeExternalKey $oAttDef, $sRemoteObjectKey)
+ {
+ $sRemoteObjectClass = $oAttDef->GetTargetClass();
+
+ /** @noinspection NotOptimalIfConditionsInspection */
+ /** @noinspection TypeUnsafeComparisonInspection */
+ if (utils::IsNullOrEmptyString($sRemoteObjectClass)
+ || utils::IsNullOrEmptyString($sRemoteObjectKey)
+ || ($sRemoteObjectKey == 0) // non-strict comparison as we might have bad surprises
+ ) {
+ return null;
+ }
+
+ /** @var AttributeLinkedSet $oAttDefMirrorLink */
+ $oAttDefMirrorLink = $oAttDef->GetMirrorLinkAttribute();
+ if (is_null($oAttDefMirrorLink) || false === $oAttDefMirrorLink->GetHasConstraint()) {
+ return null;
+ }
+
+ if (DBObject::IsObjectCurrentlyInCrud($sRemoteObjectClass, $sRemoteObjectKey)) {
+ return null;
+ }
+
+ return MetaModel::GetObject($sRemoteObjectClass, $sRemoteObjectKey, false);
+ }
+
/**
* @api
* @api-advanced
@@ -2484,6 +2589,7 @@ abstract class DBObject implements iDisplay
{
return array(true, array());
}
+
if (is_null($this->m_bCheckStatus))
{
$this->m_aCheckIssues = array();
@@ -2500,6 +2606,9 @@ abstract class DBObject implements iDisplay
$oKPI = new ExecutionKPI();
$this->DoCheckToWrite();
$oKPI->ComputeStatsForExtension($this, 'DoCheckToWrite');
+
+ $this->CheckPhpConstraint();
+
if (count($this->m_aCheckIssues) == 0)
{
$this->m_bCheckStatus = true;
@@ -2509,6 +2618,7 @@ abstract class DBObject implements iDisplay
$this->m_bCheckStatus = false;
}
}
+
return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue);
}
@@ -2661,6 +2771,7 @@ abstract class DBObject implements iDisplay
{
$this->MakeDeletionPlan($oDeletionPlan);
$oDeletionPlan->ComputeResults();
+
return (!$oDeletionPlan->FoundStopper());
}
@@ -3214,7 +3325,7 @@ abstract class DBObject implements iDisplay
}
}
- list($bRes, $aIssues) = $this->CheckToWrite(false);
+ [$bRes, $aIssues] = $this->CheckToWrite(false);
if (!$bRes) {
throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
}
@@ -3454,7 +3565,7 @@ abstract class DBObject implements iDisplay
return $this->m_iKey;
}
- list($bRes, $aIssues) = $this->CheckToWrite(false);
+ [$bRes, $aIssues] = $this->CheckToWrite(false);
if (!$bRes) {
throw new CoreCannotSaveObjectException(['issues' => $aIssues, 'class' => $sClass, 'id' => $this->GetKey()]);
}
@@ -3921,6 +4032,8 @@ abstract class DBObject implements iDisplay
*/
protected function DBDeleteSingleObject()
{
+ $this->LogCRUDEnter(__METHOD__);
+
if (MetaModel::DBIsReadOnly())
{
$this->LogCRUDExit(__METHOD__, 'DB is read-only');
@@ -4043,6 +4156,7 @@ abstract class DBObject implements iDisplay
$this->m_bIsInDB = false;
+ $this->LogCRUDExit(__METHOD__);
// Fix for N°926: do NOT reset m_iKey as it can be used to have it for reporting purposes (see the REST service to delete
// objects, reported as bug N°926)
// Thought the key is not reset, using DBInsert or DBWrite will create an object having the same characteristics and a new ID. DBUpdate is protected
@@ -4073,74 +4187,70 @@ abstract class DBObject implements iDisplay
public function DBDelete(&$oDeletionPlan = null)
{
$this->LogCRUDEnter(__METHOD__);
+ $this->AddCurrentObjectInCrudStack('DELETE');
+ try {
- static $iLoopTimeLimit = null;
- if ($iLoopTimeLimit == null)
- {
- $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
- }
- if (is_null($oDeletionPlan))
- {
- $oDeletionPlan = new DeletionPlan();
- }
- $this->MakeDeletionPlan($oDeletionPlan);
- $oDeletionPlan->ComputeResults();
+ static $iLoopTimeLimit = null;
+ if ($iLoopTimeLimit == null) {
+ $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+ }
+ if (is_null($oDeletionPlan)) {
+ $oDeletionPlan = new DeletionPlan();
+ }
+ $this->MakeDeletionPlan($oDeletionPlan);
+ $oDeletionPlan->ComputeResults();
- if ($oDeletionPlan->FoundStopper())
- {
- $aIssues = $oDeletionPlan->GetIssues();
- $this->LogCRUDError(__METHOD__, ' Errors: '.implode(', ', $aIssues));
- throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues)));
- }
+ if ($oDeletionPlan->FoundStopper()) {
+ $aIssues = $oDeletionPlan->GetIssues();
+ $this->LogCRUDError(__METHOD__, ' Errors: '.implode(', ', $aIssues));
+ throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues)));
+ }
- // Getting and setting time limit are not symetric:
- // www.php.net/manual/fr/function.set-time-limit.php#72305
- $iPreviousTimeLimit = ini_get('max_execution_time');
+ // Getting and setting time limit are not symetric:
+ // www.php.net/manual/fr/function.set-time-limit.php#72305
+ $iPreviousTimeLimit = ini_get('max_execution_time');
- foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete)
- {
- foreach ($aToDelete as $iId => $aData)
- {
- /** @var \DBObject $oToDelete */
- $oToDelete = $aData['to_delete'];
- // The deletion based on a deletion plan should not be done for each object if the deletion plan is common (Trac #457)
- // because for each object we would try to update all the preceding ones... that are already deleted
- // A better approach would be to change the API to apply the DBDelete on the deletion plan itself... just once
- // As a temporary fix: delete only the objects that are still to be deleted...
- if ($oToDelete->m_bIsInDB)
- {
- set_time_limit(intval($iLoopTimeLimit));
+ foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete) {
+ foreach ($aToDelete as $iId => $aData) {
+ /** @var \DBObject $oToDelete */
+ $oToDelete = $aData['to_delete'];
+ // The deletion based on a deletion plan should not be done for each object if the deletion plan is common (Trac #457)
+ // because for each object we would try to update all the preceding ones... that are already deleted
+ // A better approach would be to change the API to apply the DBDelete on the deletion plan itself... just once
+ // As a temporary fix: delete only the objects that are still to be deleted...
+ if ($oToDelete->m_bIsInDB) {
+ set_time_limit(intval($iLoopTimeLimit));
- $oToDelete->AddCurrentObjectInCrudStack('DELETE');
- try {
- $oToDelete->DBDeleteSingleObject();
- }
- finally {
- $oToDelete->RemoveCurrentObjectInCrudStack();
+ $oToDelete->AddCurrentObjectInCrudStack('DELETE');
+ try {
+ $oToDelete->DBDeleteSingleObject();
+ }
+ finally {
+ $oToDelete->RemoveCurrentObjectInCrudStack();
+ }
}
}
}
- }
- foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate)
- {
- foreach ($aToUpdate as $aData)
- {
- $oToUpdate = $aData['to_reset'];
- /** @var \DBObject $oToUpdate */
- foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef)
- {
- $oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
- set_time_limit(intval($iLoopTimeLimit));
- $oToUpdate->DBUpdate();
+ foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
+ foreach ($aToUpdate as $aData) {
+ $oToUpdate = $aData['to_reset'];
+ /** @var \DBObject $oToUpdate */
+ foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
+ $oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
+ set_time_limit(intval($iLoopTimeLimit));
+ $oToUpdate->DBUpdate();
+ }
}
}
+
+ set_time_limit(intval($iPreviousTimeLimit));
+ } finally {
+ $this->LogCRUDExit(__METHOD__);
+ $this->RemoveCurrentObjectInCrudStack();
}
- set_time_limit(intval($iPreviousTimeLimit));
-
- $this->LogCRUDExit(__METHOD__);
return $oDeletionPlan;
}
@@ -5246,6 +5356,7 @@ abstract class DBObject implements iDisplay
$this->m_aDeleteIssues = array(); // Ok
$this->FireEventCheckToDelete($oDeletionPlan);
$this->DoCheckToDelete($oDeletionPlan);
+ $this->CheckPhpConstraint(true);
$oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue);
$aDependentObjects = $this->GetReferencingObjects(true /* allow all data */);
@@ -6236,6 +6347,18 @@ abstract class DBObject implements iDisplay
$this->m_aCheckWarnings[] = $sWarning;
}
+ /**
+ *
+ * @api
+ *
+ * @return string[]|null
+ * @since 3.1.1 3.2.0
+ */
+ public function GetCheckWarnings(): ?array
+ {
+ return $this->m_aCheckWarnings;
+ }
+
/**
* @api
*
@@ -6484,6 +6607,11 @@ abstract class DBObject implements iDisplay
// so we need to handle null values (will give empty string after conversion)
$sConvertedId = (string)$sId;
+ if (((int)$sId) > 0) {
+ // When having a class hierarchy, we are saving the leaf class in the stack
+ $sClass = MetaModel::GetFinalClassName($sClass, $sId);
+ }
+
foreach (self::$m_aCrudStack as $aCrudStackEntry) {
if (($sClass === $aCrudStackEntry['class'])
&& ($sConvertedId === $aCrudStackEntry['id'])) {
@@ -6523,6 +6651,7 @@ abstract class DBObject implements iDisplay
*/
private function AddCurrentObjectInCrudStack(string $sCrudType): void
{
+ $this->LogCRUDDebug(__METHOD__);
self::$m_aCrudStack[] = [
'type' => $sCrudType,
'class' => get_class($this),
@@ -6539,6 +6668,7 @@ abstract class DBObject implements iDisplay
*/
private function UpdateCurrentObjectInCrudStack(): void
{
+ $this->LogCRUDDebug(__METHOD__);
$aCurrentCrudStack = array_pop(self::$m_aCrudStack);
$aCurrentCrudStack['id'] = (string)$this->GetKey();
self::$m_aCrudStack[] = $aCurrentCrudStack;
@@ -6552,7 +6682,8 @@ abstract class DBObject implements iDisplay
*/
private function RemoveCurrentObjectInCrudStack(): void
{
- array_pop(self::$m_aCrudStack);
+ $aRemoved = array_pop(self::$m_aCrudStack);
+ $this->LogCRUDDebug(__METHOD__, $aRemoved['class'].':'.$aRemoved['id']);
}
/**
diff --git a/core/metamodel.class.php b/core/metamodel.class.php
index 9af99c36f..e725c31d5 100644
--- a/core/metamodel.class.php
+++ b/core/metamodel.class.php
@@ -5155,7 +5155,7 @@ abstract class MetaModel
*/
protected static function DBCreateTables($aCallback = null)
{
- list($aErrors, $aSugFix, $aCondensedQueries) = self::DBCheckFormat();
+ [$aErrors, $aSugFix, $aCondensedQueries] = self::DBCheckFormat();
//$sSQL = implode('; ', $aCondensedQueries); Does not work - multiple queries not allowed
foreach($aCondensedQueries as $sQuery)
@@ -5177,7 +5177,7 @@ abstract class MetaModel
*/
protected static function DBCreateViews()
{
- list($aErrors, $aSugFix) = self::DBCheckViews();
+ [$aErrors, $aSugFix] = self::DBCheckViews();
foreach($aSugFix as $sClass => $aTarget)
{
@@ -6926,6 +6926,22 @@ abstract class MetaModel
return $iCount === 1;
}
+ public static function GetFinalClassName(string $sClass, int $iKey): string
+ {
+ $sFinalClassField = Metamodel::DBGetClassField($sClass);
+ if (utils::IsNullOrEmptyString($sFinalClassField)) {
+ return $sClass;
+ }
+
+ $sRootClass = MetaModel::GetRootClass($sClass);
+ $sTable = MetaModel::DBGetTable($sRootClass);
+ $sKeyCol = MetaModel::DBGetKey($sRootClass);
+ $sEscapedKey = CMDBSource::Quote($iKey);
+
+ $sQuery = "SELECT `{$sFinalClassField}` FROM `{$sTable}` WHERE `{$sKeyCol}` = {$sEscapedKey}";
+ return CMDBSource::QueryToScalar($sQuery);
+ }
+
/**
* Search for the specified class and id. If the object is archived it will be returned anyway (this is for pre-2.4
* module compatibility, see N.1108)
diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php
index aea8658e4..a099e96fd 100644
--- a/core/userrights.class.inc.php
+++ b/core/userrights.class.inc.php
@@ -1,6 +1,8 @@
ListChanges())) {
+ return;
+ }
+
+ $oProfileLinkSet = $this->Get('profile_list');
+ if ($oProfileLinkSet->Count() > 1) {
+ return;
+ }
+ $oProfileLinkSet->Rewind();
+ $iPowerPortalCount = 0;
+ $iTotalCount = 0;
+ while ($oUserProfile = $oProfileLinkSet->Fetch()) {
+ $sProfile = $oUserProfile->Get('profile');
+ if ($sProfile === 'Portal power user') {
+ $iPowerPortalCount = 1;
+ }
+ $iTotalCount++;
+ }
+ if ($iTotalCount === $iPowerPortalCount) {
+ $this->AddCheckIssue(Dict::S('Class:User/Error:PortalPowerUserHasInsufficientRights'));
+ }
+ }
+
/**
* @inheritDoc
* @since 3.0.0
diff --git a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
index 1ee54b548..69183e3b4 100755
--- a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
+++ b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
@@ -202,6 +202,7 @@
0
0
contact_id
+ true
@@ -210,6 +211,7 @@
0
0
functionalci_id
+ true
diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php
index 8309affd4..02f940528 100644
--- a/dictionaries/en.dictionary.itop.ui.php
+++ b/dictionaries/en.dictionary.itop.ui.php
@@ -182,6 +182,7 @@ Dict::Add('EN US', 'English', 'English', array(
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User',
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organizations must contain User organization',
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable anymore)',
+ 'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)',
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'At least one organization must be assigned to this user.',
'Class:User/Error:OrganizationNotAllowed' => 'Organization not allowed.',
'Class:User/Error:UserOrganizationNotAllowed' => 'The user account does not belong to your allowed organizations.',
diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php
index 664deeb07..a5a4c7afb 100644
--- a/dictionaries/fr.dictionary.itop.ui.php
+++ b/dictionaries/fr.dictionary.itop.ui.php
@@ -166,6 +166,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
'Class:User/Error:StatusChangeIsNotAllowed' => 'Impossible de changer l\'état de son propre utilisateur',
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Les organisations permises doivent contenir l\'organisation de l\'utilisateur',
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'Les profils existants ne permettent pas de modifier les utilisateurs',
+ 'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'Le profil Portal power user ne donne pas suffisamment de droits à l\'utilisateur (un autre profil doit être ajouté)',
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'L\'utilisateur doit avoir au moins une organisation.',
'Class:User/Error:OrganizationNotAllowed' => 'Organisation non autorisée.',
'Class:User/Error:UserOrganizationNotAllowed' => 'L\'utilisateur n\'appartient pas à vos organisations.',
diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php
index 92e5a9229..f87590766 100644
--- a/setup/compiler.class.inc.php
+++ b/setup/compiler.class.inc.php
@@ -2081,6 +2081,7 @@ EOF
$this->CompileCommonProperty('edit_when', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false);
+ $this->CompileCommonProperty('with_php_computation', $oField, $aParameters, $sModuleRelativeDir, false);
$aParameters['depends_on'] = $sDependencies;
} elseif ($sAttType == 'AttributeLinkedSet') {
$this->CompileCommonProperty('linked_class', $oField, $aParameters, $sModuleRelativeDir);
@@ -2092,6 +2093,7 @@ EOF
$this->CompileCommonProperty('edit_when', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false);
+ $this->CompileCommonProperty('with_php_computation', $oField, $aParameters, $sModuleRelativeDir, false);
$aParameters['depends_on'] = $sDependencies;
} elseif ($sAttType == 'AttributeExternalKey') {
$this->CompileCommonProperty('target_class', $oField, $aParameters, $sModuleRelativeDir);
diff --git a/tests/php-unit-tests/unitary-tests/application/cmdbAbstractObjectTest.php b/tests/php-unit-tests/unitary-tests/application/cmdbAbstractObjectTest.php
index 8c64f17f3..4a727cb45 100644
--- a/tests/php-unit-tests/unitary-tests/application/cmdbAbstractObjectTest.php
+++ b/tests/php-unit-tests/unitary-tests/application/cmdbAbstractObjectTest.php
@@ -1,82 +1,16 @@
GetObjectsAwaitingFireEventDbLinksChanged();
- $this->assertSame([], $aLinkModificationsStack);
-
- // retain events
- cmdbAbstractObject::SetEventDBLinksChangedBlocked(true);
-
- // Create the person
- $oPerson = $this->CreatePerson(1);
- $this->assertIsObject($oPerson);
- // Create the team
- $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
- $oTeam->DBInsert();
- // contact types
- $oContactType1 = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.rand(10000, 99999)]);
- $oContactType1->DBInsert();
- $oContactType2 = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.rand(10000, 99999)]);
- $oContactType2->DBInsert();
-
- // Prepare the link for the insertion with the team
-
- $aValues = [
- 'person_id' => $oPerson->GetKey(),
- 'role_id' => $oContactType1->GetKey(),
- 'team_id' => $oTeam->GetKey(),
- ];
- $oLinkPersonToTeam1 = MetaModel::NewObject(lnkPersonToTeam::class, $aValues);
- $oLinkPersonToTeam1->DBInsert();
-
- $aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
- self::assertCount(3, $aLinkModificationsStack);
- $aExpectedLinkStack = [
- 'Team' => [$oTeam->GetKey() => 1],
- 'Person' => [$oPerson->GetKey() => 1],
- 'ContactType' => [$oContactType1->GetKey() => 1],
- ];
- self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
-
- $oLinkPersonToTeam1->Set('role_id', $oContactType2->GetKey());
- $oLinkPersonToTeam1->DBWrite();
- $aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
- self::assertCount(3, $aLinkModificationsStack);
- $aExpectedLinkStack = [
- 'Team' => [$oTeam->GetKey() => 2],
- 'Person' => [$oPerson->GetKey() => 2],
- 'ContactType' => [
- $oContactType1->GetKey() => 2,
- $oContactType2->GetKey() => 1,
- ],
- ];
- self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
- }
-
public function testProcessClassIdDeferredUpdate()
{
// Create the team
@@ -152,288 +86,4 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
{
$this->SetNonPublicStaticProperty(cmdbAbstractObject::class, 'aObjectsAwaitingEventDbLinksChanged', $aObjects);
}
-
- /**
- * Check that EVENT_DB_LINKS_CHANGED events are not sent to the current updated/created object (Team)
- * the events are sent to the other side (Person)
- *
- * @return void
- * @throws \ArchivedObjectException
- * @throws \CoreCannotSaveObjectException
- * @throws \CoreException
- * @throws \CoreUnexpectedValue
- * @throws \CoreWarning
- * @throws \MySQLException
- * @throws \OQLException
- */
- public function testDBInsertTeam()
- {
- // Prepare the link set
- $sLinkedClass = lnkPersonToTeam::class;
- $aLinkedObjectsArray = [];
- $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray);
- $oLinkSet = new ormLinkSet(Team::class, 'persons_list', $oSet);
-
- // Create the 3 persons
- for ($i = 0; $i < 3; $i++) {
- $oPerson = $this->CreatePerson($i);
- $this->assertIsObject($oPerson);
- // Add the person to the link
- $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey()]);
- $oLinkSet->AddItem($oLink);
- }
-
- $this->debug("\n-------------> Test Starts HERE\n");
-
- $oEventReceiver = new LinksEventReceiver($this);
- $oEventReceiver->RegisterCRUDListeners();
-
- $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'persons_list' => $oLinkSet, 'org_id' => $this->getTestOrgId()]);
- $oTeam->DBInsert();
- $this->assertIsObject($oTeam);
-
- // 3 links added to person + 1 for the Team
- $this->assertEquals(4, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
- }
-
- /**
- * Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when creating a new lnk object
- *
- * @return void
- * @throws \ArchivedObjectException
- * @throws \CoreCannotSaveObjectException
- * @throws \CoreException
- * @throws \CoreUnexpectedValue
- * @throws \CoreWarning
- * @throws \MySQLException
- * @throws \OQLException
- */
- public function testAddLinkToTeam()
- {
- // Create a person
- $oPerson = $this->CreatePerson(1);
- $this->assertIsObject($oPerson);
-
- // Create a Team
- $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
- $oTeam->DBInsert();
- $this->assertIsObject($oTeam);
-
- $this->debug("\n-------------> Test Starts HERE\n");
- $oEventReceiver = new LinksEventReceiver($this);
- $oEventReceiver->RegisterCRUDListeners();
-
- // The link creation will signal both the Person an the Team
- $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
- $oLink->DBInsert();
-
- // 2 events one for Person and One for Team
- $this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
- }
-
- /**
- * Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when updating an existing lnk object
- *
- * @return void
- * @throws \ArchivedObjectException
- * @throws \CoreCannotSaveObjectException
- * @throws \CoreException
- * @throws \CoreUnexpectedValue
- * @throws \CoreWarning
- * @throws \MySQLException
- * @throws \OQLException
- */
- public function testUpdateLinkRole()
- {
- // Create a person
- $oPerson = $this->CreatePerson(1);
- $this->assertIsObject($oPerson);
-
- // Create a Team
- $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
- $oTeam->DBInsert();
- $this->assertIsObject($oTeam);
-
- // Create the link
- $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
- $oLink->DBInsert();
-
- $this->debug("\n-------------> Test Starts HERE\n");
- $oEventReceiver = new LinksEventReceiver($this);
- $oEventReceiver->RegisterCRUDListeners();
-
- // The link update will signal both the Person, the Team and the ContactType
- // Change the role
- $oContactType = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.$oLink->GetKey()]);
- $oContactType->DBInsert();
- $oLink->Set('role_id', $oContactType->GetKey());
- $oLink->DBUpdate();
-
- // 3 events one for Person, one for Team and one for ContactType
- $this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
- }
-
- /**
- * Check that when a link changes from an object to another, then both objects are notified
- *
- * @return void
- * @throws \ArchivedObjectException
- * @throws \CoreCannotSaveObjectException
- * @throws \CoreException
- * @throws \CoreUnexpectedValue
- * @throws \CoreWarning
- * @throws \MySQLException
- * @throws \OQLException
- */
- public function testUpdateLinkPerson()
- {
- // Create 2 person
- $oPerson1 = $this->CreatePerson(1);
- $this->assertIsObject($oPerson1);
-
- $oPerson2 = $this->CreatePerson(2);
- $this->assertIsObject($oPerson2);
-
- // Create a Team
- $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
- $oTeam->DBInsert();
- $this->assertIsObject($oTeam);
-
- // Create the link between Person1 and Team
- $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson1->GetKey(), 'team_id' => $oTeam->GetKey()]);
- $oLink->DBInsert();
-
- $this->debug("\n-------------> Test Starts HERE\n");
- $oEventReceiver = new LinksEventReceiver($this);
- $oEventReceiver->RegisterCRUDListeners();
-
- // The link update will signal both the Persons and the Team
- // Change the person
- $oLink->Set('person_id', $oPerson2->GetKey());
- $oLink->DBUpdate();
-
- // 3 events 2 for Person, one for Team
- $this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
- }
-
- /**
- * Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when deleting an existing lnk object
- *
- * @return void
- * @throws \ArchivedObjectException
- * @throws \CoreCannotSaveObjectException
- * @throws \CoreException
- * @throws \CoreUnexpectedValue
- * @throws \CoreWarning
- * @throws \MySQLException
- * @throws \OQLException
- */
- public function testDeleteLink()
- {
- // Create a person
- $oPerson = $this->CreatePerson(1);
- $this->assertIsObject($oPerson);
-
- // Create a Team
- $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
- $oTeam->DBInsert();
- $this->assertIsObject($oTeam);
-
- // Create the link
- $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
- $oLink->DBInsert();
-
- $this->debug("\n-------------> Test Starts HERE\n");
- $oEventReceiver = new LinksEventReceiver($this);
- $oEventReceiver->RegisterCRUDListeners();
-
- // The link delete will signal both the Person an the Team
- $oLink->DBDelete();
-
- // 3 events one for Person, one for Team
- $this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
- }
-
- /**
- * Debug called by event receivers
- *
- * @param $sMsg
- *
- * @return void
- */
- public static function DebugStatic($sMsg)
- {
- if (static::$DEBUG_UNIT_TEST) {
- if (is_string($sMsg)) {
- echo "$sMsg\n";
- } else {
- print_r($sMsg);
- }
- }
- }
-}
-
-
-/**
- * Count events received
- * And allow callbacks on events
- */
-class LinksEventReceiver
-{
- private $oTestCase;
- private $aCallbacks = [];
-
- public static $bIsObjectInCrudStack;
-
- public function __construct(ItopDataTestCase $oTestCase)
- {
- $this->oTestCase = $oTestCase;
- }
-
- public function AddCallback(string $sEvent, string $sClass, string $sFct, int $iCount = 1): void
- {
- $this->aCallbacks[$sEvent][$sClass] = [
- 'callback' => [$this, $sFct],
- 'count' => $iCount,
- ];
- }
-
- public function CleanCallbacks()
- {
- $this->aCallbacks = [];
- }
-
- // Event callbacks
- public function OnEvent(EventData $oData)
- {
- $sEvent = $oData->GetEvent();
- $oObject = $oData->Get('object');
- $sClass = get_class($oObject);
- $iKey = $oObject->GetKey();
- $this->Debug(__METHOD__.": received event '$sEvent' for $sClass::$iKey");
- cmdbAbstractObjectTest::IncrementCallCount($sEvent);
-
- if (isset($this->aCallbacks[$sEvent][$sClass])) {
- $aCallBack = $this->aCallbacks[$sEvent][$sClass];
- if ($aCallBack['count'] > 0) {
- $this->aCallbacks[$sEvent][$sClass]['count']--;
- call_user_func($this->aCallbacks[$sEvent][$sClass]['callback'], $oObject);
- }
- }
- }
-
- public function RegisterCRUDListeners(string $sEvent = null, $mEventSource = null)
- {
- $this->Debug('Registering Test event listeners');
- if (is_null($sEvent)) {
- $this->oTestCase->EventService_RegisterListener(EVENT_DB_LINKS_CHANGED, [$this, 'OnEvent']);
- return;
- }
- $this->oTestCase->EventService_RegisterListener($sEvent, [$this, 'OnEvent'], $mEventSource);
- }
-
- private function Debug($msg)
- {
- cmdbAbstractObjectTest::DebugStatic($msg);
- }
}
diff --git a/tests/php-unit-tests/unitary-tests/application/query/QueryTest.php b/tests/php-unit-tests/unitary-tests/application/query/QueryTest.php
index b9a887e83..893b2d019 100644
--- a/tests/php-unit-tests/unitary-tests/application/query/QueryTest.php
+++ b/tests/php-unit-tests/unitary-tests/application/query/QueryTest.php
@@ -82,6 +82,7 @@ class QueryTest extends ItopDataTestCase
}
$oQuery->DBInsert();
+ $this->assertFalse($oQuery->IsNew());
return $oQuery;
}
diff --git a/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php b/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php
index 4c96e6e01..418b5fef1 100644
--- a/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php
@@ -208,4 +208,33 @@ PHP
$oFormFieldNoTouchedAtt = $oAttDef->MakeFormField($oPerson);
$this->assertTrue($oFormFieldNoTouchedAtt->IsValidationDisabled(), 'email wasn\'t modified, we must not validate the corresponding field');
}
+
+ /**
+ * @dataProvider WithConstraintParameterProvider
+ *
+ * @param string $sClass
+ * @param string $sAttCode
+ * @param bool $bConstraintExpected
+ * @param bool $bComputationExpected
+ *
+ * @return void
+ * @throws \Exception
+ */
+ public function testWithConstraintAndComputationParameters(string $sClass, string $sAttCode, bool $bConstraintExpected, bool $bComputationExpected)
+ {
+ $oAttDef = \MetaModel::GetAttributeDef($sClass, $sAttCode);
+ $this->assertTrue(method_exists($oAttDef, 'GetHasConstraint'));
+ $this->assertEquals($bConstraintExpected, $oAttDef->GetHasConstraint());
+ $this->assertEquals($bComputationExpected, $oAttDef->GetHasComputation());
+ }
+
+ public function WithConstraintParameterProvider()
+ {
+ return [
+ ['User', 'profile_list', true, false],
+ ['User', 'allowed_org_list', true, false],
+ ['Person', 'team_list', false, false],
+ ['Ticket', 'functionalcis_list', false, true],
+ ];
+ }
}
\ No newline at end of file
diff --git a/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php b/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php
index 9b9c3f6b8..ebcb2e38c 100644
--- a/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php
@@ -7,7 +7,6 @@
namespace Combodo\iTop\Test\UnitTest\Core\CRUD;
use Combodo\iTop\Service\Events\EventData;
-use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use ContactType;
use CoreException;
@@ -21,6 +20,7 @@ use ormLinkSet;
use Person;
use Team;
use utils;
+use const EVENT_DB_LINKS_CHANGED;
class CRUDEventTest extends ItopDataTestCase
{
@@ -339,8 +339,8 @@ class CRUDEventTest extends ItopDataTestCase
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_CHECK_TO_WRITE]);
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_BEFORE_WRITE]);
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_AFTER_WRITE]);
- $this->assertEquals(4, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
- $this->assertEquals(20, self::$iEventCalls);
+ $this->assertArrayNotHasKey(EVENT_DB_LINKS_CHANGED, self::$aEventCalls, 'no relation with the with_php_compute attribute !');
+ $this->assertEquals(16, self::$iEventCalls);
}
/**
@@ -388,8 +388,8 @@ class CRUDEventTest extends ItopDataTestCase
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_CHECK_TO_WRITE]);
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_BEFORE_WRITE]);
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_AFTER_WRITE]);
- $this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
- $this->assertEquals(19, self::$iEventCalls);
+ $this->assertArrayNotHasKey(EVENT_DB_LINKS_CHANGED, self::$aEventCalls, 'no relation with the with_php_compute attribute !');
+ $this->assertEquals(16, self::$iEventCalls);
// Read the object explicitly from the DB to check that the role has been set
$oSet = new DBObjectSet(DBSearch::FromOQL('SELECT Team WHERE id=:id'), [], ['id' => $oTeam->GetKey()]);
@@ -495,7 +495,7 @@ class CRUDEventTest extends ItopDataTestCase
$oLnk = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
$oLnk->DBInsert();
- $this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
+ $this->assertArrayNotHasKey(EVENT_DB_LINKS_CHANGED, self::$aEventCalls, 'no relation with the with_php_compute attribute !');
}
public function testLinksDeleted()
@@ -517,7 +517,7 @@ class CRUDEventTest extends ItopDataTestCase
$oLnk->DBDelete();
- $this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
+ $this->assertArrayNotHasKey(EVENT_DB_LINKS_CHANGED, self::$aEventCalls, 'no relation with the with_php_compute attribute !');
}
// Tests with MockDBObject
diff --git a/tests/php-unit-tests/unitary-tests/core/DBObject/CheckToWritePropagationTest.php b/tests/php-unit-tests/unitary-tests/core/DBObject/CheckToWritePropagationTest.php
new file mode 100644
index 000000000..f4e025983
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/core/DBObject/CheckToWritePropagationTest.php
@@ -0,0 +1,359 @@
+ [
+ 'aProfilesBeforeUserCreation' => [
+ ],
+ 'bWaitForException' => 'CoreCannotSaveObjectException',
+ ],
+ 'Portal power user' => [
+ 'aProfilesBeforeUserCreation' => [
+ 'Portal power user',
+ ],
+ 'bWaitForException' => 'CoreCannotSaveObjectException',
+ ],
+ 'Portal power user + Configuration Manager' => [
+ 'aProfilesBeforeUserCreation' => [
+ 'Portal power user',
+ 'Configuration Manager',
+ ],
+ 'bWaitForException' => false,
+ ],
+ 'Portal power user + Configuration Manager + Admin' => [
+ 'aProfilesBeforeUserCreation' => [
+ 'Portal power user',
+ 'Configuration Manager',
+ 'Administrator',
+ ],
+ 'bWaitForException' => false,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ * @covers User::CheckPortalProfiles
+ */
+ public function testUserLocalCreation($aProfilesBeforeUserCreation, $sWaitForException)
+ {
+ $oUser = new UserLocal();
+ $sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid('', true);
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+ if (false !== $sWaitForException) {
+ $this->expectException($sWaitForException);
+ }
+ $this->commonUserCreationTest($oUser, $aProfilesBeforeUserCreation);
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ * @covers User::CheckPortalProfiles
+ */
+ public function testUserLocalDelete($aProfilesBeforeUserCreation, $sWaitForException)
+ {
+ $oUser = new UserLocal();
+ $sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid('', true);
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+ if (false !== $sWaitForException) {
+ $this->expectException($sWaitForException);
+ }
+ $this->commonUserCreationTest($oUser, $aProfilesBeforeUserCreation, false);
+
+ $oUser->DBDelete();
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ * @covers User::CheckPortalProfiles
+ */
+ public function testUserLocalUpdate($aProfilesBeforeUserCreation, $sWaitForException)
+ {
+ $oUser = new UserLocal();
+ $sLogin = 'testUserLocalUpdateWithPortalPowerUserProfile-'.uniqid('', true);
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+ if (false !== $sWaitForException) {
+ $this->expectException($sWaitForException);
+ }
+ $this->commonUserUpdateTest($oUser, $aProfilesBeforeUserCreation);
+ }
+
+ private function commonUserCreationTest($oUserToCreate, $aProfilesBeforeUserCreation, $bTestUserItopAccess = true)
+ {
+ $sUserClass = get_class($oUserToCreate);
+ list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUserToCreate, $aProfilesBeforeUserCreation);
+
+ $this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aProfilesBeforeUserCreation, $bTestUserItopAccess);
+ }
+
+ private function commonUserUpdateTest($oUserToCreate, $aProfilesBeforeUserCreation)
+ {
+ $sUserClass = get_class($oUserToCreate);
+ list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUserToCreate, ['Administrator']);
+
+ $oUserToUpdate = MetaModel::GetObject($sUserClass, $sId);
+ $oProfileList = $oUserToUpdate->Get('profile_list');
+ while ($oObj = $oProfileList->Fetch()) {
+ $oProfileList->RemoveItem($oObj->GetKey());
+ }
+
+ foreach ($aProfilesBeforeUserCreation as $sProfileName) {
+ $oAdminUrpProfile = new URP_UserProfile();
+ $oProfile = $aProfiles[$sProfileName];
+ $oAdminUrpProfile->Set('profileid', $oProfile->GetKey());
+ $oAdminUrpProfile->Set('reason', 'UNIT Tests');
+ $oProfileList->AddItem($oAdminUrpProfile);
+ }
+
+ $oUserToUpdate->Set('profile_list', $oProfileList);
+ $oUserToUpdate->DBWrite();
+
+ $this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aProfilesBeforeUserCreation);
+ }
+
+ private function CreateUserForProfileTesting(User $oUserToCreate, array $aProfilesBeforeUserCreation, $bDbInsert = true): array
+ {
+ $aProfiles = [];
+ $oSearch = DBSearch::FromOQL('SELECT URP_Profiles');
+ $oProfileSet = new DBObjectSet($oSearch);
+ while (($oProfile = $oProfileSet->Fetch()) != null) {
+ $aProfiles[$oProfile->Get('name')] = $oProfile;
+ }
+
+ $this->CreateTestOrganization();
+ $oContact = $this->CreatePerson('1');
+ $iContactId = $oContact->GetKey();
+
+ $oUserToCreate->Set('contactid', $iContactId);
+
+ $oUserProfileList = $oUserToCreate->Get('profile_list');
+ foreach ($aProfilesBeforeUserCreation as $sProfileName) {
+ $oUserProfile = new URP_UserProfile();
+ $oProfile = $aProfiles[$sProfileName];
+ $oUserProfile->Set('profileid', $oProfile->GetKey());
+ $oUserProfile->Set('reason', 'UNIT Tests');
+ $oUserProfileList->AddItem($oUserProfile);
+ }
+
+ $oUserToCreate->Set('profile_list', $oUserProfileList);
+ if ($bDbInsert) {
+ $sId = $oUserToCreate->DBInsert();
+ } else {
+ $sId = -1;
+ }
+
+ return [$sId, $aProfiles];
+ }
+
+ private function CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestItopConnection = true)
+ {
+ $oUser = MetaModel::GetObject($sUserClass, $sId);
+ $oUserProfileList = $oUser->Get('profile_list');
+ $aProfilesAfterCreation = [];
+ while (($oProfile = $oUserProfileList->Fetch()) != null) {
+ $aProfilesAfterCreation[] = $oProfile->Get('profile');
+ }
+
+ foreach ($aExpectedAssociatedProfilesAfterUserCreation as $sExpectedProfileName) {
+ $this->assertTrue(in_array($sExpectedProfileName, $aProfilesAfterCreation),
+ "profile \'$sExpectedProfileName\' should be asociated to user after creation. ".var_export($aProfilesAfterCreation, true));
+ }
+
+ if (!$bTestItopConnection) {
+ return;
+ }
+
+ $_SESSION = [];
+
+ UserRights::Login($oUser->Get('login'));
+
+ if (!UserRights::IsPortalUser()) {
+ //calling this API triggers Fatal Error on below OQL used by \User->GetContactObject() for a user with only 'portal power user' profile
+ /**
+ * Error: No result for the single row query: 'SELECT DISTINCT `Contact`.`id` AS `Contactid`, `Contact`.`name` AS `Contactname`, `Contact`.`status` AS `Contactstatus`, `Contact`.`org_id` AS `Contactorg_id`, `Organization_org_id`.`name` AS `Contactorg_name`, `Contact`.`email` AS `Contactemail`, `Contact`.`phone` AS `Contactphone`, `Contact`.`notify` AS `Contactnotify`, `Contact`.`function` AS `Contactfunction`, `Contact`.`finalclass` AS `Contactfinalclass`, IF((`Contact`.`finalclass` IN ('Team', 'Contact')), CAST(CONCAT(COALESCE(`Contact`.`name`, '')) AS CHAR), CAST(CONCAT(COALESCE(`Contact_poly_Person`.`first_name`, ''), COALESCE(' ', ''), COALESCE(`Contact`.`name`, '')) AS CHAR)) AS `Contactfriendlyname`, COALESCE((`Contact`.`status` = 'inactive'), 0) AS `Contactobsolescence_flag`, `Contact`.`obsolescence_date` AS `Contactobsolescence_date`, CAST(CONCAT(COALESCE(`Organization_org_id`.`name`, '')) AS CHAR) AS `Contactorg_id_friendlyname`, COALESCE((`Organization_org_id`.`status` = 'inactive'), 0) AS `Contactorg_id_obsolescence_flag` FROM `contact` AS `Contact` INNER JOIN `organization` AS `Organization_org_id` ON `Contact`.`org_id` = `Organization_org_id`.`id` LEFT JOIN `person` AS `Contact_poly_Person` ON `Contact`.`id` = `Contact_poly_Person`.`id` WHERE ((`Contact`.`id` = 40) AND 0) '.
+ */
+ NavigationMenuFactory::MakeStandard();
+ }
+
+ $this->assertTrue(true, 'after fix N°5324 no exception raised');
+ // logout
+ $_SESSION = [];
+ }
+
+ /**
+ * @dataProvider ProfilesLinksProvider
+ */
+ public function testProfilesLinksDBDelete(string $sProfileNameToRemove, $bRaiseException = false)
+ {
+ $aInitialProfiles = [$sProfileNameToRemove, 'Portal power user'];
+
+ $oUser = new UserExternal();
+ $sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid('', true);
+ $oUser->Set('login', $sLogin);
+
+ [$sId, $aProfiles] = $this->CreateUserForProfileTesting($oUser, $aInitialProfiles);
+
+ if ($bRaiseException) {
+ $this->expectException(DeleteException::class);
+ }
+
+ $aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
+ if (array_key_exists($sProfileNameToRemove, $aURPUserProfileByUser)) {
+ $oURPUserProfile = $aURPUserProfileByUser[$sProfileNameToRemove];
+ $oURPUserProfile->DBDelete();
+ }
+ }
+
+ /**
+ * @dataProvider ProfilesLinksProvider
+ */
+ public function testProfilesLinksEdit_ChangeProfileId(string $sInitialProfile, $bRaiseException = false)
+ {
+ $oUser = new UserExternal();
+ $sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid('', true);
+ $oUser->Set('login', $sLogin);
+
+ $sUserClass = get_class($oUser);
+ list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, [$sInitialProfile]);
+
+ $oURP_Profile = MetaModel::GetObjectByColumn('URP_Profiles', 'name', 'Portal power user');
+
+ $aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
+
+ if ($bRaiseException) {
+ $this->expectException(CoreCannotSaveObjectException::class);
+ }
+
+ if (array_key_exists($sInitialProfile, $aURPUserProfileByUser)) {
+ $oURPUserProfile = $aURPUserProfileByUser[$sInitialProfile];
+ $oURPUserProfile->Set('profileid', $oURP_Profile->GetKey());
+ $oURPUserProfile->DBWrite();
+ }
+
+ if (!$bRaiseException) {
+ $aExpectedProfilesAfterUpdate = ['Portal power user', 'Portal user'];
+ $this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedProfilesAfterUpdate);
+ }
+ }
+
+ public function ProfilesLinksProvider()
+ {
+ return [
+ 'Administrator' => ['sProfileNameToMove' => 'Administrator', 'bRaiseException' => true],
+ 'Portal user' => ['sProfileNameToMove' => 'Portal user', 'bRaiseException' => true],
+ ];
+ }
+
+ /**
+ * @dataProvider ProfilesLinksProvider
+ */
+ public function testProfilesLinksEdit_ChangeUserId($sProfileNameToMove, $bRaiseException = false)
+ {
+ $aInitialProfiles = [$sProfileNameToMove, 'Portal power user'];
+
+ $oUser = new UserExternal();
+ $sLogin1 = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid('', true);
+ $oUser->Set('login', $sLogin1);
+
+ $sUserClass = get_class($oUser);
+ list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, $aInitialProfiles);
+
+ $oUser = new UserExternal();
+ $sLogin2 = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid('', true);
+ $oUser->Set('login', $sLogin2);
+ list ($sAnotherUserId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, ['Configuration Manager']);
+
+ if ($bRaiseException) {
+ $this->expectException(CoreCannotSaveObjectException::class);
+ }
+
+ $aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
+ if (array_key_exists($sProfileNameToMove, $aURPUserProfileByUser)) {
+ $oURPUserProfile = $aURPUserProfileByUser[$sProfileNameToMove];
+ $oURPUserProfile->Set('userid', $sAnotherUserId);
+ $oURPUserProfile->DBWrite();
+ }
+
+ if (!$bRaiseException) {
+ $aExpectedProfilesAfterUpdate = [$sProfileNameToMove, 'Configuration Manager'];
+ $this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sAnotherUserId, $aExpectedProfilesAfterUpdate);
+
+ $aExpectedProfilesAfterUpdate = ['Portal power user', 'Portal user'];
+ $this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedProfilesAfterUpdate);
+ }
+ }
+
+ private function GetURPUserProfileByUser($iUserId): array
+ {
+ $aRes = [];
+ $oSearch = DBSearch::FromOQL("SELECT URP_UserProfile WHERE userid=$iUserId");
+ $oSet = new DBObjectSet($oSearch);
+ while (($oURPUserProfile = $oSet->Fetch()) != null) {
+ $aRes[$oURPUserProfile->Get('profile')] = $oURPUserProfile;
+ }
+
+ return $aRes;
+ }
+
+ public function CustomizedPortalsProvider()
+ {
+ return [
+ 'console + customized portal' => [
+ 'aPortalDispatcherData' => [
+ 'customer-portal',
+ 'backoffice',
+ ],
+ ],
+ 'console + itop portal + customized portal' => [
+ 'aPortalDispatcherData' => [
+ 'itop-portal',
+ 'customer-portal',
+ 'backoffice',
+ ],
+ ],
+ ];
+ }
+}
diff --git a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php
index 9e65086e9..542573c74 100644
--- a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php
@@ -488,29 +488,4 @@ class UserRightsTest extends ItopDataTestCase
'with Admins hidden' => [true, 0],
];
}
-
- /**
- * @dataProvider WithConstraintParameterProvider
- * @param string $sClass
- * @param string $sAttCode
- * @param bool $bExpected
- *
- * @return void
- * @throws \Exception
- */
- public function testWithConstraintParameter(string $sClass, string $sAttCode, bool $bExpected)
- {
- $oAttDef = \MetaModel::GetAttributeDef($sClass, $sAttCode);
- $this->assertTrue(method_exists($oAttDef, "GetHasConstraint"));
- $this->assertEquals($bExpected, $oAttDef->GetHasConstraint());
- }
-
- public function WithConstraintParameterProvider()
- {
- return [
- ['User', 'profile_list', true],
- ['User', 'allowed_org_list', true],
- ['Person', 'team_list', false],
- ];
- }
}