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); + } +}