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], - ]; - } }