diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php
index 748cb26bc..6fb1987ad 100644
--- a/application/cmdbabstract.class.inc.php
+++ b/application/cmdbabstract.class.inc.php
@@ -47,6 +47,7 @@ use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
use Combodo\iTop\Service\Links\LinkSetDataTransformer;
use Combodo\iTop\Service\Links\LinkSetModel;
+use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectHelper;
define('OBJECT_PROPERTIES_TAB', 'ObjectProperties');
@@ -2818,33 +2819,33 @@ JS
}
}
// Custom operation for the form ?
- if (isset($aExtraParams['custom_operation'])) {
- $sOperation = $aExtraParams['custom_operation'];
- } else {
- if ($this->GetDisplayMode() === static::ENUM_DISPLAY_MODE_EDIT) {
- $sOperation = 'apply_modify';
- } else {
- $sOperation = 'apply_new';
- }
- }
+ if (isset($aExtraParams['custom_operation'])) {
+ $sOperation = $aExtraParams['custom_operation'];
+ } else {
+ if ($this->GetDisplayMode() === static::ENUM_DISPLAY_MODE_EDIT) {
+ $sOperation = 'apply_modify';
+ } else {
+ $sOperation = 'apply_new';
+ }
+ }
- $oContentBlock = new UIContentBlock();
- $oPage->AddUiBlock($oContentBlock);
+ $oContentBlock = new UIContentBlock();
+ $oPage->AddUiBlock($oContentBlock);
- $oForm = new Form("form_{$this->m_iFormId}");
- $oForm->SetAction($sFormAction);
- $sOnSubmitForm = "let bOnSubmitForm = OnSubmit('form_{$this->m_iFormId}');";
- if (isset($aExtraParams['js_handlers']['form_on_submit'])) {
- $oForm->SetOnSubmitJsCode($sOnSubmitForm.$aExtraParams['js_handlers']['form_on_submit']);
- } else {
- $oForm->SetOnSubmitJsCode($sOnSubmitForm."return bOnSubmitForm;");
- }
- $oContentBlock->AddSubBlock($oForm);
+ $oForm = new Form("form_{$this->m_iFormId}");
+ $oForm->SetAction($sFormAction);
+ $sOnSubmitForm = "let bOnSubmitForm = OnSubmit('form_{$this->m_iFormId}');";
+ if (isset($aExtraParams['js_handlers']['form_on_submit'])) {
+ $oForm->SetOnSubmitJsCode($sOnSubmitForm . $aExtraParams['js_handlers']['form_on_submit']);
+ } else {
+ $oForm->SetOnSubmitJsCode($sOnSubmitForm . "return bOnSubmitForm;");
+ }
+ $oContentBlock->AddSubBlock($oForm);
- if ($this->GetDisplayMode() === static::ENUM_DISPLAY_MODE_EDIT) {
- // The object already exists in the database, it's a modification
- $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('id', $iKey, "{$sPrefix}_id"));
- }
+ if ($this->GetDisplayMode() === static::ENUM_DISPLAY_MODE_EDIT) {
+ // The object already exists in the database, it's a modification
+ $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('id', $iKey, "{$sPrefix}_id"));
+ }
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', $sOperation));
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('class', $sClass));
@@ -2853,6 +2854,11 @@ JS
$oPage->SetTransactionId($iTransactionId);
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('transaction_id', $iTransactionId));
+ // Add temporary object watchdog (only on root form)
+ if (!utils::IsXmlHttpRequest()) {
+ $oPage->add_ready_script(TemporaryObjectHelper::GetWatchDogJS($iTransactionId));
+ }
+
// TODO 3.0.0: Is this (the if condition, not the code inside) still necessary?
if (isset($aExtraParams['wizard_container']) && $aExtraParams['wizard_container']) {
$sClassLabel = MetaModel::GetName($sClass);
@@ -2863,34 +2869,34 @@ JS
}
}
- $oToolbarButtons = ToolbarUIBlockFactory::MakeStandard(null);
+ $oToolbarButtons = ToolbarUIBlockFactory::MakeStandard(null);
- $oCancelButton = ButtonUIBlockFactory::MakeForCancel();
- $oCancelButton->AddCSSClasses(['action', 'cancel']);
- $oToolbarButtons->AddSubBlock($oCancelButton);
- $oApplyButton = ButtonUIBlockFactory::MakeForPrimaryAction($sApplyButton, null, null, true);
- $oApplyButton->AddCSSClass('action');
- $oToolbarButtons->AddSubBlock($oApplyButton);
- $bAreTransitionsHidden = isset($aExtraParams['hide_transitions']) && $aExtraParams['hide_transitions'] === true;
- $aTransitions = $this->EnumTransitions();
- if (!isset($aExtraParams['custom_operation']) && !$bAreTransitionsHidden && count($aTransitions)) {
- // Transitions are displayed only for the standard new/modify actions, not for modify_all or any other case...
- $oSetToCheckRights = DBObjectSet::FromObject($this);
+ $oCancelButton = ButtonUIBlockFactory::MakeForCancel();
+ $oCancelButton->AddCSSClasses(['action', 'cancel']);
+ $oToolbarButtons->AddSubBlock($oCancelButton);
+ $oApplyButton = ButtonUIBlockFactory::MakeForPrimaryAction($sApplyButton, null, null, true);
+ $oApplyButton->AddCSSClass('action');
+ $oToolbarButtons->AddSubBlock($oApplyButton);
+ $bAreTransitionsHidden = isset($aExtraParams['hide_transitions']) && $aExtraParams['hide_transitions'] === true;
+ $aTransitions = $this->EnumTransitions();
+ if (!isset($aExtraParams['custom_operation']) && !$bAreTransitionsHidden && count($aTransitions)) {
+ // Transitions are displayed only for the standard new/modify actions, not for modify_all or any other case...
+ $oSetToCheckRights = DBObjectSet::FromObject($this);
- $oTransitionPopoverMenu = new PopoverMenu();
- $sTPMSectionId = 'transitions';
- $oTransitionPopoverMenu->AddSection($sTPMSectionId);
- $aStimuli = Metamodel::EnumStimuli($sClass);
- foreach ($aTransitions as $sStimulusCode => $aTransitionDef) {
- $iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? UserRights::IsStimulusAllowed($sClass,
- $sStimulusCode, $oSetToCheckRights) : UR_ALLOWED_NO;
- switch ($iActionAllowed) {
- case UR_ALLOWED_YES:
- // Button to be displayed on its own on large screens
- $oButton = ButtonUIBlockFactory::MakeForPrimaryAction($aStimuli[$sStimulusCode]->GetLabel(), 'next_action', $sStimulusCode, true);
- $oButton->AddCSSClass('action');
- $oButton->SetColor(Button::ENUM_COLOR_SCHEME_NEUTRAL);
- $oToolbarButtons->AddSubBlock($oButton);
+ $oTransitionPopoverMenu = new PopoverMenu();
+ $sTPMSectionId = 'transitions';
+ $oTransitionPopoverMenu->AddSection($sTPMSectionId);
+ $aStimuli = Metamodel::EnumStimuli($sClass);
+ foreach ($aTransitions as $sStimulusCode => $aTransitionDef) {
+ $iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? UserRights::IsStimulusAllowed($sClass,
+ $sStimulusCode, $oSetToCheckRights) : UR_ALLOWED_NO;
+ switch ($iActionAllowed) {
+ case UR_ALLOWED_YES:
+ // Button to be displayed on its own on large screens
+ $oButton = ButtonUIBlockFactory::MakeForPrimaryAction($aStimuli[$sStimulusCode]->GetLabel(), 'next_action', $sStimulusCode, true);
+ $oButton->AddCSSClass('action');
+ $oButton->SetColor(Button::ENUM_COLOR_SCHEME_NEUTRAL);
+ $oToolbarButtons->AddSubBlock($oButton);
// Button to be displayed in a grouped button on smaller screens
$oTPMPopupMenuItem = new JSPopupMenuItem('next_action--'.$oButton->GetId(), $oButton->GetLabel(), "$(`#{$oButton->GetId()}`).trigger(`click`);");
diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php
index 9f3be25e8..82e53ba80 100644
--- a/application/ui.extkeywidget.class.inc.php
+++ b/application/ui.extkeywidget.class.inc.php
@@ -168,8 +168,6 @@ class UIExtKeyWidget
$sMessage = Dict::S('UI:Message:EmptyList:UseSearchForm');
$sAttrFieldPrefix = ($this->bSearchMode) ? '' : 'attr_';
-
-
$sFilter = addslashes($oAllowedValues->GetFilter()->ToOQL());
if ($this->bSearchMode) {
$sWizHelper = 'null';
@@ -1070,18 +1068,27 @@ JS
{
$oObj = MetaModel::NewObject($this->sTargetClass);
$aErrors = $oObj->UpdateObjectFromPostedForm($this->iId);
- if (count($aErrors) == 0)
- {
- $oObj->DBInsert();
+ if (count($aErrors) == 0) {
+
+ // Retrieve JSON data
+ $sJSON = utils::ReadParam('json', '{}', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
+ $oJSON = json_decode($sJSON);
+
+ $oObj->SetContextSection('temporary_objects', [
+ 'create' => [
+ 'transaction_id' => utils::ReadParam('root_transaction_id', '', false, utils::ENUM_SANITIZATION_FILTER_TRANSACTION_ID),
+ 'host_class' => $oJSON->m_sClass,
+ 'host_att_code' => $this->sAttCode,
+ ],
+ ]);
+ $oObj->DBInsertNoReload();
+
return array('name' => $oObj->GetName(), 'id' => $oObj->GetKey());
- }
- else
- {
+ } else {
return array('error' => implode(' ', $aErrors), 'id' => 0);
}
}
- catch(Exception $e)
- {
+ catch (Exception $e) {
return array('error' => $e->getMessage(), 'id' => 0);
}
}
diff --git a/application/utils.inc.php b/application/utils.inc.php
index 575da4fa1..95edafc1f 100644
--- a/application/utils.inc.php
+++ b/application/utils.inc.php
@@ -3371,5 +3371,22 @@ HTML;
{
return in_array($sTrait, self::TraitsUsedByClass($sClass, true));
}
-
+
+ /**
+ * Get stack trace as string array.
+ *
+ * @return array
+ * @since 3.1.0
+ */
+ public static function GetStackTraceAsArray(): array
+ {
+ $e = new Exception();
+ $aTrace = explode("\n", $e->getTraceAsString());
+ // Remove call to this method
+ array_shift($aTrace);
+ // Remove Main
+ array_pop($aTrace);
+
+ return $aTrace;
+ }
}
diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php
index 88c0faf7c..6fac02369 100644
--- a/core/attributedef.class.inc.php
+++ b/core/attributedef.class.inc.php
@@ -7221,14 +7221,26 @@ class AttributeExternalKey extends AttributeDBFieldVoid
{
return 0;
}
- if (MetaModel::IsValidObject($proposedValue))
- {
+ if (MetaModel::IsValidObject($proposedValue)) {
return $proposedValue->GetKey();
}
return (int)$proposedValue;
}
+ public function WriteExternalValues(DBObject $oHostObject): void
+ {
+ $sTargetKey = $oHostObject->Get($this->GetCode());
+ $oFilter = DBSearch::FromOQL('SELECT `'.TemporaryObjectDescriptor::class.'` WHERE item_class=:class AND item_id=:id');
+ $oSet = new DBObjectSet($oFilter, [], ['class' => $this->GetTargetClass(), 'id' => $sTargetKey]);
+ while ($oTemporaryObjectDescriptor = $oSet->Fetch()) {
+ $oTemporaryObjectDescriptor->Set('host_class', get_class($oHostObject));
+ $oTemporaryObjectDescriptor->Set('host_id', $oHostObject->GetKey());
+ $oTemporaryObjectDescriptor->Set('host_att_code', $this->GetCode());
+ $oTemporaryObjectDescriptor->DBUpdate();
+ }
+ }
+
public function GetMaximumComboLength()
{
return $this->GetOptional('max_combo_length', MetaModel::GetConfig()->Get('max_combo_length'));
diff --git a/core/config.class.inc.php b/core/config.class.inc.php
index 8d38d09be..58677b075 100644
--- a/core/config.class.inc.php
+++ b/core/config.class.inc.php
@@ -137,7 +137,7 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
- 'log_purge.max_keep_days' => [
+ 'log_purge.max_keep_days' => [
'type' => 'integer',
'description' => 'Optional purge number of days to keep logs.',
'default' => 365,
@@ -145,7 +145,7 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
- 'event_service.debug.filter_events' => [
+ 'event_service.debug.filter_events' => [
'type' => 'array',
'description' => 'List of events name to filter Event Service debug messages',
'default' => [],
@@ -153,7 +153,7 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
- 'event_service.debug.filter_sources' => [
+ 'event_service.debug.filter_sources' => [
'type' => 'array',
'description' => 'List of event sources to filter Event Service debug messages',
'default' => '',
@@ -161,6 +161,38 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
+ 'temporary_object.force_creation' => [
+ 'type' => 'bool',
+ 'description' => 'If true, all the objects created by the external key are temporary',
+ 'default' => false,
+ 'value' => false,
+ 'source_of_value' => '',
+ 'show_in_conf_sample' => false,
+ ],
+ 'temporary_object.lifetime' => [
+ 'type' => 'integer',
+ 'description' => 'Seconds for temporary objects created',
+ 'default' => 300,
+ 'value' => 300,
+ 'source_of_value' => '',
+ 'show_in_conf_sample' => false,
+ ],
+ 'temporary_object.watchdog_interval' => [
+ 'type' => 'integer',
+ 'description' => 'Seconds between watchdog signals',
+ 'default' => 60,
+ 'value' => false,
+ 'source_of_value' => '',
+ 'show_in_conf_sample' => false,
+ ],
+ 'temporary_object.garbage_interval' => [
+ 'type' => 'integer',
+ 'description' => 'Seconds between garbage collections',
+ 'default' => 60,
+ 'value' => false,
+ 'source_of_value' => '',
+ 'show_in_conf_sample' => false,
+ ],
'app_env_label' => [
'type' => 'string',
'description' => 'Label displayed to describe the current application environment, defaults to the environment name (e.g. "production")',
@@ -185,7 +217,7 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
- 'db_host' => [
+ 'db_host' => [
'type' => 'string',
'default' => null,
'value' => '',
diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml
index e5a1e74de..d15ca731b 100644
--- a/core/datamodel.core.xml
+++ b/core/datamodel.core.xml
@@ -489,6 +489,12 @@
boolean
false
+
+ create_temporary_object
+ false
+ boolean
+ false
+
on_target_delete
false
diff --git a/core/dbobject.class.php b/core/dbobject.class.php
index a9b10573d..8e1ec6b89 100644
--- a/core/dbobject.class.php
+++ b/core/dbobject.class.php
@@ -7,6 +7,7 @@
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
+use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectManager;
/**
* All objects to be displayed in the application (either as a list or as details)
@@ -194,27 +195,30 @@ abstract class DBObject implements iDisplay
*/
protected static array $m_aCrudStack = [];
+ /** @var array Context for update insert operations */
+ private array $aContext = [];
+
// Protect DBUpdate against infinite loop
protected $iUpdateLoopCount;
const MAX_UPDATE_LOOP_COUNT = 10;
/**
- * DBObject constructor.
- *
- * You should preferably use MetaModel::NewObject() instead of this constructor.
- * The whole collection of parameters is [*optional*] please refer to DBObjectSet::FromRow()
- *
- * @internal The availability of this method is not guaranteed in the long term, you should preferably use MetaModel::NewObject().
- * @see MetaModel::NewObject()
- *
- * @param null|array $aRow If given : DBObjectSet::FromRow() will be used to fetch the object
- * @param string $sClassAlias
- * @param null|array $aAttToLoad
- * @param null|array $aExtendedDataSpec
- *
- * @throws CoreException
- */
+ * DBObject constructor.
+ *
+ * You should preferably use MetaModel::NewObject() instead of this constructor.
+ * The whole collection of parameters is [*optional*] please refer to DBObjectSet::FromRow()
+ *
+ * @internal The availability of this method is not guaranteed in the long term, you should preferably use MetaModel::NewObject().
+ * @see MetaModel::NewObject()
+ *
+ * @param null|array $aRow If given : DBObjectSet::FromRow() will be used to fetch the object
+ * @param string $sClassAlias
+ * @param null|array $aAttToLoad
+ * @param null|array $aExtendedDataSpec
+ *
+ * @throws CoreException
+ */
public function __construct($aRow = null, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null)
{
$this->iUpdateLoopCount = 0;
@@ -1427,7 +1431,7 @@ abstract class DBObject implements iDisplay
}
$sPreview = '';
if(SummaryCardService::IsAllowedForClass($sObjClass) && $bIgnorePreview === false){
- $sPreview = SummaryCardService::GetHyperlinkMarkup($sObjClass, $sObjKey);
+ $sPreview = SummaryCardService::GetHyperlinkMarkup($sObjClass, $sObjKey);
}
$sRet = "$sHLink";
return $sRet;
@@ -3091,13 +3095,12 @@ abstract class DBObject implements iDisplay
$this->AddCurrentObjectInCrudStack('INSERT');
try {
- if (MetaModel::DBIsReadOnly())
- {
- $sErrorMessage = "Cannot Insert object of class '$sClass' because of an ongoing maintenance: the database is in ReadOnly mode";
+ if (MetaModel::DBIsReadOnly()) {
+ $sErrorMessage = "Cannot Insert object of class '$sClass' because of an ongoing maintenance: the database is in ReadOnly mode";
- IssueLog::Error("$sErrorMessage\n".MyHelpers::get_callstack_text(1));
- throw new CoreException("$sErrorMessage (see the log for more information)");
- }
+ IssueLog::Error("$sErrorMessage\n".MyHelpers::get_callstack_text(1));
+ throw new CoreException("$sErrorMessage (see the log for more information)");
+ }
if ($this->m_bIsInDB) {
throw new CoreException('The object already exists into the Database, you may want to use the clone function');
@@ -3131,7 +3134,7 @@ abstract class DBObject implements iDisplay
}
$this->ComputeStopWatchesDeadline(true);
-
+
$iTransactionRetry = 1;
$bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
if ($bIsTransactionEnabled) {
@@ -3170,6 +3173,8 @@ abstract class DBObject implements iDisplay
$this->DBWriteLinks();
$this->WriteExternalAttributes();
+ $this->HandleTemporaryDescriptor();
+
// Write object creation history within the transaction
$this->RecordObjCreation();
@@ -3290,8 +3295,8 @@ abstract class DBObject implements iDisplay
* 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
* order to make sure that the newly cloned object is really distinct from its clone
- *
- * @internal
+ *
+ * @internal
*/
public function __clone()
{
@@ -3300,7 +3305,6 @@ abstract class DBObject implements iDisplay
$this->m_iKey = self::GetNextTempId(get_class($this));
}
-
/**
* Update an object in DB
*
@@ -3439,6 +3443,8 @@ abstract class DBObject implements iDisplay
$this->DBWriteLinks();
$this->WriteExternalAttributes();
+ $this->HandleTemporaryDescriptor();
+
if (count($aChanges) != 0) {
$this->RecordAttChanges($aChanges, $aOriginalValues);
}
@@ -6391,5 +6397,62 @@ abstract class DBObject implements iDisplay
$sPadding = str_pad('', count(self::$m_aCrudStack), '!');
IssueLog::Error("CRUD !!$sPadding Error $sFunction $sClass:$sKey $sComment", LogChannels::DM_CRUD);
}
+
+ /**
+ * @param $aContext
+ *
+ * @return void
+ * @since 3.1.0
+ */
+ private function HandleTemporaryDescriptor()
+ {
+ if ($this->HasContextSection('temporary_objects')) {
+ TemporaryObjectManager::GetInstance()->HandleTemporaryObjects($this, $this->GetContextSection('temporary_objects'));
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function GetContext(): array
+ {
+ return $this->aContext;
+ }
+
+ /**
+ * Set context section data.
+ *
+ * @param string $sSection
+ * @param $value
+ *
+ */
+ public function SetContextSection(string $sSection, $value)
+ {
+ $this->aContext[$sSection] = $value;
+ }
+
+ /**
+ * @param string $sSection
+ *
+ * @return mixed
+ */
+ public function GetContextSection(string $sSection)
+ {
+ if ($this->HasContextSection($sSection)) {
+ return $this->aContext[$sSection];
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string $sSection
+ *
+ * @return bool
+ */
+ public function HasContextSection(string $sSection): bool
+ {
+ return array_key_exists($sSection, $this->aContext);
+ }
}
diff --git a/core/log.class.inc.php b/core/log.class.inc.php
index 223b49d58..a05d0429f 100644
--- a/core/log.class.inc.php
+++ b/core/log.class.inc.php
@@ -612,6 +612,8 @@ class LogChannels
public const PORTAL = 'portal';
+ public const TEMPORARY_OBJECTS = 'TemporaryObjects';
+
/**
* @var string
* @since 3.1.0
diff --git a/datamodels/2.x/itop-structure/module.itop-structure.php b/datamodels/2.x/itop-structure/module.itop-structure.php
index 0b01e08c6..1ddbe981f 100644
--- a/datamodels/2.x/itop-structure/module.itop-structure.php
+++ b/datamodels/2.x/itop-structure/module.itop-structure.php
@@ -22,6 +22,7 @@ SetupWebPage::AddModule(
//
'datamodel' => array(
'main.itop-structure.php',
+ 'src/Model/TemporaryObjectDescriptor.php',
),
'data.struct' => array(
),
diff --git a/datamodels/2.x/itop-structure/src/Model/TemporaryObjectDescriptor.php b/datamodels/2.x/itop-structure/src/Model/TemporaryObjectDescriptor.php
new file mode 100644
index 000000000..f9b2b1920
--- /dev/null
+++ b/datamodels/2.x/itop-structure/src/Model/TemporaryObjectDescriptor.php
@@ -0,0 +1,96 @@
+ 'structure',
+ 'key_type' => 'autoincrement',
+ 'name_attcode' => array('item_class', 'temp_id'),
+ 'image_attcode' => '',
+ 'state_attcode' => '',
+ 'reconc_keys' => array(''),
+ 'db_table' => 'priv_temporary_object_descriptor',
+ 'db_key_field' => 'id',
+ 'db_finalclass_field' => '',
+ 'style' => new ormStyle(null, null, null, null, null, null),
+ 'indexes' => array(
+ 1 =>
+ array(
+ 0 => 'temp_id',
+ ),
+ 2 =>
+ array(
+ 0 => 'item_class',
+ 1 => 'item_id',
+ ),
+ ),
+ );
+ MetaModel::Init_Params($aParams);
+ MetaModel::Init_InheritAttributes();
+ MetaModel::Init_AddAttribute(new AttributeDateTime('expiration_date', array('sql' => 'expiration_date', 'is_null_allowed' => false, 'default_value' => '', 'allowed_values' => null, 'depends_on' => array(), 'always_load_in_tables' => false)));
+ MetaModel::Init_AddAttribute(new AttributeString('temp_id', array('sql' => 'temp_id', 'is_null_allowed' => true, 'default_value' => '', 'allowed_values' => null, 'depends_on' => array(), 'always_load_in_tables' => false)));
+ MetaModel::Init_AddAttribute(new AttributeString('item_class', array('sql' => 'item_class', 'is_null_allowed' => false, 'default_value' => '', 'allowed_values' => null, 'depends_on' => array(), 'always_load_in_tables' => false)));
+ MetaModel::Init_AddAttribute(new AttributeObjectKey('item_id', array('class_attcode' => 'item_class', 'sql' => 'item_id', 'is_null_allowed' => true, 'allowed_values' => null, 'depends_on' => array(), 'always_load_in_tables' => false)));
+ MetaModel::Init_AddAttribute(new AttributeDateTime('creation_date', array('sql' => 'creation_date', 'is_null_allowed' => true, 'default_value' => '', 'allowed_values' => null, 'depends_on' => array(), 'always_load_in_tables' => false)));
+ MetaModel::Init_AddAttribute(new AttributeString('host_class', array('sql' => 'host_class', 'is_null_allowed' => true, 'default_value' => '', 'allowed_values' => null, 'depends_on' => array(), 'always_load_in_tables' => false)));
+ MetaModel::Init_AddAttribute(new AttributeObjectKey('host_id', array('class_attcode' => 'host_class', 'sql' => 'host_id', 'is_null_allowed' => true, 'allowed_values' => null, 'depends_on' => array(), 'always_load_in_tables' => false)));
+ MetaModel::Init_AddAttribute(new AttributeString('host_att_code', array('sql' => 'host_att_code', 'is_null_allowed' => true, 'default_value' => '', 'allowed_values' => null, 'depends_on' => array(), 'always_load_in_tables' => false)));
+ MetaModel::Init_AddAttribute(new AttributeEnum("operation", array("allowed_values" => new ValueSetEnum('create,delete'), "sql" => "operation", "default_value" => "create", "is_null_allowed" => true, "depends_on" => array())));
+
+ MetaModel::Init_SetZListItems('details', array(
+ 0 => 'temp_id',
+ 1 => 'item_class',
+ 2 => 'item_id',
+ 3 => 'creation_date',
+ 4 => 'expiration_date',
+ 5 => 'meta',
+ ));
+ MetaModel::Init_SetZListItems('standard_search', array(
+ 0 => 'temp_id',
+ 1 => 'item_class',
+ 2 => 'item_id',
+ ));
+ MetaModel::Init_SetZListItems('list', array(
+ 0 => 'temp_id',
+ 1 => 'item_class',
+ 2 => 'item_id',
+ 3 => 'creation_date',
+ 4 => 'expiration_date',
+ ));;
+ }
+
+
+ public function DBInsertNoReload()
+ {
+ $this->SetCurrentDateIfNull('creation_date');
+
+ return parent::DBInsertNoReload();
+ }
+
+
+ /**
+ * Set/Update all of the '_item' fields
+ *
+ * @param object $oItem Container item
+ *
+ * @return void
+ */
+ public function SetItem($oItem, $bUpdateOnChange = false)
+ {
+ $sClass = get_class($oItem);
+ $iItemId = $oItem->GetKey();
+
+ $this->Set('item_class', $sClass);
+ $this->Set('item_id', $iItemId);
+ }
+}
diff --git a/js/extkeywidget.js b/js/extkeywidget.js
index 16609ae73..9c792820b 100644
--- a/js/extkeywidget.js
+++ b/js/extkeywidget.js
@@ -651,6 +651,25 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
$('#ac_create_'+me.id).dialog('close');
};
+ /**
+ * Extract transaction id of the root object edited.
+ * When create/update a new object via external key,
+ * this transaction id reflects the root form transaction id an not the current form transaction id.
+ *
+ * @constructor
+ */
+ this.GetRootTransactionId = function(){
+ // Retrieve the object form
+ const oForm = $(`#${me.id}`).closest('form');
+ // If root transaction id exist, then use it
+ let oFieldTransaction = $('input[name=root_transaction_id]', oForm);
+ if(oFieldTransaction.length === 0){
+ // otherwise, use the object form transaction id
+ oFieldTransaction = $('input[name=transaction_id]', oForm);
+ }
+ return oFieldTransaction.val();
+ }
+
this.CreateObject = function (bTargetClassSelected) {
if ($('#'+me.id).prop('disabled')) {
return;
@@ -679,6 +698,10 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
// Run the query and get the result back directly in HTML
var sLocalTargetClass = me.sTargetClass; // Remember the target class since it will be reset when closing the dialog
+
+ // Handle transaction id
+ const sRootFormTransactionId = me.GetRootTransactionId();
+
me.ajax_request = $.post(AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'), theMap,
function (data) {
$('#ajax_'+me.id).html(data);
@@ -696,6 +719,8 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
if ($('#ac_create_'+me.id).height() > ($(window).height()-70)) {
$('#ac_create_'+me.id).height($(window).height()-70);
}
+ // Add root_transaction_id
+ $('#ac_create_'+me.id+' form').append(``)
});
},
'html'
diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php
index 5b700ec56..2bababb02 100644
--- a/lib/composer/autoload_classmap.php
+++ b/lib/composer/autoload_classmap.php
@@ -371,6 +371,7 @@ return array(
'Combodo\\iTop\\Controller\\Links\\LinkSetController' => $baseDir . '/sources/Controller/Links/LinkSetController.php',
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => $baseDir . '/sources/Controller/OAuth/OAuthLandingController.php',
'Combodo\\iTop\\Controller\\PreferencesController' => $baseDir . '/sources/Controller/PreferencesController.php',
+ 'Combodo\\iTop\\Controller\\TemporaryObjects\\TemporaryObjectController' => $baseDir . '/sources/Controller/TemporaryObjects/TemporaryObjectController.php',
'Combodo\\iTop\\Controller\\iController' => $baseDir . '/sources/Controller/iController.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => $baseDir . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
@@ -463,6 +464,12 @@ return array(
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Service/Router/Exception/RouteNotFoundException.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => $baseDir . '/sources/Service/Router/Exception/RouterException.php',
'Combodo\\iTop\\Service\\Router\\Router' => $baseDir . '/sources/Service/Router/Router.php',
+ 'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectConfig' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectConfig.php',
+ 'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectGC' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectGC.php',
+ 'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectHelper' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectHelper.php',
+ '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',
'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 f764ea377..13e569074 100644
--- a/lib/composer/autoload_static.php
+++ b/lib/composer/autoload_static.php
@@ -735,6 +735,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Controller\\Links\\LinkSetController' => __DIR__ . '/../..' . '/sources/Controller/Links/LinkSetController.php',
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthLandingController.php',
'Combodo\\iTop\\Controller\\PreferencesController' => __DIR__ . '/../..' . '/sources/Controller/PreferencesController.php',
+ 'Combodo\\iTop\\Controller\\TemporaryObjects\\TemporaryObjectController' => __DIR__ . '/../..' . '/sources/Controller/TemporaryObjects/TemporaryObjectController.php',
'Combodo\\iTop\\Controller\\iController' => __DIR__ . '/../..' . '/sources/Controller/iController.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
@@ -827,6 +828,12 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouteNotFoundException.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouterException.php',
'Combodo\\iTop\\Service\\Router\\Router' => __DIR__ . '/../..' . '/sources/Service/Router/Router.php',
+ 'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectConfig' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectConfig.php',
+ 'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectGC' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectGC.php',
+ 'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectHelper' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectHelper.php',
+ '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',
'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php',
diff --git a/lib/composer/installed.php b/lib/composer/installed.php
index a69f6ff0d..5f032ca9e 100644
--- a/lib/composer/installed.php
+++ b/lib/composer/installed.php
@@ -5,7 +5,7 @@
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
- 'reference' => '1b7529fcb988f27abcc14834836b047e765323bd',
+ 'reference' => 'dbf3393c9729a20f0bf389d343507238d61fef56',
'name' => 'combodo/itop',
'dev' => true,
),
@@ -25,7 +25,7 @@
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
- 'reference' => '1b7529fcb988f27abcc14834836b047e765323bd',
+ 'reference' => 'dbf3393c9729a20f0bf389d343507238d61fef56',
'dev_requirement' => false,
),
'combodo/tcpdf' => array(
diff --git a/pages/ajax.render.php b/pages/ajax.render.php
index 496e55fb3..ffdb84bc6 100644
--- a/pages/ajax.render.php
+++ b/pages/ajax.render.php
@@ -16,6 +16,7 @@ use Combodo\iTop\Controller\PreferencesController;
use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
use Combodo\iTop\Service\Router\Router;
+use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectManager;
require_once('../approot.inc.php');
@@ -816,6 +817,9 @@ try
$bReleaseLock = iTopOwnershipLock::ReleaseLock($sObjClass, $iObjKey, $sToken);
}
+ // Invalidate temporary objects
+ TemporaryObjectManager::GetInstance()->CancelAllTemporaryObjects($iTransactionId);
+
IssueLog::Trace('on_form_cancel', $sObjClass, array(
'$iObjKey' => $iObjKey,
'$sTransactionId' => $iTransactionId,
@@ -2553,7 +2557,7 @@ EOF
/** @internal */
case 'object.modify':
$oController = new ObjectController();
- $oPage = $oController->Modify();
+ $oPage = $oController->OperationModify();
break;
default:
diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php
index 2eb065477..e25d851b8 100644
--- a/setup/compiler.class.inc.php
+++ b/setup/compiler.class.inc.php
@@ -2083,6 +2083,7 @@ EOF
$this->CompileCommonProperty('min_autocomplete_chars', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('allow_target_creation', $oField, $aParameters, $sModuleRelativeDir);
$this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir, 'select');
+ $this->CompileCommonProperty('create_temporary_object', $oField, $aParameters, $sModuleRelativeDir, false);
$aParameters['depends_on'] = $sDependencies;
} elseif ($sAttType == 'AttributeObjectKey') {
$this->CompileCommonProperty('class_attcode', $oField, $aParameters, $sModuleRelativeDir);
diff --git a/setup/xmldataloader.class.inc.php b/setup/xmldataloader.class.inc.php
index 53da7867d..906389143 100644
--- a/setup/xmldataloader.class.inc.php
+++ b/setup/xmldataloader.class.inc.php
@@ -189,14 +189,17 @@ class XMLDataLoader
function LoadFile($sFilePath, $bUpdateKeyCacheOnly = false, bool $bSearch = false)
{
global $aKeys;
-
+
$oXml = simplexml_load_file($sFilePath);
-
- $aReplicas = array();
- foreach($oXml as $sClass => $oXmlObj)
- {
- if (!MetaModel::IsValidClass($sClass))
- {
+
+ if (!$oXml) {
+ SetupLog::Error("Unable to load xml file - $sFilePath");
+ throw(new Exception("Unable to load xml file - $sFilePath"));
+ }
+
+ $aReplicas = array();
+ foreach ($oXml as $sClass => $oXmlObj) {
+ if (!MetaModel::IsValidClass($sClass)) {
SetupLog::Error("Unknown class - $sClass");
throw(new Exception("Unknown class - $sClass"));
}
diff --git a/sources/Controller/Base/Layout/ObjectController.php b/sources/Controller/Base/Layout/ObjectController.php
index 7479f8a15..1725be8dc 100644
--- a/sources/Controller/Base/Layout/ObjectController.php
+++ b/sources/Controller/Base/Layout/ObjectController.php
@@ -398,24 +398,31 @@ JS;
{
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not created (see $aErrors)', $sClass, array(
'$sTransactionId' => $sTransactionId,
- '$aErrors' => $aErrors,
- '$sUser' => UserRights::GetUser(),
- 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
- 'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
+ '$aErrors' => $aErrors,
+ '$sUser' => UserRights::GetUser(),
+ 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
+ 'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
));
throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors));
}
- $oObj->DBInsertNoReload();// No need to reload
+ // Transactions are now handled in DBInsert
+ $oObj->SetContextSection('temporary_objects', [
+ 'finalize' => [
+ 'transaction_id' => $sTransactionId,
+ ],
+ ]);
+ $oObj->DBInsertNoReload();
+
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object created', $sClass, array(
- '$id' => $oObj->GetKey(),
+ '$id' => $oObj->GetKey(),
'$sTransactionId' => $sTransactionId,
- '$aErrors' => $aErrors,
- '$sUser' => UserRights::GetUser(),
- 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
- 'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
+ '$aErrors' => $aErrors,
+ '$sUser' => UserRights::GetUser(),
+ 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
+ 'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
));
utils::RemoveTransaction($sTransactionId);
@@ -596,23 +603,28 @@ JS;
else
{
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object updated', $sClass, array(
- '$id' => $id,
+ '$id' => $id,
'$sTransactionId' => $sTransactionId,
- '$aErrors' => $aErrors,
- 'IsModified' => $oObj->IsModified(),
- '$sUser' => UserRights::GetUser(),
- 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
- 'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
+ '$aErrors' => $aErrors,
+ 'IsModified' => $oObj->IsModified(),
+ '$sUser' => UserRights::GetUser(),
+ 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
+ 'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
));
- try
- {
- if (!empty($aErrors))
- {
+ try {
+ if (!empty($aErrors)) {
throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors));
}
+
// Transactions are now handled in DBUpdate
+ $oObj->SetContextSection('temporary_objects', [
+ 'finalize' => [
+ 'transaction_id' => $sTransactionId,
+ ],
+ ]);
$oObj->DBUpdate();
+
$sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName());
$sSeverity = 'ok';
if ($this->IsHandlingXmlHttpRequest()) {
diff --git a/sources/Controller/TemporaryObjects/TemporaryObjectController.php b/sources/Controller/TemporaryObjects/TemporaryObjectController.php
new file mode 100644
index 000000000..a7cf862d1
--- /dev/null
+++ b/sources/Controller/TemporaryObjects/TemporaryObjectController.php
@@ -0,0 +1,78 @@
+oTemporaryObjectManager = TemporaryObjectManager::GetInstance();
+ }
+
+ /**
+ * OperationWatchDog.
+ *
+ * Watchdog for delaying expiration date of temporary objects linked to the provided temporary id.
+ *
+ * @return JsonPage
+ */
+ public function OperationWatchDog(): JsonPage
+ {
+ $oPage = new JsonPage();
+
+ // Retrieve temp id
+ $sTempId = utils::ReadParam('temp_id', '', false, utils::ENUM_SANITIZATION_FILTER_STRING);
+
+ // Delay temporary objects expiration
+ $bResult = $this->oTemporaryObjectManager->ExtendTemporaryObjectsLifetime($sTempId);
+
+ return $oPage->SetData([
+ 'success' => $bResult,
+ ]);
+ }
+
+ /**
+ * OperationGarbage.
+ *
+ * Garbage temporary objects based on expiration date.
+ *
+ * @return JsonPage
+ */
+ public function OperationGarbage(): JsonPage
+ {
+ $oPage = new JsonPage();
+
+ // Garbage expired temporary objects
+ $bResult = $this->oTemporaryObjectManager->GarbageExpiredTemporaryObjects();
+
+ return $oPage->SetData([
+ 'success' => $bResult,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/sources/Service/Base/ObjectRepository.php b/sources/Service/Base/ObjectRepository.php
index c2a4ace71..f8ada588a 100644
--- a/sources/Service/Base/ObjectRepository.php
+++ b/sources/Service/Base/ObjectRepository.php
@@ -6,6 +6,7 @@
namespace Combodo\iTop\Service\Base;
+use cmdbAbstractObject;
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
use DBObject;
use DBObjectSearch;
@@ -299,4 +300,40 @@ class ObjectRepository
return ObjectRepository::ComputeOthersData($oObject, $sObjectClass, $aObjectData, $aComplementAttributeSpec, $sObjectImageAttCode);
}
+
+ /**
+ * DeleteFromOql.
+ *
+ * @param string $sOql OQL expression
+ *
+ * @return bool
+ */
+ static public function DeleteFromOql(string $sOql): bool
+ {
+ try {
+
+ // Create db search
+ $oDbObjectSearch = DBSearch::FromOQL($sOql);
+
+ // Create db set from db search
+ $oDbObjectSet = new DBObjectSet($oDbObjectSearch);
+
+ // Delete objects
+ while ($oObject = $oDbObjectSet->Fetch()) {
+ $oObject->DBDelete();
+ }
+
+ // return operation success
+ return true;
+ }
+ catch (Exception $e) {
+
+ ExceptionLog::LogException($e);
+
+ return false;
+ }
+
+ }
+
+
}
\ No newline at end of file
diff --git a/sources/Service/TemporaryObjects/TemporaryObjectConfig.php b/sources/Service/TemporaryObjects/TemporaryObjectConfig.php
new file mode 100644
index 000000000..967157eb9
--- /dev/null
+++ b/sources/Service/TemporaryObjects/TemporaryObjectConfig.php
@@ -0,0 +1,111 @@
+iGarbageInterval = $oConfig->Get(TemporaryObjectHelper::CONFIG_GARBAGE_INTERVAL);
+ $this->iConfigTemporaryLifetime = $oConfig->Get(TemporaryObjectHelper::CONFIG_TEMP_LIFETIME);
+ $this->bConfigTemporaryForce = $oConfig->Get(TemporaryObjectHelper::CONFIG_FORCE);
+ $this->iWatchdogInterval = $oConfig->Get(TemporaryObjectHelper::CONFIG_GARBAGE_INTERVAL);
+ }
+
+ /**
+ * GetInstance.
+ *
+ * @return TemporaryObjectConfig
+ */
+ public static function GetInstance(): TemporaryObjectConfig
+ {
+ if (is_null(self::$oSingletonInstance)) {
+ self::$oSingletonInstance = new TemporaryObjectConfig();
+ }
+
+ return self::$oSingletonInstance;
+ }
+ /**
+ * @return int
+ */
+ public function GetGarbageInterval(): int
+ {
+ return $this->iGarbageInterval;
+ }
+
+ /**
+ * @param int $iGarbageInterval
+ */
+ public function SetGarbageInterval(int $iGarbageInterval): void
+ {
+ $this->iGarbageInterval = $iGarbageInterval;
+ }
+
+ /**
+ * @return int
+ */
+ public function GetConfigTemporaryLifetime(): int
+ {
+ return $this->iConfigTemporaryLifetime;
+ }
+
+ /**
+ * @param int $iConfigTemporaryLifetime
+ */
+ public function SetConfigTemporaryLifetime(int $iConfigTemporaryLifetime): void
+ {
+ $this->iConfigTemporaryLifetime = $iConfigTemporaryLifetime;
+ }
+
+ /**
+ * @return bool
+ */
+ public function GetConfigTemporaryForce(): bool
+ {
+ return $this->bConfigTemporaryForce;
+ }
+
+ /**
+ * @param bool $bConfigTemporaryForce
+ */
+ public function SetConfigTemporaryForce(bool $bConfigTemporaryForce): void
+ {
+ $this->bConfigTemporaryForce = $bConfigTemporaryForce;
+ }
+
+ /**
+ * @return int
+ */
+ public function GetWatchdogInterval(): int
+ {
+ return $this->iWatchdogInterval;
+ }
+
+ /**
+ * @param int $iWatchdogInterval
+ */
+ public function SetWatchdogInterval(int $iWatchdogInterval): void
+ {
+ $this->iWatchdogInterval = $iWatchdogInterval;
+ }
+
+}
\ No newline at end of file
diff --git a/sources/Service/TemporaryObjects/TemporaryObjectGC.php b/sources/Service/TemporaryObjects/TemporaryObjectGC.php
new file mode 100644
index 000000000..67b02e8b1
--- /dev/null
+++ b/sources/Service/TemporaryObjects/TemporaryObjectGC.php
@@ -0,0 +1,45 @@
+oTemporaryObjectManager = TemporaryObjectManager::GetInstance();
+ }
+
+ /** @inheritDoc * */
+ public function GetPeriodicity()
+ {
+ return TemporaryObjectConfig::GetInstance()->GetWatchdogInterval();
+ }
+
+ /** @inheritDoc * */
+ public function Process($iUnixTimeLimit)
+ {
+ // Garbage temporary objects
+ $this->oTemporaryObjectManager->GarbageExpiredTemporaryObjects();
+ }
+}
\ No newline at end of file
diff --git a/sources/Service/TemporaryObjects/TemporaryObjectHelper.php b/sources/Service/TemporaryObjects/TemporaryObjectHelper.php
new file mode 100644
index 000000000..fe1f8f44f
--- /dev/null
+++ b/sources/Service/TemporaryObjects/TemporaryObjectHelper.php
@@ -0,0 +1,45 @@
+GetWatchdogInterval();
+
+ return <<oTemporaryObjectRepository = TemporaryObjectRepository::GetInstance();
+ }
+
+ /**
+ * CreateTemporaryObject.
+ *
+ * @param string $sTempId Temporary id context for the temporary object
+ * @param string $sObjectClass Temporary object class
+ * @param string $sObjectKey Temporary object key
+ * @param string $sOperation temporary operation on file TemporaryObjectHelper::OPERATION_CREATE or TemporaryObjectHelper::OPERATION_DELETE
+ *
+ * @return TemporaryObjectDescriptor|null
+ */
+ public function CreateTemporaryObject(string $sTempId, string $sObjectClass, string $sObjectKey, string $sOperation): ?TemporaryObjectDescriptor
+ {
+ $result = $this->oTemporaryObjectRepository->Create($sTempId, $sObjectClass, $sObjectKey, $sOperation);
+
+ // Log
+ IssueLog::Debug("TemporaryObjectsManager: Create a temporary object attached to temporary id $sTempId", LogChannels::TEMPORARY_OBJECTS, [
+ 'temp_id' => $sTempId,
+ 'item_class' => $sObjectClass,
+ 'item_id' => $sObjectKey,
+ 'succeeded' => $result != null,
+ ]);
+
+ return $result;
+ }
+
+ /**
+ * Cancel the ongoing operation (create or delete) on all the temporary objects impacted by this transaction id
+ *
+ * @param string $sTransactionId form transaction id
+ *
+ * @return bool true if success
+ */
+ public function CancelAllTemporaryObjects(string $sTransactionId): bool
+ {
+ try {
+ // Get temporary object descriptors
+ $oDbObjectSet = $this->oTemporaryObjectRepository->SearchByTempId($sTransactionId, true);
+
+ // Cancel temporary objects...
+ $bResult = $this->CancelTemporaryObjects($oDbObjectSet->ToArray());
+
+ // Log
+ IssueLog::Debug("TemporaryObjectsManager: Cancel all temporary objects attached to temporary id $sTransactionId", LogChannels::TEMPORARY_OBJECTS, [
+ 'temp_id' => $sTransactionId,
+ 'succeeded' => $bResult,
+ ]);
+
+ // return operation success
+ return true;
+ }
+ catch (Exception $e) {
+ ExceptionLog::LogException($e);
+
+ return false;
+ }
+
+ }
+
+ /**
+ * Cancel the ongoing operation (create or delete) on the given temporary objects
+ *
+ * @param array{\TemporaryObjectDescriptor} $aTemporaryObjectDescriptor
+ *
+ * @return bool true if success
+ */
+ private function CancelTemporaryObjects(array $aTemporaryObjectDescriptor): bool
+ {
+ try {
+ // All operations succeeded
+ $bResult = true;
+
+ /** @var TemporaryObjectDescriptor $oTemporaryObjectDescriptor */
+ foreach ($aTemporaryObjectDescriptor as $oTemporaryObjectDescriptor) {
+
+ // Cancel temporary objects
+ if (!$this->CancelTemporaryObject($oTemporaryObjectDescriptor)) {
+ $bResult = false;
+ }
+ }
+
+ return $bResult;
+ }
+ catch (Exception $e) {
+ ExceptionLog::LogException($e);
+
+ return false;
+ }
+
+ }
+
+
+ /**
+ * Extends the temporary object descriptor lifetime
+ *
+ * @param string $sTransactionId
+ *
+ * @return bool
+ */
+ public function ExtendTemporaryObjectsLifetime(string $sTransactionId): bool
+ {
+ try {
+ // Create db set from db search
+ $oDbObjectSet = $this->oTemporaryObjectRepository->SearchByTempId($sTransactionId);
+
+ // Expiration date
+ $iExpirationDate = time() + TemporaryObjectConfig::GetInstance()->GetConfigTemporaryLifetime();
+
+ // Delay objects expiration
+ while ($oObject = $oDbObjectSet->Fetch()) {
+ $oObject->Set('expiration_date', $iExpirationDate);
+ $oObject->DBUpdate();
+ }
+
+ // Log
+ $date = new DateTime();
+ $date->setTimestamp($iExpirationDate);
+ IssueLog::Debug("TemporaryObjectsManager: Delay all temporary objects descriptors expiration date attached to temporary id $sTransactionId", LogChannels::TEMPORARY_OBJECTS, [
+ 'temp_id' => $sTransactionId,
+ 'expiration_date' => date_format($date, 'Y-m-d H:i:s'),
+ 'total_temporary_objects' => $this->oTemporaryObjectRepository->CountTemporaryObjectsByTempId($sTransactionId),
+ ]);
+
+ // return operation success
+ return true;
+ }
+ catch (Exception $e) {
+ ExceptionLog::LogException($e);
+
+ return false;
+ }
+ }
+
+ /**
+ * Accept all the temporary objects operations
+ *
+ * @param string $sTransactionId
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ private function FinalizeTemporaryObjects(string $sTransactionId)
+ {
+ // All operations succeeded
+ $bResult = true;
+
+ // Get temporary object descriptors
+ $oDBObjectSet = $this->oTemporaryObjectRepository->SearchByTempId($sTransactionId, true);
+
+ // Iterate throw descriptors...
+ /** @var TemporaryObjectDescriptor $oTemporaryObjectDescriptor */
+ while ($oTemporaryObjectDescriptor = $oDBObjectSet->Fetch()) {
+ // Retrieve attributes values
+ $sHostClass = $oTemporaryObjectDescriptor->Get('host_class');
+ $sHostId = $oTemporaryObjectDescriptor->Get('host_id');
+
+ // No host object
+ if ($sHostId === 0) {
+ $bResult = $bResult && $this->CancelTemporaryObject($oTemporaryObjectDescriptor);
+ continue;
+ }
+
+ // Host object pointed by descriptor doesn't exist anymore
+ $oHostObject = MetaModel::GetObject($sHostClass, $sHostId, false);
+ if (is_null($oHostObject)) {
+ $bResult = $bResult && $this->CancelTemporaryObject($oTemporaryObjectDescriptor);
+ continue;
+ }
+
+ // Otherwise confirm
+ $bResult = $bResult && $this->ConfirmTemporaryObject($oTemporaryObjectDescriptor);
+ }
+
+ // Log
+ IssueLog::Debug("TemporaryObjectsManager: Finalize all temporary objects attached to temporary id $sTransactionId", LogChannels::TEMPORARY_OBJECTS, [
+ 'temp_id' => $sTransactionId,
+ 'succeeded' => $bResult,
+ ]);
+
+ }
+
+ /**
+ * Accept operation on the given temporary object
+ *
+ * @param TemporaryObjectDescriptor $oTemporaryObjectDescriptor
+ *
+ * @return bool
+ */
+ private function ConfirmTemporaryObject(TemporaryObjectDescriptor $oTemporaryObjectDescriptor): bool
+ {
+ try {
+ // Retrieve attributes values
+ $sOperation = $oTemporaryObjectDescriptor->Get('operation');
+ $sItemClass = $oTemporaryObjectDescriptor->Get('item_class');
+ $sItemId = $oTemporaryObjectDescriptor->Get('item_id');
+
+ // Get temporary object
+ $oTemporaryObject = MetaModel::GetObject($sItemClass, $sItemId);
+
+ if ($sOperation === TemporaryObjectHelper::OPERATION_DELETE) {
+ // Delete temporary object
+ $oTemporaryObject->DBDelete();
+ IssueLog::Info("Temporary Object [$sItemClass:$sItemId] removed (operation: $sOperation)", LogChannels::TEMPORARY_OBJECTS, utils::GetStackTraceAsArray());
+ } elseif ($sOperation === TemporaryObjectHelper::OPERATION_CREATE) {
+ // Send an event in case of creation confirmation
+ $oTemporaryObject->FireEvent(TemporaryObjectsEvents::TEMPORARY_OBJECT_EVENT_CONFIRM_CREATE);
+ }
+
+ // Remove temporary object descriptor entry
+ $oTemporaryObjectDescriptor->DBDelete();
+
+ // Log
+ IssueLog::Debug("Temporary Object [$sItemClass:$sItemId] $sOperation confirmed", LogChannels::TEMPORARY_OBJECTS, utils::GetStackTraceAsArray());
+
+ return true;
+ }
+ catch (Exception $e) {
+ ExceptionLog::LogException($e);
+
+ return false;
+ }
+ }
+
+ /**
+ * CancelTemporaryObject.
+ *
+ * @param TemporaryObjectDescriptor $oTemporaryObjectDescriptor
+ *
+ * @return bool
+ */
+ private function CancelTemporaryObject(TemporaryObjectDescriptor $oTemporaryObjectDescriptor): bool
+ {
+ try {
+ // Retrieve attributes values
+ $sOperation = $oTemporaryObjectDescriptor->Get('operation');
+ $sItemClass = $oTemporaryObjectDescriptor->Get('item_class');
+ $sItemId = $oTemporaryObjectDescriptor->Get('item_id');
+
+ if ($sOperation === TemporaryObjectHelper::OPERATION_CREATE) {
+
+ // Get temporary object
+ $oTemporaryObject = MetaModel::GetObject($sItemClass, $sItemId, false);
+
+ // Delete temporary object
+ if (!is_null($oTemporaryObject)) {
+ $oTemporaryObject->DBDelete();
+ }
+
+ IssueLog::Info("Temporary Object [$sItemClass:$sItemId] removed (operation: $sOperation)", LogChannels::TEMPORARY_OBJECTS, utils::GetStackTraceAsArray());
+ }
+
+ // Remove temporary object descriptor entry
+ $oTemporaryObjectDescriptor->DBDelete();
+
+ return true;
+ }
+ catch (Exception $e) {
+ ExceptionLog::LogException($e);
+
+ return false;
+ }
+ }
+
+ /**
+ * Handle temporary objects.
+ *
+ * @param \DBObject $oDBObject
+ * @param array $aContext
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public function HandleTemporaryObjects(DBObject $oDBObject, array $aContext)
+ {
+ if (array_key_exists('create', $aContext)) {
+ // Retrieve context information
+ $aContextCreation = $aContext['create'];
+ $sTransactionId = $aContextCreation['transaction_id'] ?? null;
+ $sHostClass = $aContextCreation['host_class'] ?? null;
+ $sHostAttCode = $aContextCreation['host_att_code'] ?? null;
+
+ // Security
+ if (is_null($sTransactionId) || is_null($sHostClass) || is_null($sHostAttCode)) {
+ return;
+ }
+
+ // Get host class attribute definition
+ try {
+ $oAttDef = MetaModel::GetAttributeDef($sHostClass, $sHostAttCode);
+ }
+ catch (Exception $e) {
+ ExceptionLog::LogException($e);
+
+ return;
+ }
+
+ // If creation as temporary object requested or force for all objects
+ if (($oAttDef->IsParam('create_temporary_object') && $oAttDef->Get('create_temporary_object'))
+ || TemporaryObjectConfig::GetInstance()->GetConfigTemporaryForce()) {
+
+ $this->CreateTemporaryObject($sTransactionId, get_class($oDBObject), $oDBObject->GetKey(), TemporaryObjectHelper::OPERATION_CREATE);
+ }
+ }
+ if (array_key_exists('finalize', $aContext)) {
+ // Retrieve context information
+ $aContextFinalize = $aContext['finalize'];
+ $sTransactionId = $aContextFinalize['transaction_id'] ?? null;
+
+ // validate temporary objects
+ $this->FinalizeTemporaryObjects($sTransactionId);
+ }
+ }
+
+ /**
+ * GarbageExpiredTemporaryObjects.
+ *
+ * @return bool
+ */
+ public function GarbageExpiredTemporaryObjects(): bool
+ {
+ try {
+ // Search for expired temporary objects
+ $oDBObjectSet = $this->oTemporaryObjectRepository->SearchByExpired();
+
+ // Cancel temporary objects
+ $this->CancelTemporaryObjects($oDBObjectSet->ToArray());
+
+ return true;
+ }
+ catch (Exception $e) {
+ ExceptionLog::LogException($e);
+
+ return false;
+ }
+ }
+}
diff --git a/sources/Service/TemporaryObjects/TemporaryObjectRepository.php b/sources/Service/TemporaryObjects/TemporaryObjectRepository.php
new file mode 100644
index 000000000..1f3a76b99
--- /dev/null
+++ b/sources/Service/TemporaryObjects/TemporaryObjectRepository.php
@@ -0,0 +1,203 @@
+ $sOperation,
+ 'temp_id' => $sTempId,
+ 'expiration_date' => time() + TemporaryObjectConfig::GetInstance()->GetConfigTemporaryLifetime(),
+ 'item_class' => $sObjectClass,
+ 'item_id' => $sObjectKey,
+ ]);
+ $oTemporaryObjectDescriptor->DBInsert();
+
+ return $oTemporaryObjectDescriptor;
+ }
+ catch (Exception $e) {
+
+ ExceptionLog::LogException($e);
+
+ return null;
+ }
+ }
+
+ /**
+ * SearchByTempId.
+ *
+ * @param string $sTempId temporary id
+ * @param bool $bReverseOrder reverse order of result
+ *
+ * @return \DBObjectSet
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public function SearchByTempId(string $sTempId, bool $bReverseOrder = false): DBObjectSet
+ {
+ // Prepare OQL
+ $sOQL = sprintf('SELECT `%s` WHERE temp_id=:temp_id', TemporaryObjectDescriptor::class);
+
+ // Create db search
+ $oDbObjectSearch = DBSearch::FromOQL($sOQL);
+
+ // Create db set from db search
+ $oDbObjectSet = new DBObjectSet($oDbObjectSearch, [], [
+ 'temp_id' => $sTempId,
+ ]);
+
+ // Reverse order
+ if ($bReverseOrder) {
+ $oDbObjectSet->SetOrderBy([
+ 'id' => false,
+ ]);
+ }
+
+ return $oDbObjectSet;
+ }
+
+ /**
+ * SearchByItem.
+ *
+ * @param string $sItemClass
+ * @param string $sItemId
+ * @param bool $bReverseOrder reverse order of result
+ *
+ * @return \DBObjectSet
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public function SearchByItem(string $sItemClass, string $sItemId, bool $bReverseOrder = false): DBObjectSet
+ {
+ // Prepare OQL
+ $sOQL = sprintf('SELECT `%s` WHERE item_class=:item_class AND item_id=:item_id', TemporaryObjectDescriptor::class);
+
+ // Create db search
+ $oDbObjectSearch = DBSearch::FromOQL($sOQL);
+
+ // Create db set from db search
+ $oDbObjectSet = new DBObjectSet($oDbObjectSearch, [], [
+ 'item_class' => $sItemClass,
+ 'item_id' => $sItemId,
+ ]);
+
+ // Reverse order
+ if ($bReverseOrder) {
+ $oDbObjectSet->SetOrderBy([
+ 'id' => false,
+ ]);
+ }
+
+ return $oDbObjectSet;
+ }
+
+ /**
+ * CountTemporaryObjectsByTempId.
+ *
+ * @param string $sTempId
+ *
+ * @return int
+ */
+ public function CountTemporaryObjectsByTempId(string $sTempId): int
+ {
+ try {
+
+ $oDbObjectSet = $this->SearchByTempId($sTempId);
+
+ // return operation success
+ return $oDbObjectSet->count();
+ }
+ catch (Exception $e) {
+
+ ExceptionLog::LogException($e);
+
+ return -1;
+ }
+ }
+
+ /**
+ * SearchByExpired.
+ *
+ * @return DBObjectSet
+ * @throws \OQLException
+ */
+ public function SearchByExpired(): DBObjectSet
+ {
+ // Prepare OQL
+ $sOQL = sprintf('SELECT `%s` WHERE expiration_date<:now', TemporaryObjectDescriptor::class);
+
+ // Create db search
+ $oDbObjectSearch = DBSearch::FromOQL($sOQL);
+
+ // Create db set from db search
+ $sDateNow = date(AttributeDateTime::GetSQLFormat(), time());
+
+ return new DBObjectSet($oDbObjectSearch, ['id' => false], ['now' => $sDateNow]);
+ }
+}
diff --git a/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php b/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php
new file mode 100644
index 000000000..1d0408e59
--- /dev/null
+++ b/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php
@@ -0,0 +1,47 @@
+ 'cmdbAbstractObject',
+ ],
+ 'The MetaModel is fully started',
+ '',
+ [
+ new EventDataDescription(
+ 'object',
+ 'The object concerned by the creation confirmation',
+ 'DBObject',
+ ),
+ new EventDataDescription(
+ 'debug_info',
+ 'Debug string',
+ 'string',
+ ),
+ ],
+ 'application'));
+ }
+
+}
\ No newline at end of file
diff --git a/tests/php-unit-tests/unitary-tests/sources/Service/TemporaryObjects/TemporaryObjectManagerTest.php b/tests/php-unit-tests/unitary-tests/sources/Service/TemporaryObjects/TemporaryObjectManagerTest.php
new file mode 100644
index 000000000..86c49529d
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/sources/Service/TemporaryObjects/TemporaryObjectManagerTest.php
@@ -0,0 +1,172 @@
+oConfig = TemporaryObjectConfig::GetInstance();
+ $this->oManager = TemporaryObjectManager::GetInstance();
+ }
+
+ public function testCreateTemporaryObject()
+ {
+ $sTempId = 'testCreateTemporaryObject';
+ $this->oConfig->SetConfigTemporaryLifetime(3000);
+ $this->oConfig->SetConfigTemporaryForce(true);
+
+ $oDescriptor = $this->oManager->CreateTemporaryObject($sTempId, 'FakedClass', -1, TemporaryObjectHelper::OPERATION_CREATE);
+
+ $this->assertNull( $oDescriptor);
+
+ $oOrg = $this->CreateTestOrganization();
+ $oDescriptor = $this->CreateTemporaryObject($sTempId, $oOrg, 3000, TemporaryObjectHelper::OPERATION_CREATE);
+
+ $this->assertNotNull( $oDescriptor);
+ }
+
+ public function testCancelAllTemporaryObjects()
+ {
+ $sTempId = 'testCancelAllTemporaryObjects';
+ $oRepository = TemporaryObjectRepository::GetInstance();
+
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, 3000, TemporaryObjectHelper::OPERATION_CREATE);
+ $this->assertEquals(1, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+
+ $this->oManager->CancelAllTemporaryObjects($sTempId);
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, 3000, TemporaryObjectHelper::OPERATION_CREATE);
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, 3000, TemporaryObjectHelper::OPERATION_CREATE);
+ $this->assertEquals(2, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+
+ $this->oManager->CancelAllTemporaryObjects($sTempId);
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+ }
+
+ public function testExtendTemporaryObjectsLifetime()
+ {
+ $sTempId = 'testExtendTemporaryObjectsLifetime';
+ $oRepository = TemporaryObjectRepository::GetInstance();
+
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, -1, TemporaryObjectHelper::OPERATION_CREATE);
+ $this->assertEquals(1, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+ $this->assertEquals(1, $oRepository->SearchByExpired()->Count());
+
+ $this->oConfig->SetConfigTemporaryLifetime(3000);
+ $this->oManager->ExtendTemporaryObjectsLifetime($sTempId);
+ $this->assertEquals(0, $oRepository->SearchByExpired()->Count());
+ }
+
+ public function testGarbageExpiredTemporaryObjects()
+ {
+ $sTempId = 'testGarbageExpiredTemporaryObjects';
+ $oRepository = TemporaryObjectRepository::GetInstance();
+
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, -1, TemporaryObjectHelper::OPERATION_CREATE);
+ $this->assertEquals(1, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+ $this->assertEquals(1, $oRepository->SearchByExpired()->Count());
+
+ $this->oManager->GarbageExpiredTemporaryObjects();
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, -1, TemporaryObjectHelper::OPERATION_CREATE);
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, -1, TemporaryObjectHelper::OPERATION_CREATE);
+ $this->assertEquals(2, $oRepository->SearchByExpired()->Count());
+
+ $this->oManager->GarbageExpiredTemporaryObjects();
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+
+ $this->oManager->GarbageExpiredTemporaryObjects();
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+ }
+
+ public function testHandleCreatedTemporaryObjects()
+ {
+ $sTempId = 'testHandleTemporaryObjects';
+ $oRepository = TemporaryObjectRepository::GetInstance();
+
+ $oOrg = $this->CreateTestOrganization();
+ $oOrgTemp = $this->CreateTestOrganization();
+ $oOrg->Set('parent_id', $oOrgTemp->GetKey());
+ $oOrg->DBUpdate();
+
+ $aContext = ['create' => ['transaction_id' => $sTempId, 'host_class' => get_class($oOrg), 'host_att_code' => 'parent_id',]];
+ $this->oConfig->SetConfigTemporaryForce(true);
+ $this->oConfig->SetConfigTemporaryLifetime(3000);
+
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+ $this->oManager->HandleTemporaryObjects($oOrg, $aContext);
+ $this->assertEquals(1, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+
+ $aContext = ['finalize' => ['transaction_id' => $sTempId,]];
+ $this->oManager->HandleTemporaryObjects($oOrg, $aContext);
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+ }
+
+ public function testHandleDeletedTemporaryObjects()
+ {
+ $sTempId = 'testHandleTemporaryObjectsDelete';
+ $oRepository = TemporaryObjectRepository::GetInstance();
+
+ $oOrg = $this->CreateTestOrganization();
+ $oOrgTemp = $this->CreateTestOrganization();
+ $oOrg->Set('parent_id', $oOrgTemp->GetKey());
+ $oOrg->DBUpdate();
+
+ // Create a temporary delete
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+ $oTemporaryObjectDescriptor = TemporaryObjectManager::GetInstance()->CreateTemporaryObject($sTempId, get_class($oOrgTemp), $oOrgTemp->Get('id'), TemporaryObjectHelper::OPERATION_DELETE);
+ $oTemporaryObjectDescriptor->Set('host_class', get_class($oOrg));
+ $oTemporaryObjectDescriptor->Set('host_id', $oOrg->GetKey());
+ $oTemporaryObjectDescriptor->Set('host_att_code', 'parent_id');
+ $oTemporaryObjectDescriptor->DBUpdate();
+ $this->assertEquals(1, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+
+ $aContext = ['finalize' => ['transaction_id' => $sTempId,]];
+ $this->oManager->HandleTemporaryObjects($oOrg, $aContext);
+ $this->assertEquals(0, $oRepository->CountTemporaryObjectsByTempId($sTempId));
+ $oDeletedObject = \MetaModel::GetObject(get_class($oOrgTemp), $oOrgTemp->Get('id'), false);
+ $this->assertNull($oDeletedObject);
+ }
+
+
+ private function CreateTemporaryObject($sTempId, $oDBObject, int $iLifetime, string $sOperation)
+ {
+ $this->oConfig->SetConfigTemporaryLifetime($iLifetime);
+ $this->oConfig->SetConfigTemporaryForce(true);
+
+ return $this->oManager->CreateTemporaryObject($sTempId, get_class($oDBObject), $oDBObject->GetKey(), $sOperation);
+ }
+}
diff --git a/tests/php-unit-tests/unitary-tests/sources/Service/TemporaryObjects/TemporaryObjectRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Service/TemporaryObjects/TemporaryObjectRepositoryTest.php
new file mode 100644
index 000000000..c560ee7de
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/sources/Service/TemporaryObjects/TemporaryObjectRepositoryTest.php
@@ -0,0 +1,103 @@
+oTemporaryObjectConfig = TemporaryObjectConfig::GetInstance();
+ }
+
+ public function testSearchByExpired()
+ {
+ $sTempId = 'testSearchByExpired';
+
+ $oOrg = $this->CreateTestOrganization();
+ $oRepository = TemporaryObjectRepository::GetInstance();
+ $this->CreateTemporaryObject($sTempId, $oOrg, 3000);
+ $oObjectSet = $oRepository->SearchByExpired();
+ $this->assertEquals(0, $oObjectSet->Count());
+
+ $this->CreateTemporaryObject($sTempId, $oOrg, -1);
+ $oObjectSet = $oRepository->SearchByExpired();
+ $this->assertEquals(1, $oObjectSet->Count());
+
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, -1);
+ $oObjectSet = $oRepository->SearchByExpired();
+ $this->assertEquals(2, $oObjectSet->Count());
+ }
+
+ public function testSearchByTempId()
+ {
+ $sTempId = 'testSearchByTempId';
+
+ // First temp object
+ $oOrg = $this->CreateTestOrganization();
+ $oDescriptor = $this->CreateTemporaryObject($sTempId, $oOrg, 3000);
+ $oRepository = TemporaryObjectRepository::GetInstance();
+ $oObjectSet = $oRepository->SearchByTempId($sTempId);
+ $this->assertEquals(1, $oObjectSet->Count());
+ $oDBObject = $oObjectSet->Fetch();
+ $this->assertEquals($oDescriptor->GetKey(), $oDBObject->GetKey());
+ $this->assertEquals(get_class($oDescriptor), get_class($oDBObject));
+
+ // Second temp object
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, 3000);
+ $oObjectSet = $oRepository->SearchByTempId($sTempId);
+ $this->assertEquals(2, $oObjectSet->Count());
+ }
+
+ public function testCountTemporaryObjectsByTempId()
+ {
+ $sTempId = 'testCountTemporaryObjectsByTempId';
+
+ // First temp object
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, 3000);
+ $oRepository = TemporaryObjectRepository::GetInstance();
+ $iCount = $oRepository->CountTemporaryObjectsByTempId($sTempId);
+ $this->assertEquals(1, $iCount);
+
+ // Second temp object
+ $oOrg = $this->CreateTestOrganization();
+ $this->CreateTemporaryObject($sTempId, $oOrg, 3000);
+ $iCount = $oRepository->CountTemporaryObjectsByTempId($sTempId);
+ $this->assertEquals(2, $iCount);
+ }
+
+ private function CreateTemporaryObject($sTempId, DBObject $oDBObject, int $iLifetime)
+ {
+ $this->oTemporaryObjectConfig->SetConfigTemporaryLifetime($iLifetime);
+ $this->oTemporaryObjectConfig->SetConfigTemporaryForce(true);
+
+ $oManager = TemporaryObjectManager::GetInstance();
+
+ return $oManager->CreateTemporaryObject($sTempId, get_class($oDBObject), $oDBObject->GetKey(), TemporaryObjectHelper::OPERATION_CREATE);
+ }
+}