create_temporary_object
false
diff --git a/core/dbobject.class.php b/core/dbobject.class.php
index 2770566c3..8bb13c0fb 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,130 @@ abstract class DBObject implements iDisplay
}
}
+ /**
+ * Trigger onObjectUpdate on the target object when an object pointed by a LinkSet is modified, added or removed
+ *
+ * @since 3.1.1 3.2.0 N°6531 method creation
+ */
+ final protected function ActivateOnObjectUpdateTriggersForTargetObjects(): void
+ {
+ $aPreviousValues = $this->ListPreviousValuesForUpdatedAttributes();
+
+ $aClassExtKeyAttCodes = MetaModel::GetAttributesList(get_class($this), [AttributeExternalKey::class]);
+ foreach ($aClassExtKeyAttCodes as $sExtKeyWithMirrorLinkAttCode) {
+ /** @var AttributeExternalKey $oExtKeyWithMirrorLinkAttDef */
+ $oExtKeyWithMirrorLinkAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyWithMirrorLinkAttCode);
+
+ /** @var AttributeLinkedSet $oAttDefMirrorLink */
+ $oAttDefMirrorLink = $oExtKeyWithMirrorLinkAttDef->GetMirrorLinkAttribute();
+ if (is_null($oAttDefMirrorLink)) {
+ // No LinkSet pointing to me
+ continue;
+ }
+ $sAttCodeMirrorLink = $oAttDefMirrorLink->GetCode();
+ $sTargetObjectClass = $oExtKeyWithMirrorLinkAttDef->GetTargetClass();
+
+ if (array_key_exists($sExtKeyWithMirrorLinkAttCode, $aPreviousValues)) {
+ // need to update old target also
+ $sPreviousTargetObjectKey = $aPreviousValues[$sExtKeyWithMirrorLinkAttCode];
+ $oPreviousTargetObject = static::GetObjectIfNotInCRUDStack($sTargetObjectClass, $sPreviousTargetObjectKey);
+ $this->ActivateOnObjectUpdateTriggers($oPreviousTargetObject, [$sAttCodeMirrorLink]);
+ }
+
+ // we need to update remote with current lnk instance
+ $oTargetObject = static::GetObjectIfNotInCRUDStack($sTargetObjectClass, $this->Get($sExtKeyWithMirrorLinkAttCode));
+ $this->ActivateOnObjectUpdateTriggers($oTargetObject, [$sAttCodeMirrorLink]);
+ }
+ }
+
+ final static protected function GetObjectIfNotInCRUDStack($sClass, $sKey)
+ {
+ if (DBObject::IsObjectCurrentlyInCrud($sClass, $sKey)) {
+ return null;
+ }
+
+ return MetaModel::GetObject($sClass, $sKey, false);
+ }
+
+ /**
+ * Cascade CheckToWrite to Target Objects With LinkSet Pointing To Me
+ * @since 3.1.1 3.2.0 N°6228 method creation
+ */
+ final protected function CheckToWriteForTargetObjects(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);
+
+ /** @var AttributeLinkedSet $oAttDefMirrorLink */
+ $oAttDefMirrorLink = $oExtKeyWithMirrorLinkAttDef->GetMirrorLinkAttribute();
+ if (is_null($oAttDefMirrorLink) || (false === $oAttDefMirrorLink->HasPHPConstraint())) {
+ continue;
+ }
+ $sAttCodeMirrorLink = $oAttDefMirrorLink->GetCode();
+ $sTargetObjectClass = $oExtKeyWithMirrorLinkAttDef->GetTargetClass();
+
+ $oTargetObject = static::GetObjectIfNotInCRUDStack($sTargetObjectClass, $this->Get($sExtKeyWithMirrorLinkAttCode));
+
+ if ($this->IsNew()) {
+ $this->CheckToWriteForSingleTargetObject_Internal('add', $oTargetObject, $sAttCodeMirrorLink, false);
+ } else if ($bIsCheckToDelete) {
+ $this->CheckToWriteForSingleTargetObject_Internal('remove', $oTargetObject, $sAttCodeMirrorLink, true);
+ } else {
+ if (array_key_exists($sExtKeyWithMirrorLinkAttCode, $aChanges)) {
+ // need to update remote old + new
+ $aPreviousValues = $this->ListPreviousValuesForUpdatedAttributes();
+ $sPreviousTargetObjectKey = $aPreviousValues[$sExtKeyWithMirrorLinkAttCode];
+ $oPreviousTargetObject = static::GetObjectIfNotInCRUDStack($sTargetObjectClass, $sPreviousTargetObjectKey);
+ $this->CheckToWriteForSingleTargetObject_Internal('remove', $oPreviousTargetObject, $sAttCodeMirrorLink, false);
+ $this->CheckToWriteForSingleTargetObject_Internal('add', $oTargetObject, $sAttCodeMirrorLink, false);
+ } else {
+ $this->CheckToWriteForSingleTargetObject_Internal('modify', $oTargetObject, $sAttCodeMirrorLink, false); // we need to update remote with current lnk instance
+ }
+ }
+ }
+ }
+
+ private function CheckToWriteForSingleTargetObject_Internal(string $sAction, ?DBObject $oTargetObject, string $sAttCodeMirrorLink, bool $bIsCheckToDelete): void
+ {
+ if (is_null($oTargetObject)) {
+ return;
+ }
+
+ $this->LogCRUDDebug(__METHOD__, "action: $sAction ".get_class($oTargetObject).'::'.$oTargetObject->GetKey()." ($sAttCodeMirrorLink)");
+
+ /** @var \ormLinkSet $oTargetValue */
+ $oTargetValue = $oTargetObject->Get($sAttCodeMirrorLink);
+ switch ($sAction) {
+ case 'add':
+ $oTargetValue->AddItem($this);
+ break;
+ case 'remove':
+ $oTargetValue->RemoveItem($this->GetKey());
+ break;
+ case 'modify':
+ $oTargetValue->ModifyItem($this);
+ break;
+ }
+ $oTargetObject->Set($sAttCodeMirrorLink, $oTargetValue);
+ [$bCheckStatus, $aCheckIssues, $bSecurityIssue] = $oTargetObject->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;
+ }
+ $aTargetCheckWarnings = $oTargetObject->GetCheckWarnings();
+ if (is_array($aTargetCheckWarnings)) {
+ $this->m_aCheckWarnings = array_merge($this->m_aCheckWarnings ?? [], $aTargetCheckWarnings);
+ }
+ }
+
/**
* @api
* @api-advanced
@@ -2484,6 +2609,7 @@ abstract class DBObject implements iDisplay
{
return array(true, array());
}
+
if (is_null($this->m_bCheckStatus))
{
$this->m_aCheckIssues = array();
@@ -2500,6 +2626,9 @@ abstract class DBObject implements iDisplay
$oKPI = new ExecutionKPI();
$this->DoCheckToWrite();
$oKPI->ComputeStatsForExtension($this, 'DoCheckToWrite');
+
+ $this->CheckToWriteForTargetObjects();
+
if (count($this->m_aCheckIssues) == 0)
{
$this->m_bCheckStatus = true;
@@ -2509,6 +2638,7 @@ abstract class DBObject implements iDisplay
$this->m_bCheckStatus = false;
}
}
+
return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue);
}
@@ -2594,7 +2724,7 @@ abstract class DBObject implements iDisplay
*
* an array of displayable error is added in {@see DBObject::$m_aDeleteIssues}
*
- * @internal
+ * @internal
*
* @param \DeletionPlan $oDeletionPlan
*
@@ -2659,8 +2789,15 @@ abstract class DBObject implements iDisplay
*/
public function CheckToDelete(&$oDeletionPlan)
{
- $this->MakeDeletionPlan($oDeletionPlan);
- $oDeletionPlan->ComputeResults();
+ $this->AddCurrentObjectInCrudStack('DELETE');
+ try {
+ $this->MakeDeletionPlan($oDeletionPlan);
+ $oDeletionPlan->ComputeResults();
+ }
+ finally {
+ $this->RemoveCurrentObjectInCrudStack();
+ }
+
return (!$oDeletionPlan->FoundStopper());
}
@@ -2711,7 +2848,7 @@ abstract class DBObject implements iDisplay
{
// The value is a scalar, the comparison must be 100% strict
if($this->m_aOrigValues[$sAtt] !== $proposedValue)
- {
+ {
//echo "$sAtt:\n";
//var_dump($this->m_aOrigValues[$sAtt]);
//var_dump($proposedValue);
@@ -2833,7 +2970,7 @@ abstract class DBObject implements iDisplay
/**
* Used only by insert, Meant to be overloaded
- *
+ *
* @overwritable-hook You can extend this method in order to provide your own logic.
*/
protected function OnObjectKeyReady()
@@ -2941,7 +3078,7 @@ abstract class DBObject implements iDisplay
// fields in first array, values in the second
$aFieldsToWrite = array();
$aValuesToWrite = array();
-
+
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
{
// Add it to the list of fields to write
@@ -2950,7 +3087,7 @@ abstract class DBObject implements iDisplay
}
$aHierarchicalKeys = array();
-
+
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) {
// Skip this attribute if not defined in this table
if ((!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode) && !$oAttDef->CopyOnAllTables())
@@ -2960,7 +3097,7 @@ abstract class DBObject implements iDisplay
$aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]);
foreach($aAttColumns as $sColumn => $sValue)
{
- $aFieldsToWrite[] = "`$sColumn`";
+ $aFieldsToWrite[] = "`$sColumn`";
$aValuesToWrite[] = CMDBSource::Quote($sValue);
}
if ($oAttDef->IsHierarchicalKey())
@@ -2984,7 +3121,7 @@ abstract class DBObject implements iDisplay
self::$m_aBulkInsertCols[$sClass][$sTable] = implode(', ', $aFieldsToWrite);
}
self::$m_aBulkInsertItems[$sClass][$sTable][] = '('.implode (', ', $aValuesToWrite).')';
-
+
$iNewKey = 999999; // TODO - compute next id....
}
else
@@ -3069,7 +3206,7 @@ abstract class DBObject implements iDisplay
// fields in first array, values in the second
$aFieldsToWrite = array();
$aValuesToWrite = array();
-
+
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
{
// Add it to the list of fields to write
@@ -3104,7 +3241,7 @@ abstract class DBObject implements iDisplay
$aAttColumns = $oAttDef->GetSQLValues($value);
foreach($aAttColumns as $sColumn => $sValue)
{
- $aFieldsToWrite[] = "`$sColumn`";
+ $aFieldsToWrite[] = "`$sColumn`";
$aValuesToWrite[] = CMDBSource::Quote($sValue);
}
if ($oAttDef->IsHierarchicalKey())
@@ -3214,7 +3351,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()));
}
@@ -3335,7 +3472,7 @@ abstract class DBObject implements iDisplay
* @throws \MySQLException
* @throws \OQLException
*/
- public function PostInsertActions(): void
+ protected function PostInsertActions(): void
{
$this->FireEventAfterWrite([], true);
$oKPI = new ExecutionKPI();
@@ -3359,6 +3496,9 @@ abstract class DBObject implements iDisplay
// - TriggerOnObjectMention
$this->ActivateOnMentionTriggers(true);
+
+ // - Trigger for object pointing to the current object
+ $this->ActivateOnObjectUpdateTriggersForTargetObjects();
}
/**
@@ -3386,7 +3526,7 @@ abstract class DBObject implements iDisplay
$this->RecordObjCreation();
return $ret;
}
-
+
/**
* This function is automatically called after cloning an object with the "clone" PHP language construct
* The purpose of this method is to reset the appropriate attributes of the object in
@@ -3454,7 +3594,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()]);
}
@@ -3651,7 +3791,7 @@ abstract class DBObject implements iDisplay
* @throws \MySQLException
* @throws \OQLException
*/
- public function PostUpdateActions(array $aChanges): void
+ protected function PostUpdateActions(array $aChanges): void
{
$this->FireEventAfterWrite($aChanges, false);
$oKPI = new ExecutionKPI();
@@ -3659,20 +3799,10 @@ abstract class DBObject implements iDisplay
$oKPI->ComputeStatsForExtension($this, 'AfterUpdate');
// - TriggerOnObjectUpdate
- $aClassList = MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL);
- $aParams = array('class_list' => $aClassList);
- $oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN (:class_list)'),
- array(), $aParams);
- while ($oTrigger = $oSet->Fetch()) {
- /** @var \TriggerOnObjectUpdate $oTrigger */
- try {
- $oTrigger->DoActivate($this->ToArgs());
- }
- catch (Exception $e) {
- $oTrigger->LogException($e, $this);
- utils::EnrichRaisedException($oTrigger, $e);
- }
- }
+ $this->ActivateOnObjectUpdateTriggers($this);
+
+ // - Trigger for object pointing to the current object
+ $this->ActivateOnObjectUpdateTriggersForTargetObjects();
$sClass = get_class($this);
if (MetaModel::HasLifecycle($sClass))
@@ -3718,6 +3848,37 @@ abstract class DBObject implements iDisplay
$this->ActivateOnMentionTriggers(false, $aChanges);
}
+ /**
+ * @param \DBObject $oObject
+ * @param array|null $aAttributes
+ *
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ private function ActivateOnObjectUpdateTriggers(?DBObject $oObject, array $aAttributes = null): void
+ {
+ if (is_null($oObject)) {
+ return;
+ }
+
+ // - TriggerOnObjectUpdate
+ $aClassList = MetaModel::EnumParentClasses(get_class($oObject), ENUM_PARENT_CLASSES_ALL);
+ $aParams = array('class_list' => $aClassList);
+ $oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN (:class_list)'),
+ array(), $aParams);
+ while ($oTrigger = $oSet->Fetch()) {
+ /** @var \TriggerOnObjectUpdate $oTrigger */
+ try {
+ $oTrigger->DoActivateForSpecificAttributes($oObject->ToArgs(), $aAttributes);
+ }
+ catch (Exception $e) {
+ $oTrigger->LogException($e, $oObject);
+ utils::EnrichRaisedException($oTrigger, $e);
+ }
+ }
+ }
/**
* Increment attribute with specified value.
@@ -3921,6 +4082,8 @@ abstract class DBObject implements iDisplay
*/
protected function DBDeleteSingleObject()
{
+ $this->LogCRUDEnter(__METHOD__);
+
if (MetaModel::DBIsReadOnly())
{
$this->LogCRUDExit(__METHOD__, 'DB is read-only');
@@ -4041,8 +4204,11 @@ abstract class DBObject implements iDisplay
$this->AfterDelete();
$oKPI->ComputeStatsForExtension($this, 'AfterDelete');
+ // - Trigger for object pointing to the current object
+ $this->ActivateOnObjectUpdateTriggersForTargetObjects();
$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
@@ -4054,7 +4220,7 @@ abstract class DBObject implements iDisplay
* First, checks if the object can be deleted regarding database integrity.
* If the answer is yes, it performs any required cleanup (delete other objects or reset external keys) in addition to the object
* deletion.
- *
+ *
* @api
*
* @param \DeletionPlan $oDeletionPlan Do not use: aims at dealing with recursion
@@ -4073,74 +4239,65 @@ abstract class DBObject implements iDisplay
public function DBDelete(&$oDeletionPlan = null)
{
$this->LogCRUDEnter(__METHOD__);
+ try {
+ static $iLoopTimeLimit = null;
+ if ($iLoopTimeLimit == null) {
+ $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+ }
+ if (is_null($oDeletionPlan)) {
+ $oDeletionPlan = new DeletionPlan();
+ }
- 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 (false === $this->CheckToDelete($oDeletionPlan)) {
+ $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 symmetric:
+ // 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__);
}
- set_time_limit(intval($iPreviousTimeLimit));
-
- $this->LogCRUDExit(__METHOD__);
return $oDeletionPlan;
}
@@ -4447,7 +4604,7 @@ abstract class DBObject implements iDisplay
*
* @api
*
- */
+ */
public function Reset($sAttCode)
{
$this->Set($sAttCode, $this->GetDefaultValue($sAttCode));
@@ -4459,7 +4616,7 @@ abstract class DBObject implements iDisplay
* Suitable for use as a lifecycle action
*
* @api
- */
+ */
public function Copy($sDestAttCode, $sSourceAttCode)
{
$oTypeValueToCopy = MetaModel::GetAttributeDef(get_class($this), $sSourceAttCode);
@@ -4789,7 +4946,7 @@ abstract class DBObject implements iDisplay
{
throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this));
}
-
+
$oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
if (!$oKeyAttDef instanceof AttributeExternalKey)
{
@@ -4807,14 +4964,14 @@ abstract class DBObject implements iDisplay
$ret = $oRemoteObj->GetForTemplate($sRemoteAttCode);
}
}
- else
+ else
{
switch($sPlaceholderAttCode)
{
case 'id':
$ret = $this->GetKey();
break;
-
+
case 'name()':
$ret = $this->GetName();
break;
@@ -5001,7 +5158,7 @@ abstract class DBObject implements iDisplay
if ($oOwner)
{
$sLinkSetOwnerClass = get_class($oOwner);
-
+
$oMyChangeOp = MetaModel::NewObject($sChangeOpClass);
$oMyChangeOp->Set("objclass", $sLinkSetOwnerClass);
$oMyChangeOp->Set("objkey", $iLinkSetOwnerId);
@@ -5028,7 +5185,7 @@ abstract class DBObject implements iDisplay
{
/** @var \AttributeLinkedSet $oLinkSet */
if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue;
-
+
$iLinkSetOwnerId = $this->Get($sExtKeyAttCode);
$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove');
if ($oMyChangeOp)
@@ -5246,8 +5403,9 @@ abstract class DBObject implements iDisplay
$this->m_aDeleteIssues = array(); // Ok
$this->FireEventCheckToDelete($oDeletionPlan);
$this->DoCheckToDelete($oDeletionPlan);
+ $this->CheckToWriteForTargetObjects(true);
$oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue);
-
+
$aDependentObjects = $this->GetReferencingObjects(true /* allow all data */);
// Getting and setting time limit are not symmetric:
@@ -5429,7 +5587,7 @@ abstract class DBObject implements iDisplay
$aSynchroClasses[] = $sTarget;
}
}
-
+
foreach($aSynchroClasses as $sClass)
{
if ($this instanceof $sClass)
@@ -6236,6 +6394,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
*
@@ -6483,9 +6653,10 @@ abstract class DBObject implements iDisplay
// during insert key is reset from -1 to null
// so we need to handle null values (will give empty string after conversion)
$sConvertedId = (string)$sId;
+ $oRootClass = MetaModel::GetRootClass($sClass);
foreach (self::$m_aCrudStack as $aCrudStackEntry) {
- if (($sClass === $aCrudStackEntry['class'])
+ if (($oRootClass === $aCrudStackEntry['class'])
&& ($sConvertedId === $aCrudStackEntry['id'])) {
return true;
}
@@ -6500,12 +6671,14 @@ abstract class DBObject implements iDisplay
* @param string $sClass
*
* @return bool
+ * @throws \CoreException
* @since 3.1.0 N°5609
*/
final public static function IsClassCurrentlyInCrud(string $sClass): bool
{
+ $sRootClass = MetaModel::GetRootClass($sClass);
foreach (self::$m_aCrudStack as $aCrudStackEntry) {
- if ($sClass === $aCrudStackEntry['class']) {
+ if ($sRootClass === $aCrudStackEntry['class']) {
return true;
}
}
@@ -6523,9 +6696,11 @@ abstract class DBObject implements iDisplay
*/
private function AddCurrentObjectInCrudStack(string $sCrudType): void
{
+ $this->LogCRUDDebug(__METHOD__);
+ $sRootClass = MetaModel::GetRootClass(get_class($this));
self::$m_aCrudStack[] = [
'type' => $sCrudType,
- 'class' => get_class($this),
+ 'class' => $sRootClass,
'id' => (string)$this->GetKey(), // GetKey() doesn't have type hinting, so forcing type to avoid getting an int
];
}
@@ -6539,6 +6714,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 +6728,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/log.class.inc.php b/core/log.class.inc.php
index 29b359689..a8b920815 100644
--- a/core/log.class.inc.php
+++ b/core/log.class.inc.php
@@ -3,7 +3,7 @@
//
// This file is part of iTop.
//
-// iTop is free software; you can redistribute it and/or modify
+// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
@@ -576,6 +576,11 @@ class LogChannels
public const DATATABLE = 'Datatable';
public const DEADLOCK = 'DeadLock';
+ /**
+ * @var string Everything related to PHP sessions tracking
+ * @since 3.1.1 3.2.0 N°6901
+ */
+ public const SESSIONTRACKER = 'SessionTracker';
/**
* @var string Everything related to the datamodel CRUD
diff --git a/core/metamodel.class.php b/core/metamodel.class.php
index 9af99c36f..82de32529 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
+ {
+ if (MetaModel::IsStandaloneClass($sClass)) {
+ return $sClass;
+ }
+
+ $sRootClass = MetaModel::GetRootClass($sClass);
+ $sTable = MetaModel::DBGetTable($sRootClass);
+ $sKeyCol = MetaModel::DBGetKey($sRootClass);
+ $sEscapedKey = CMDBSource::Quote($iKey);
+ $sFinalClassField = Metamodel::DBGetClassField($sRootClass);
+
+ $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/trigger.class.inc.php b/core/trigger.class.inc.php
index b0222184c..7cc3a1d80 100644
--- a/core/trigger.class.inc.php
+++ b/core/trigger.class.inc.php
@@ -546,6 +546,38 @@ class TriggerOnObjectUpdate extends TriggerOnObject
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form
}
+ /**
+ * Activate trigger based on attribute list given instead of changed attributes
+ *
+ * @param array $aContextArgs
+ * @param array|null $aAttributes if null default to changed attributes
+ *
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \MissingQueryArgument
+ * @throws \MySQLException
+ * @throws \MySQLHasGoneAwayException
+ * @throws \OQLException
+ * @since 3.1.1 3.2.0 N°6228
+ */
+ public function DoActivateForSpecificAttributes(array $aContextArgs, ?array $aAttributes)
+ {
+ if (isset($aContextArgs['this->object()']))
+ {
+ /** @var \DBObject $oObject */
+ $oObject = $aContextArgs['this->object()'];
+ if (is_null($aAttributes)) {
+ $aChanges = $oObject->ListPreviousValuesForUpdatedAttributes();
+ } else {
+ $aChanges = array_fill_keys($aAttributes, true);
+ }
+ if (false === $this->IsTargetObject($oObject->GetKey(), $aChanges)) {
+ return;
+ }
+ }
+ parent::DoActivate($aContextArgs);
+ }
+
public function IsTargetObject($iObjectId, $aChanges = array())
{
if (!parent::IsTargetObject($iObjectId, $aChanges))
diff --git a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
index 8a5696b57..fab2e442d 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/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php
index 4989deb64..80e888e24 100644
--- a/lib/composer/autoload_classmap.php
+++ b/lib/composer/autoload_classmap.php
@@ -473,6 +473,8 @@ return array(
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectManager' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectManager.php',
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectRepository' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectRepository.php',
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectsEvents' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php',
+ 'Combodo\\iTop\\SessionTracker\\SessionGC' => $baseDir . '/sources/SessionTracker/SessionGC.php',
+ 'Combodo\\iTop\\SessionTracker\\SessionHandler' => $baseDir . '/sources/SessionTracker/SessionHandler.php',
'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Config' => $baseDir . '/core/config.class.inc.php',
diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php
index da75d88ba..065c0f934 100644
--- a/lib/composer/autoload_static.php
+++ b/lib/composer/autoload_static.php
@@ -837,6 +837,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectManager' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectManager.php',
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectRepository' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectRepository.php',
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectsEvents' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php',
+ 'Combodo\\iTop\\SessionTracker\\SessionGC' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionGC.php',
+ 'Combodo\\iTop\\SessionTracker\\SessionHandler' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionHandler.php',
'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php',
diff --git a/pages/ajax.render.php b/pages/ajax.render.php
index 2981273ab..a200cce29 100644
--- a/pages/ajax.render.php
+++ b/pages/ajax.render.php
@@ -68,7 +68,7 @@ try
break;
default:
- ContextTag::AddContext(ContextTag::TAG_CONSOLE);
+ $oTag = new ContextTag(ContextTag::TAG_CONSOLE);
}
// First check if we can redirect the route to a dedicated controller
diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php
index 9bbb07a6a..e0ecf4d7f 100644
--- a/setup/compiler.class.inc.php
+++ b/setup/compiler.class.inc.php
@@ -2080,6 +2080,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);
@@ -2091,6 +2092,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/sources/Application/Helper/Session.php b/sources/Application/Helper/Session.php
index 4b708a61e..867e3ca70 100644
--- a/sources/Application/Helper/Session.php
+++ b/sources/Application/Helper/Session.php
@@ -7,6 +7,7 @@
namespace Combodo\iTop\Application\Helper;
+use Combodo\iTop\SessionTracker\SessionHandler;
use utils;
/**
@@ -34,6 +35,7 @@ class Session
}
if (!self::$bIsInitialized) {
+ SessionHandler::session_set_save_handler();
session_name('itop-'.md5(APPROOT));
}
@@ -214,4 +216,4 @@ class Session
return utils::IsModeCLI();
}
-}
\ No newline at end of file
+}
diff --git a/sources/SessionTracker/SessionGC.php b/sources/SessionTracker/SessionGC.php
new file mode 100644
index 000000000..8653ea26d
--- /dev/null
+++ b/sources/SessionTracker/SessionGC.php
@@ -0,0 +1,32 @@
+
+ * @package Combodo\iTop\SessionTracker
+ * @since 3.1.1 3.2.0 N°6901
+ */
+class SessionGC implements \iBackgroundProcess
+{
+ /**
+ * @inheritDoc
+ */
+ public function GetPeriodicity()
+ {
+ return 60 * 1; // seconds
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function Process($iTimeLimit)
+ {
+ $iMaxLifetime = ini_get('session.gc_maxlifetime') ?? 1440;
+ $oSessionHandler = new SessionHandler();
+ $iProcessed = $oSessionHandler->gc_with_time_limit($iMaxLifetime, $iTimeLimit);
+ return "processed $iProcessed tasks";
+ }
+}
diff --git a/sources/SessionTracker/SessionHandler.php b/sources/SessionTracker/SessionHandler.php
new file mode 100644
index 000000000..5aa0c4072
--- /dev/null
+++ b/sources/SessionTracker/SessionHandler.php
@@ -0,0 +1,240 @@
+
+ * @package Combodo\iTop\SessionTracker
+ * @since 3.1.1 3.2.0 N°6901
+ */
+class SessionHandler extends \SessionHandler
+{
+ /**
+ * @param string $session_id
+ *
+ * @return bool
+ */
+ #[ReturnTypeWillChange]
+ public function destroy($session_id) : bool
+ {
+ IssueLog::Debug("Destroy PHP session", \LogChannels::SESSIONTRACKER, [
+ 'session_id' => $session_id,
+ ]);
+ $bRes = parent::destroy($session_id);
+
+ if ($bRes) {
+ $this->unlink_session_file($session_id);
+ }
+
+ return $bRes;
+ }
+
+ /**
+ * @param int $max_lifetime
+ */
+ #[ReturnTypeWillChange]
+ public function gc($max_lifetime) : bool
+ {
+ IssueLog::Debug("Run PHP sessions garbage collector", \LogChannels::SESSIONTRACKER, [
+ 'max_lifetime' => $max_lifetime,
+ ]);
+ $iRes = parent::gc($max_lifetime);
+ $this->gc_with_time_limit($max_lifetime);
+ return $iRes;
+ }
+
+ /**
+ * @param string $save_path
+ * @param string $session_name
+ */
+ #[ReturnTypeWillChange]
+ public function open($save_path, $session_name) : bool
+ {
+ $bRes = parent::open($save_path, $session_name);
+
+ $session_id = session_id();
+ IssueLog::Debug("Open PHP session", \LogChannels::SESSIONTRACKER, [
+ 'session_id' => $session_id,
+ ]);
+
+ if ($bRes) {
+ $this->touch_session_file($session_id);
+ }
+
+ return $bRes;
+ }
+
+ /**
+ * @param string $session_id
+ * @param string $data
+ *
+ * @return bool
+ */
+ #[ReturnTypeWillChange]
+ public function write($session_id, $data) : bool
+ {
+ $bRes = parent::write($session_id, $data);
+
+ IssueLog::Debug("Write PHP session", \LogChannels::SESSIONTRACKER, [
+ 'session_id' => $session_id,
+ 'data' => $data,
+ ]);
+
+ if ($bRes) {
+ $this->touch_session_file($session_id);
+ }
+
+ return $bRes;
+ }
+
+ public static function session_set_save_handler() : void
+ {
+ if (false === utils::GetConfig()->Get('sessions_tracking.enabled')){
+ //feature disabled
+ return;
+ }
+
+ $sessionhandler = new SessionHandler();
+ session_set_save_handler($sessionhandler, true);
+
+ $iThreshold = utils::GetConfig()->Get('sessions_tracking.gc_threshold');
+ $iThreshold = min(100, $iThreshold);
+ $iThreshold = max(1, $iThreshold);
+ if ((100 != $iThreshold) && (rand(1, 100) > $iThreshold)) {
+ return;
+ }
+
+ $iMaxLifetime = ini_get('session.gc_maxlifetime') ?? 60;
+ $iMaxDurationInSeconds = utils::GetConfig()->Get('sessions_tracking.gc_duration_in_seconds');
+ $sessionhandler->gc_with_time_limit($iMaxLifetime, time() + $iMaxDurationInSeconds);
+ }
+
+ private function generate_session_content(?string $sPreviousFileVersionContent) : ?string
+ {
+ try {
+ $sUserId = UserRights::GetUserId();
+ if (null === $sUserId) {
+ return null;
+ }
+
+ // Default value in case of
+ // - First time file creation
+ // - Data corruption (not a json / not an array / no previous creation_time key)
+ $iCreationTime = time();
+
+ if (! is_null($sPreviousFileVersionContent)) {
+ $aJson = json_decode($sPreviousFileVersionContent, true);
+ if (is_array($aJson) && array_key_exists('creation_time', $aJson)) {
+ $iCreationTime = $aJson['creation_time'];
+ }
+ }
+
+ return json_encode (
+ [
+ 'login_mode' => Session::Get('login_mode'),
+ 'user_id' => $sUserId,
+ 'creation_time' => $iCreationTime,
+ 'context' => implode('|', ContextTag::GetStack())
+ ]
+ );
+ } catch(Exception $e) {
+
+ }
+
+ return null;
+ }
+
+ private function get_file_path($session_id) : string
+ {
+ return utils::GetDataPath() . "sessions/session_$session_id";
+ }
+
+ private function touch_session_file($session_id) : ?string
+ {
+ if (strlen($session_id) == 0) {
+ return null;
+ }
+
+ clearstatcache();
+ if (! is_dir(utils::GetDataPath() . "sessions")) {
+ @mkdir(utils::GetDataPath() . "sessions");
+ }
+
+ $sFilePath = $this->get_file_path($session_id);
+
+ $sPreviousFileVersionContent = null;
+ if (is_file($sFilePath)) {
+ $sPreviousFileVersionContent = file_get_contents($sFilePath);
+ }
+ $sNewContent = $this->generate_session_content($sPreviousFileVersionContent);
+ if (is_null($sNewContent) || ($sPreviousFileVersionContent === $sNewContent)) {
+ @touch($sFilePath);
+ } else {
+ file_put_contents($sFilePath, $sNewContent);
+ }
+
+ return $sFilePath;
+ }
+
+ private function unlink_session_file($session_id)
+ {
+ $sFilePath = $this->get_file_path($session_id);
+ if (is_file($sFilePath)) {
+ @unlink($sFilePath);
+ }
+ }
+
+ /**
+ * @param int $max_lifetime
+ * @param int $iTimeLimit Unix timestamp of time limit not to exceed. -1 for no limit.
+ *
+ * @return int
+ */
+ public function gc_with_time_limit(int $max_lifetime, int $iTimeLimit = -1) : int
+ {
+ $aFiles = $this->list_session_files();
+ $iProcessed = 0;
+ $now = time();
+
+ foreach ($aFiles as $sFile) {
+ if ($now - filemtime($sFile) > $max_lifetime) {
+ @unlink($sFile);
+ $iProcessed++;
+ }
+
+ if (-1 !== $iTimeLimit && time() > $iTimeLimit) {
+ //cleanup processing has to stop after $iTimeLimit
+ break;
+ }
+ }
+
+ return $iProcessed;
+ }
+
+ public function list_session_files() : array
+ {
+ clearstatcache();
+ if (! is_dir(utils::GetDataPath() . "sessions")) {
+ @mkdir(utils::GetDataPath() . "sessions");
+ }
+
+ return glob(utils::GetDataPath() . "sessions/session_*");
+ }
+}
diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php
index c14340cbc..29429f0fe 100644
--- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php
+++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php
@@ -79,6 +79,22 @@ abstract class ItopDataTestCase extends ItopTestCase
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = false;
+ protected static $aURP_Profiles = [
+ 'Administrator' => 1,
+ 'Portal user' => 2,
+ 'Configuration Manager' => 3,
+ 'Service Desk Agent' => 4,
+ 'Support Agent' => 5,
+ 'Problem Manager' => 6,
+ 'Change Implementor' => 7,
+ 'Change Supervisor' => 8,
+ 'Change Approver' => 9,
+ 'Service Manager' => 10,
+ 'Document author' => 11,
+ 'Portal power user' => 12,
+ 'REST Services User' => 1024,
+ ];
+
/**
* This method is called before the first test of this test class is run (in the current process).
*/
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..254c9789e 100644
--- a/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php
@@ -208,4 +208,34 @@ 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);
+ $sConstraintExpected = $bConstraintExpected ? 'true' : 'false';
+ $sComputationExpected = $bComputationExpected ? 'true' : 'false';
+ $this->assertEquals($bConstraintExpected, $oAttDef->HasPHPConstraint(), "Standard DataModel should be configured with property 'has_php_constraint'=$sConstraintExpected for $sClass:$sAttCode");
+ $this->assertEquals($bComputationExpected, $oAttDef->HasPHPComputation(), "Standard DataModel should be configured with property 'has_php_computation'=$sComputationExpected for $sClass:$sAttCode");
+ }
+
+ 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..8ab191336
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/core/DBObject/CheckToWritePropagationTest.php
@@ -0,0 +1,77 @@
+CreateUser($sLogin, self::$aURP_Profiles['Administrator'], 'ABCD1234@gabuzomeu');
+ $sUserId1 = $oUser1->GetKey();
+ $sLogin = 'testCascadeCheckToWrite-'.uniqid('', true);
+ $oUser2 = $this->CreateUser($sLogin, self::$aURP_Profiles['Administrator'], 'ABCD1234@gabuzomeu');
+ $sUserId2 = $oUser2->GetKey();
+
+ $this->EventService_RegisterListener(EVENT_DB_CHECK_TO_WRITE, [$this, 'CheckToWriteEventListener'], 'User');
+ $sEventKeyUser1 = $this->GetEventKey(EVENT_DB_CHECK_TO_WRITE, UserLocal::class, $sUserId1);
+ $sEventKeyUser2 = $this->GetEventKey(EVENT_DB_CHECK_TO_WRITE, UserLocal::class, $sUserId2);
+
+ // Add URP_UserProfile
+ self::$aEventCalls = [];
+ $oURPUserProfile = new URP_UserProfile();
+ $oURPUserProfile->Set('profileid', self::$aURP_Profiles['Support Agent']);
+ $oURPUserProfile->Set('userid', $sUserId1);
+ $oURPUserProfile->Set('reason', 'UNIT Tests');
+ $oURPUserProfile->DBInsert();
+ $this->assertArrayHasKey($sEventKeyUser1, self::$aEventCalls, 'User checkToWrite should be called when a URP_UserProfile is created');
+
+ // Update URP_UserProfile (change profile)
+ self::$aEventCalls = [];
+ $oURPUserProfile->Set('profileid', self::$aURP_Profiles['Problem Manager']);
+ $oURPUserProfile->DBUpdate();
+ $this->assertArrayHasKey($sEventKeyUser1, self::$aEventCalls, 'User checkToWrite should be called when a URP_UserProfile is updated');
+
+ // Update URP_UserProfile (move from User1 to User2)
+ self::$aEventCalls = [];
+ $oURPUserProfile->Set('userid', $sUserId2);
+ $oURPUserProfile->DBUpdate();
+ $this->assertCount(2, self::$aEventCalls, 'Previous User and new User checkToWrite should be called when a URP_UserProfile is moved from a User to another');
+ $this->assertArrayHasKey($sEventKeyUser1, self::$aEventCalls, 'Previous User checkToWrite should be called when a URP_UserProfile is moved from a User to another');
+ $this->assertArrayHasKey($sEventKeyUser2, self::$aEventCalls, 'New User checkToWrite should be called when a URP_UserProfile is moved from a User to another');
+
+ // Delete URP_UserProfile from User2
+ self::$aEventCalls = [];
+ $oURPUserProfile->DBDelete();
+ $this->assertArrayHasKey($sEventKeyUser2, self::$aEventCalls, 'User checkToWrite should be called when a URP_UserProfile is deleted');
+
+ $oUser1->DBDelete();
+ $oUser2->DBDelete();
+ }
+
+ public function CheckToWriteEventListener(EventData $oEventData)
+ {
+ $oObject = $oEventData->GetEventData()['object'];
+ $sEvent = $oEventData->GetEvent();
+ $sClass = get_class($oObject);
+ $sId = $oObject->GetKey();
+ self::$aEventCalls[$this->GetEventKey($sEvent, $sClass, $sId)] = true;
+ }
+
+ private function GetEventKey($sEvent, $sClass, $sId)
+ {
+ return "event: $sEvent, class: $sClass, id: $sId";
+ }
+
+}
diff --git a/tests/php-unit-tests/unitary-tests/core/DBObject/CustomCheckToWriteTest.php b/tests/php-unit-tests/unitary-tests/core/DBObject/CustomCheckToWriteTest.php
new file mode 100644
index 000000000..93cea9f58
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/core/DBObject/CustomCheckToWriteTest.php
@@ -0,0 +1,64 @@
+ [
+ 'aProfiles' => [],
+ 'bExpectedCheckStatus' => false,
+ ],
+ 'Portal power user' => [
+ 'aProfiles' => ['Portal power user',],
+ 'bExpectedCheckStatus' => true,
+ ],
+ 'Portal power user + Configuration Manager' => [
+ 'aProfiles' => ['Portal power user', 'Configuration Manager',],
+ 'bExpectedCheckStatus' => true,
+ ],
+ 'Portal power user + Configuration Manager + Admin' => [
+ 'aProfiles' => ['Portal power user', 'Configuration Manager', 'Administrator',],
+ 'bExpectedCheckStatus' => true,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ * @covers User::CheckPortalProfiles
+ */
+ public function testUserLocalCheckPortalProfiles($aProfiles, $bExpectedCheckStatus)
+ {
+ $oUser = new UserLocal();
+ $sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid('', true);
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+ $oProfileList = $oUser->Get('profile_list');
+
+ foreach ($aProfiles as $sProfileName) {
+ $oAdminUrpProfile = new URP_UserProfile();
+ $oAdminUrpProfile->Set('profileid', self::$aURP_Profiles[$sProfileName]);
+ $oAdminUrpProfile->Set('reason', 'UNIT Tests');
+ $oProfileList->AddItem($oAdminUrpProfile);
+ }
+
+ $oUser->Set('profile_list', $oProfileList);
+
+ [$bCheckStatus, $aCheckIssues, $bSecurityIssue] = $oUser->CheckToWrite();
+ $this->assertEquals($bExpectedCheckStatus, $bCheckStatus);
+ }
+
+}
diff --git a/tests/php-unit-tests/unitary-tests/core/MetaModelTest.php b/tests/php-unit-tests/unitary-tests/core/MetaModelTest.php
index 721d87d52..e60006e22 100644
--- a/tests/php-unit-tests/unitary-tests/core/MetaModelTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/MetaModelTest.php
@@ -33,6 +33,29 @@ class MetaModelTest extends ItopDataTestCase
parent::tearDown();
}
+ /**
+ * @covers MetaModel::GetObjectByName
+ * @return void
+ * @throws \CoreException
+ */
+ public function testGetFinalClassName()
+ {
+ // Standalone classes
+ $this->assertEquals('Organization', MetaModel::GetFinalClassName('Organization', 1), 'Should work with standalone classes');
+ $this->assertEquals('SynchroDataSource', MetaModel::GetFinalClassName('SynchroDataSource', 1), 'Should work with standalone classes');
+
+ // 2 levels hierarchy
+ $this->assertEquals('Person', MetaModel::GetFinalClassName('Contact', 1));
+ $this->assertEquals('Person', MetaModel::GetFinalClassName('Person', 1));
+
+ // multi-level hierarchy
+ $oServer1 = MetaModel::GetObjectByName('Server', 'Server1');
+ $sServer1Id = $oServer1->GetKey();
+ foreach (MetaModel::EnumParentClasses('Server',ENUM_PARENT_CLASSES_ALL) as $sClass) {
+ $this->assertEquals('Server', MetaModel::GetFinalClassName($sClass, $sServer1Id), 'Should return Server for all the classes in the hierarchy');
+ }
+ }
+
/**
* @group itopRequestMgmt
* @covers MetaModel::ApplyParams()
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],
- ];
- }
}
diff --git a/tests/php-unit-tests/unitary-tests/sources/SessionTracker/SessionHandlerTest.php b/tests/php-unit-tests/unitary-tests/sources/SessionTracker/SessionHandlerTest.php
new file mode 100644
index 000000000..ac89a8421
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/sources/SessionTracker/SessionHandlerTest.php
@@ -0,0 +1,241 @@
+aFiles=[];
+ $this->oTag = new ContextTag(ContextTag::TAG_REST);
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ $this->oTag = null;
+
+ foreach ($this->aFiles as $sFile){
+ if (is_file($sFile)){
+ @unlink($sFile);
+ }
+ }
+ }
+
+ private function CreateUserAndLogIn() : ? string {
+ $_SESSION = [];
+ $oUser = $this->CreateContactlessUser("admin" . uniqid(), 1, "1234@Abcdefg");
+
+ \UserRights::Login($oUser->Get('login'));
+ return $oUser->GetKey();
+ }
+
+ private function GenerateSessionContent(SessionHandler $oSessionHandler, ?string $sPreviousFileVersionContent) : ?string {
+ return $this->InvokeNonPublicMethod(SessionHandler::class, "generate_session_content", $oSessionHandler, $aArgs = [$sPreviousFileVersionContent]);
+ }
+
+ /*
+ * @covers SessionHandler::generate_session_content
+ */
+ public function testGenerateSessionContentNoUserLoggedIn(){
+ $oSessionHandler = new SessionHandler();
+ $sContent = $this->GenerateSessionContent($oSessionHandler, null);
+ $this->assertNull($sContent, "Session content should be null when there is no user logged in");
+ }
+
+ public function GenerateSessionContentCorruptedPreviousFileContentProvider() {
+ return [
+ 'not a json' => [ "not a json" ],
+ 'not an array' => [ json_encode("not an array") ],
+ 'array without creation_time key' => [ json_encode([]) ],
+ ];
+ }
+
+ /**
+ * @covers SessionHandler::generate_session_content
+ * @dataProvider GenerateSessionContentCorruptedPreviousFileContentProvider
+ */
+ public function testGenerateSessionContent_SessionFileRepairment(?string $sFileContent){
+ $sUserId = $this->CreateUserAndLogIn();
+
+ $oSessionHandler = new SessionHandler();
+ Session::Set('login_mode', 'foo_login_mode');
+
+ $sContent = $this->GenerateSessionContent($oSessionHandler, $sFileContent);
+
+ $this->assertNotNull($sContent, 'Should not return null');
+ $aJson = json_decode($sContent, true);
+ $this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sContent);
+ $this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sContent");
+ $this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sContent");
+ $this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sContent");
+ $this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sContent");
+ }
+
+ /*
+ * @covers SessionHandler::generate_session_content
+ */
+ public function testGenerateSessionContent(){
+ $sUserId = $this->CreateUserAndLogIn();
+
+ $oSessionHandler = new SessionHandler();
+ Session::Set('login_mode', 'foo_login_mode');
+
+ //first time
+ $sFirstContent = $this->GenerateSessionContent($oSessionHandler, null);
+
+ $this->assertNotNull($sFirstContent, 'Should not return null');
+ $aJson = json_decode($sFirstContent, true);
+ $this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sFirstContent);
+ $this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sFirstContent");
+ $this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sFirstContent");
+ $this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sFirstContent");
+ $this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sFirstContent");
+
+ $iFirstSessionCreationTime = $aJson['creation_time'];
+
+ // Switch context + change user id via impersonation
+ // check it is still tracked in session files
+ $oOtherUser = $this->CreateContactlessUser("admin" . uniqid(), 1, "1234@Abcdefg");
+ $this->assertTrue(\UserRights::Impersonate($oOtherUser->Get('login')), "Failed to execute impersonate on: ".$oOtherUser->Get('login'));
+ $oTag2 = new ContextTag(ContextTag::TAG_SYNCHRO);
+ $sNewContent = $this->GenerateSessionContent($oSessionHandler, $sFirstContent);
+ $this->assertNotNull($sNewContent, 'Should not return null');
+ $aJson = json_decode($sNewContent, true);
+ $this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sNewContent);
+ $this->assertEquals(ContextTag::TAG_REST . '|' . ContextTag::TAG_SYNCHRO, $aJson['context'] ?? '', "After impersonation, should report the new context tags in [context]: $sNewContent");
+ $this->assertEquals($iFirstSessionCreationTime, $aJson['creation_time'] ?? '', "After impersonation, should still report the the session start timestamp in [creation_time]: $sNewContent");
+ $this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "After impersonation, should still report the login mode in [login_mode]: $sNewContent");
+ $this->assertEquals($oOtherUser->GetKey(), $aJson['user_id'] ?? '', "Should report the impersonate user in [user_id]: $sNewContent");
+ }
+
+ private function touchSessionFile(SessionHandler $oSessionHandler, $session_id) : ?string {
+ $sRes = $this->InvokeNonPublicMethod(SessionHandler::class, "touch_session_file", $oSessionHandler, $aArgs = [$session_id]);
+ if (!is_null($sRes) && is_file($sRes)) {
+ // Record the file for cleanup on tearDown
+ $this->aFiles[] = $sRes;
+ }
+ clearstatcache();
+ return $sRes;
+ }
+
+ /*
+ * @covers SessionHandler::touch_session_file
+ */
+ public function testTouchSessionFile_NoUserLoggedIn(){
+ $oSessionHandler = new SessionHandler();
+ $session_id = uniqid();
+ $sFile = $this->touchSessionFile($oSessionHandler, $session_id);
+ $this->assertEquals(true, is_file($sFile), "Should return a file name: '$sFile' is not a valid file name");
+ $sContent = file_get_contents($sFile);
+ $this->assertEquals(null, $sContent, 'Should create an empty file, found: '.$sContent);
+ }
+
+ /*
+ * @covers SessionHandler::touch_session_file
+ */
+ public function testTouchSessionFile_UserLoggedIn(){
+ $sUserId = $this->CreateUserAndLogIn();
+ Session::Set('login_mode', 'foo_login_mode');
+
+ $oSessionHandler = new SessionHandler();
+ $session_id = uniqid();
+ $sFile = $this->touchSessionFile($oSessionHandler, $session_id);
+ $this->assertEquals(true, is_file($sFile), "Should return a file name: '$sFile' is not a valid file name");
+ $sFirstContent = file_get_contents($sFile);
+
+ $iFirstCTime = filectime($sFile) - 1;
+ // Set it in the past to check that it will be further updated (without the need to sleep...)
+ touch($sFile, $iFirstCTime);
+
+ $this->assertNotNull($sFirstContent, 'Should not return null');
+ $aJson = json_decode($sFirstContent, true);
+ $this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sFirstContent);
+ $this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sFirstContent");
+ $this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sFirstContent");
+ $this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sFirstContent");
+ $this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sFirstContent");
+
+ $this->touchSessionFile($oSessionHandler, $session_id);
+ $sNewContent = file_get_contents($sFile);
+ $this->assertEquals($sFirstContent, $sNewContent, 'On successive calls, should not modify an existing session file');
+ $this->assertGreaterThan($iFirstCTime, filectime($sFile), 'On successive calls, should have changed the file ctime');
+ }
+
+ /**
+ * @covers SessionHandler::touch_session_file
+ */
+ public function testTouchSessionFileWithEmptySessionId() {
+ $this->CreateUserAndLogIn();
+ Session::Set('login_mode', 'toto');
+
+ $oSessionHandler = new SessionHandler();
+ $this->assertNull($this->touchSessionFile($oSessionHandler, ''), 'Should return null when session id is an empty string');
+ $this->assertNull($this->touchSessionFile($oSessionHandler, false), 'Should return null when session id (boolean) false');
+ }
+
+ private function GetFilePath(SessionHandler $oSessionHandler, $session_id) : string {
+ $sFile = $this->InvokeNonPublicMethod(SessionHandler::class, "get_file_path", $oSessionHandler, $aArgs = [$session_id]);
+ // Record file for cleanup on tearDown
+ $this->aFiles[] = $sFile;
+ return $sFile;
+ }
+
+ public function GgcWithTimeLimitProvider(){
+ return [
+ 'no cleanup time limit' => [
+ 'iTimeLimit' => -1,
+ 'iExpectedProcessed' => 2
+ ],
+ 'cleanup time limit in the pass => first file removed only' => [
+ 'iTimeLimit' => time() - 1,
+ 'iExpectedProcessed' => 1
+ ],
+ ];
+ }
+
+ /**
+ * @covers SessionHandler::gc_with_time_limit
+ * @covers SessionHandler::list_session_files
+ * @dataProvider GgcWithTimeLimitProvider
+ */
+ public function testGgcWithTimeLimit($iTimeLimit, $iExpectedProcessed) {
+ $oSessionHandler = new SessionHandler();
+ //remove all first
+ $oSessionHandler->gc_with_time_limit(-1);
+ $this->assertEquals([], $oSessionHandler->list_session_files(), 'list_session_files should report no file at startup');
+
+ $max_lifetime = 1440;
+ $iNbExpiredFiles = 2;
+ $iNbFiles = 5;
+ $iExpiredTimeStamp = time() - $max_lifetime - 1;
+ for($i=0; $i<$iNbFiles; $i++) {
+ $sFile = $this->GetFilePath($oSessionHandler, uniqid());
+ file_put_contents($sFile, "fakedata");
+
+ if ($iNbExpiredFiles > 0){
+ $iNbExpiredFiles--;
+ touch($sFile, $iExpiredTimeStamp);
+ }
+ }
+
+ $aFoundSessionFiles = $oSessionHandler->list_session_files();
+ $this->assertEquals($iNbFiles, sizeof($aFoundSessionFiles), 'list_session_files should reports all files');
+ foreach ($aFoundSessionFiles as $sFile){
+ $this->assertTrue(is_file($sFile), 'list_session_files should return a valid file paths, found: '.$sFile);
+ }
+
+ $iProcessed = $oSessionHandler->gc_with_time_limit($max_lifetime, $iTimeLimit);
+ $this->assertEquals($iExpectedProcessed, $iProcessed, 'gc_with_time_limit should report the count of expired files');
+ $this->assertEquals($iNbFiles - $iExpectedProcessed, sizeof($oSessionHandler->list_session_files()), 'gc_with_time_limit should actually remove all processed files');
+ }
+}
diff --git a/webservices/cron.php b/webservices/cron.php
index b39ab8efe..9e8c4bec5 100644
--- a/webservices/cron.php
+++ b/webservices/cron.php
@@ -108,7 +108,7 @@ function RunTask(BackgroundTask $oTask, $iTimeLimit)
// Time in seconds allowed to the task
$iCurrTimeLimit = $iTimeLimit;
// Compute allowed time
- if ($oRefClass->implementsInterface('iScheduledProcess') === false)
+ if ($oRefClass->implementsInterface('iScheduledProcess') === false)
{
// Periodic task, allow only X times ($iMaxTaskExecutionTime) its periodicity (GetPeriodicity())
$iMaxTaskExecutionTime = MetaModel::GetConfig()->Get('cron_task_max_execution_time');
@@ -148,7 +148,7 @@ function RunTask(BackgroundTask $oTask, $iTimeLimit)
$oTask->Set('first_run_date', $oDateStarted->format('Y-m-d H:i:s'));
}
$oTask->ComputeDurations($fDuration); // does increment the counter and compute statistics
-
+
// Update the timestamp since we want to be able to re-order the tasks based on the time they finished
$oDateEnded = new DateTime();
$oTask->Set('latest_run_date', $oDateEnded->format('Y-m-d H:i:s'));