From 9bee4bdb5c21771934954f183b1daa55c741a573 Mon Sep 17 00:00:00 2001 From: Potherca-Bot Date: Tue, 2 Sep 2025 16:42:12 +0000 Subject: [PATCH 1/2] Adds separate file for 'CellStatus_Ambiguous.php'. --- .../CellStatus_Ambiguous.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/{bulkchange.class.inc.php => bulkchange/CellStatus_Ambiguous.php} (100%) diff --git a/core/bulkchange.class.inc.php b/core/bulkchange/CellStatus_Ambiguous.php similarity index 100% rename from core/bulkchange.class.inc.php rename to core/bulkchange/CellStatus_Ambiguous.php From 6f5bed6b98339c7d7676f6c855937ffbb2201a81 Mon Sep 17 00:00:00 2001 From: Potherca-Bot Date: Tue, 2 Sep 2025 16:42:13 +0000 Subject: [PATCH 2/2] Changes content in separated file 'CellStatus_Ambiguous.php'. --- core/bulkchange/CellStatus_Ambiguous.php | 1708 +--------------------- 1 file changed, 39 insertions(+), 1669 deletions(-) diff --git a/core/bulkchange/CellStatus_Ambiguous.php b/core/bulkchange/CellStatus_Ambiguous.php index 1865fafc4..171e87bcc 100644 --- a/core/bulkchange/CellStatus_Ambiguous.php +++ b/core/bulkchange/CellStatus_Ambiguous.php @@ -1,1673 +1,43 @@ 2007, Windows only) in changing its interpretation of a CSV file (by default Excel reads data as ISO-8859-1 -not 100% sure!) -use Combodo\iTop\Application\WebPage\iTopWebPage; -use Combodo\iTop\Application\WebPage\WebPage; - -define('UTF8_BOM', chr(239).chr(187).chr(191)); // 0xEF, 0xBB, 0xBF - - -/** - * CellChangeSpec - * A series of classes, keeping the information about a given cell: could it be changed or not (and why)? - * - * @package iTopORM - */ -abstract class CellChangeSpec -{ - protected $m_proposedValue; - protected $m_sOql; // in case of ambiguity - - public function __construct($proposedValue, $sOql = '') - { - $this->m_proposedValue = $proposedValue; - $this->m_sOql = $sOql; - } - - public function GetPureValue() - { - // Todo - distinguish both values - return $this->m_proposedValue; - } - - /** - * @throws \Exception - * @since 3.2.0 - */ - public function GetCLIValue(bool $bLocalizedValues = false): string - { - if (is_object($this->m_proposedValue)) { - if ($this->m_proposedValue instanceof ReportValue) { - return $this->m_proposedValue->GetAsCSV($bLocalizedValues, ',', '"'); - } - throw new Exception('Unexpected class : '. get_class($this->m_proposedValue)); - } - return $this->m_proposedValue; - } - - /** - * @throws \Exception - * @since 3.2.0 - */ - public function GetHTMLValue(bool $bLocalizedValues = false): string - { - if (is_object($this->m_proposedValue)) { - if ($this->m_proposedValue instanceof ReportValue) { - return $this->m_proposedValue->GetAsHTML($bLocalizedValues); - } - throw new Exception('Unexpected class : '. get_class($this->m_proposedValue)); - } - return utils::EscapeHtml($this->m_proposedValue); - } - - - /** - * @since 3.1.0 N°5305 - */ - public function SetDisplayableValue(string $sDisplayableValue) - { - $this->m_proposedValue = $sDisplayableValue; - } - - public function GetOql() - { - return $this->m_sOql; - } - - /** - * @since 3.2.0 - */ - public function GetCLIValueAndDescription(): string - { - return sprintf("%s%s", - $this->GetCLIValue(), - $this->GetDescription() - ); - } - - abstract public function GetDescription(); -} - - -class CellStatus_Void extends CellChangeSpec -{ - public function GetDescription() - { - return ''; - } -} - -class CellStatus_Modify extends CellChangeSpec -{ - protected $m_previousValue; - - public function __construct($proposedValue, $previousValue = null) - { - // Unused (could be costly to know -see the case of reconciliation on ext keys) - //$this->m_previousValue = $previousValue; - parent::__construct($proposedValue); - } - - public function GetDescription() - { - return Dict::S('UI:CSVReport-Value-Modified'); - } - - //public function GetPreviousValue() - //{ - // return $this->m_previousValue; - //} -} - -class CellStatus_Issue extends CellStatus_Modify -{ - protected $m_sReason; - - public function __construct($proposedValue, $previousValue, $sReason) - { - $this->m_sReason = $sReason; - parent::__construct($proposedValue, $previousValue); - } - - public function GetCLIValue(bool $bLocalizedValues = false): string - { - if (is_null($this->m_proposedValue)) { - return Dict::Format('UI:CSVReport-Value-SetIssue'); - } - return Dict::Format('UI:CSVReport-Value-ChangeIssue',$this->m_proposedValue); - } - - public function GetHTMLValue(bool $bLocalizedValues = false): string - { - if (is_null($this->m_proposedValue)) - { - return Dict::Format('UI:CSVReport-Value-SetIssue'); - } - if ($this->m_proposedValue instanceof ReportValue) - { - return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue->GetAsHTML($bLocalizedValues)); - } - return Dict::Format('UI:CSVReport-Value-ChangeIssue',utils::EscapeHtml($this->m_proposedValue)); - } - - public function GetDescription() - { - return $this->m_sReason; - } - /* - * @since 3.2.0 - */ - public function GetCLIValueAndDescription(): string - { - return sprintf("%s. %s", - $this->GetCLIValue(), - $this->GetDescription() - ); - } -} - -class CellStatus_SearchIssue extends CellStatus_Issue -{ - /** @var string|null $m_sAllowedValues */ - private $m_sAllowedValues; - - /** - * @since 3.1.0 N°5305 - * @var string $sSerializedSearch - */ - private $sSerializedSearch; - - /** @var string|null $m_sTargetClass */ - private $m_sTargetClass; - - /** - * @since 3.1.0 N°5305 - * @var string $sAllowedValuesSearch - */ - private $sAllowedValuesSearch; - - /** - * CellStatus_SearchIssue constructor. - * @since 3.1.0 N°5305 - * - * @param string $sOql : main message - * @param string $sReason : main message - * @param null $sClass : used for additional message that provides allowed values for current class $sClass - * @param null $sAllowedValues : used for additional message that provides allowed values $sAllowedValues for current class - * @param string|null $sAllowedValuesSearch : used to search all allowed values - */ - public function __construct($sSerializedSearch, $sReason, $sClass = null, $sAllowedValues = null, string $sAllowedValuesSearch = null) - { - parent::__construct(null, null, $sReason); - $this->sSerializedSearch = $sSerializedSearch; - $this->m_sAllowedValues = $sAllowedValues; - $this->m_sTargetClass = $sClass; - $this->sAllowedValuesSearch = $sAllowedValuesSearch; - } - - public function GetCLIValue(bool $bLocalizedValues = false): string - { - if (null === $this->m_sReason) { - return Dict::Format('UI:CSVReport-Value-NoMatch', ''); - } - - return $this->m_sReason; - } - - public function GetHTMLValue(bool $bLocalizedValues = false): string - { - if (null === $this->m_sReason) { - return Dict::Format('UI:CSVReport-Value-NoMatch', ''); - } - - return utils::EscapeHtml($this->m_sReason); - } - - public function GetDescription() - { - if (\utils::IsNullOrEmptyString($this->m_sAllowedValues) || - \utils::IsNullOrEmptyString($this->m_sTargetClass)) { - return ''; - } - - return Dict::Format('UI:CSVReport-Value-NoMatch-PossibleValues', $this->m_sTargetClass, $this->m_sAllowedValues); - } - - /** - * @since 3.1.0 N°5305 - * @return string - */ - public function GetSearchLinkUrl() - { - return sprintf("UI.php?operation=search&filter=%s", - rawurlencode($this->sSerializedSearch ?? "") - ); - } - - /** - * @since 3.1.0 N°5305 - * @return null|string - */ - public function GetAllowedValuesLinkUrl(): ?string - { - return sprintf("UI.php?operation=search&filter=%s", - rawurlencode($this->sAllowedValuesSearch ?? "") - ); - } -} - -class CellStatus_NullIssue extends CellStatus_Issue -{ - public function __construct() - { - parent::__construct(null, null, null); - } - - public function GetDescription() - { - return Dict::S('UI:CSVReport-Value-Missing'); - } -} - -/** - * Class to differ formatting depending on the caller - */ -class ReportValue -{ - /** - * @param DBObject $oObject - * @param string $sAttCode - * @param bool $bOriginal - */ - public function __construct(protected DBObject $oObject, protected string $sAttCode, protected bool $bOriginal){} - - public function GetAsHTML(bool $bLocalizedValues) - { - if ($this->bOriginal) { - return $this->oObject->GetOriginalAsHTML($this->sAttCode, $bLocalizedValues); - } - return $this->oObject->GetAsHTML($this->sAttCode, $bLocalizedValues); - } - public function GetAsCSV (bool $bLocalizedValues, string $sCsvSep, string $sCsvDelimiter) { - if ($this->bOriginal) { - return $this->oObject->GetOriginalAsCSV($this->sAttCode, $sCsvSep, $sCsvDelimiter, $bLocalizedValues); - } - return $this->oObject->GetAsCSV($this->sAttCode, $sCsvSep, $sCsvDelimiter, $bLocalizedValues); - } -} - class CellStatus_Ambiguous extends CellStatus_Issue { - protected $m_iCount; - /** - * @since 3.1.0 N°5305 - * @var string - */ - protected $sSerializedSearch; - - /** - * @since 3.1.0 N°5305 - * - * @param $previousValue - * @param int $iCount - * @param string $sSerializedSearch - * - */ - public function __construct($previousValue, $iCount, $sSerializedSearch) - { - $this->m_iCount = $iCount; - $this->sSerializedSearch = $sSerializedSearch; - parent::__construct(null, $previousValue, ''); - } - - public function GetDescription() - { - $sCount = $this->m_iCount; - return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount); - } - - /** - * @since 3.1.0 N°5305 - * @return string - */ - public function GetSearchLinkUrl() - { - return sprintf("UI.php?operation=search&filter=%s", - rawurlencode($this->sSerializedSearch ?? "") - ); - } -} - - -/** - * RowStatus - * A series of classes, keeping the information about a given row: could it be changed or not (and why)? - * - * @package iTopORM - */ -abstract class RowStatus -{ - public function __construct() - { - } - - abstract public function GetDescription(); -} - -class RowStatus_NoChange extends RowStatus -{ - public function GetDescription() - { - return Dict::S('UI:CSVReport-Row-Unchanged'); - } -} - -class RowStatus_NewObj extends RowStatus -{ - public function GetDescription() - { - return Dict::S('UI:CSVReport-Row-Created'); - } -} - -class RowStatus_Modify extends RowStatus -{ - protected $m_iChanged; - - public function __construct($iChanged) - { - $this->m_iChanged = $iChanged; - } - - public function GetDescription() - { - return Dict::Format('UI:CSVReport-Row-Updated', $this->m_iChanged); - } -} - -class RowStatus_Disappeared extends RowStatus_Modify -{ - public function GetDescription() - { - return Dict::Format('UI:CSVReport-Row-Disappeared', $this->m_iChanged); - } -} - -class RowStatus_Issue extends RowStatus -{ - protected $m_sReason; - - public function __construct($sReason) - { - $this->m_sReason = $sReason; - } - - public function GetDescription() - { - return Dict::Format('UI:CSVReport-Row-Issue', $this->m_sReason); - } -} - -/** - * class dedicated to testability - * not used/ignored in csv imports UI/CLI - * @since 3.1.0 N°5305 - */ -class RowStatus_Error extends RowStatus -{ - /** @var string */ - protected $m_sError; - - public function __construct($sError) - { - $this->m_sError = $sError; - } - - public function GetDescription() - { - return $this->m_sError; - } -} - -/** - * BulkChange - * Interpret a given data set and update the DB accordingly (fake mode avail.) - * - * @package iTopORM - */ -class BulkChange -{ - /** @var string */ - protected $m_sClass; - protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string) - // #@# todo: rename the variables to sColIndex - /** @var array attcode as key, iCol as value */ - protected $m_aAttList; - /** @var array> sExtKeyAttCode as key, array of sExtReconcKeyAttCode/iCol as value */ - protected $m_aExtKeys; - /** @var string[] list of attcode (attcode = 'id' for the pkey) */ - protected $m_aReconcilKeys; - /** @var string OQL - if specified, then the missing items will be reported */ - protected $m_sSynchroScope; - /** - * @var array attcode as key, attvalue as value. Values to be set when an object gets out of scope - * (ignored if no scope has been defined) - */ - protected $m_aOnDisappear; - /** - * @see DateTime::createFromFormat - * @var string Date format specification - */ - protected $m_sDateFormat; - /** - * @see AttributeEnum - * @var boolean true if Values in the data set are localized - */ - protected $m_bLocalizedValues; - /** @var array Cache for resolving external keys based on the given search criterias */ - protected $m_aExtKeysMappingCache; - /** @var int number of columns */ - protected $m_iNbCol; - - public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false, $iNbCol = 0) - { - $this->m_sClass = $sClass; - $this->m_aData = $aData; - $this->m_aAttList = $aAttList; - $this->m_aReconcilKeys = $aReconcilKeys; - $this->m_aExtKeys = $aExtKeys; - $this->m_sSynchroScope = $sSynchroScope; - $this->m_aOnDisappear = $aOnDisappear; - $this->m_sDateFormat = $sDateFormat; - $this->m_bLocalizedValues = $bLocalize; - $this->m_aExtKeysMappingCache = array(); - $this->m_iNbCol =$iNbCol; - } - - protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults) - { - $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass()); - foreach ($this->m_aExtKeys[$sAttCode] as $sReconKeyAttCode => $iCol) - { - if ($sReconKeyAttCode == 'id') - { - $value = (int) $aRowData[$iCol]; - } - else - { - // The foreign attribute is one of our reconciliation key - $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode); - $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - } - $oReconFilter->AddCondition($sReconKeyAttCode, $value, '='); - $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); - } - - $oExtObjects = new CMDBObjectSet($oReconFilter); - $aKeys = $oExtObjects->ToArray(); - return array($oReconFilter, $aKeys); - } - - // Returns true if the CSV data specifies that the external key must be left undefined - protected function IsNullExternalKeySpec($aRowData, $sAttCode) - { - //$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) - { - // The foreign attribute is one of our reconciliation key - if (isset($aRowData[$iCol]) && strlen($aRowData[$iCol]) > 0) - { - return false; - } - } - return true; - } - - /** - * @param DBObject $oTargetObj - * @param array $aRowData - * @param array $aErrors - * - * @return array - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - protected function PrepareObject(&$oTargetObj, $aRowData, &$aErrors) - { - $aResults = array(); - $aErrors = array(); - - // External keys reconciliation - // - foreach($this->m_aExtKeys as $sAttCode => $aReconKeys) - { - // Skip external keys used for the reconciliation process - // if (!array_key_exists($sAttCode, $this->m_aAttList)) continue; - - $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); - - if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) - { - foreach ($aReconKeys as $sReconKeyAttCode => $iCol) - { - // Default reporting - // $aRowData[$iCol] is always null - $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); - } - if ($oExtKey->IsNullAllowed()) - { - $oTargetObj->Set($sAttCode, $oExtKey->GetNullValue()); - $aResults[$sAttCode]= new CellStatus_Void($oExtKey->GetNullValue()); - } - else - { - $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-Null'); - $aResults[$sAttCode]= new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), Dict::S('UI:CSVReport-Value-Issue-Null')); - } - } - else - { - $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass()); - - $aCacheKeys = array(); - foreach ($aReconKeys as $sReconKeyAttCode => $iCol) - { - // The foreign attribute is one of our reconciliation key - if ($sReconKeyAttCode == 'id') - { - $value = $aRowData[$iCol]; - } - else - { - $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode); - $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - } - $aCacheKeys[] = $value; - $oReconFilter->AddCondition($sReconKeyAttCode, $value, '='); - $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); - } - $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query... - $iForeignKey = null; - // TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case... - if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache)) - { - $this->m_aExtKeysMappingCache[$sAttCode] = array(); - } - if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode])) - { - // Cache hit - $iObjectFoundCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c']; - $iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k']; - // Record the hit - $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++; - } - else - { - // Cache miss, let's initialize it - $oExtObjects = new CMDBObjectSet($oReconFilter); - $iObjectFoundCount = $oExtObjects->Count(); - if ($iObjectFoundCount == 1) - { - $oForeignObj = $oExtObjects->Fetch(); - $iForeignKey = $oForeignObj->GetKey(); - } - $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array( - 'c' => $iObjectFoundCount, - 'k' => $iForeignKey, - 'oql' => $oReconFilter->ToOql(), - 'h' => 0, // number of hits on this cache entry - ); - } - switch($iObjectFoundCount) - { - case 0: - $oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter); - $aResults[$sAttCode] = $oCellStatus_SearchIssue; - $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound'); - break; - - case 1: - // Do change the external key attribute - $oTargetObj->Set($sAttCode, $iForeignKey); - break; - - default: - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iObjectFoundCount); - $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iObjectFoundCount, $oReconFilter->serialize()); - } - } - - // Report - if (!array_key_exists($sAttCode, $aResults)) - { - $iForeignObj = $oTargetObj->Get($sAttCode); - if (array_key_exists($sAttCode, $oTargetObj->ListChanges())) - { - if ($oTargetObj->IsNew()) - { - $aResults[$sAttCode]= new CellStatus_Void($iForeignObj); - } - else - { - $aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode)); - foreach ($aReconKeys as $sReconKeyAttCode => $iCol) - { - // Report the change on reconciliation values as well - $aResults[$iCol] = new CellStatus_Modify($aRowData[$iCol]); - } - } - } - else - { - $aResults[$sAttCode]= new CellStatus_Void($iForeignObj); - } - } - } - - // Set the object attributes - // - foreach ($this->m_aAttList as $sAttCode => $iCol) - { - // skip the private key, if any - if (($sAttCode == 'id') || ($sAttCode == 'friendlyname')) { - continue; - } - - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - - // skip reconciliation keys - if (!$oAttDef->IsWritable() && in_array($sAttCode, $this->m_aReconcilKeys)) { - continue; - } - - $aReasons = array(); - $iFlags = ($oTargetObj->IsNew()) - ? $oTargetObj->GetInitialStateAttributeFlags($sAttCode, $aReasons) - : $oTargetObj->GetAttributeFlags($sAttCode, $aReasons); - if ((($iFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY) && ($oTargetObj->Get($sAttCode) != $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues))) { - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Readonly', $sAttCode, $oTargetObj->Get($sAttCode), $aRowData[$iCol]); - } - else if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) - { - try - { - $oSet = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - $oTargetObj->Set($sAttCode, $oSet); - } - catch(CoreException $e) - { - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Format', $e->getMessage()); - } - } - else - { - $value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) - { - if ($oAttDef instanceof AttributeEnum || $oAttDef instanceof AttributeTagSet){ - /** @var AttributeDefinition $oAttributeDefinition */ - $oAttributeDefinition = $oAttDef; - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-AllowedValues', $sAttCode, implode(',', $oAttributeDefinition->GetAllowedValues())); - } else { - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode); - } - } - else - { - $res = $oTargetObj->CheckValue($sAttCode, $value); - if ($res === true) - { - $oTargetObj->Set($sAttCode, $value); - } - else - { - // $res is a string with the error description - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Unknown', $sAttCode, $res); - } - } - } - } - - // Reporting on fields - // - $aChangedFields = $oTargetObj->ListChanges(); - foreach ($this->m_aAttList as $sAttCode => $iCol) { - if ($sAttCode == 'id') { - $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); - } - else { - $sCurValue = new ReportValue($oTargetObj, $sAttCode, false); - $sOrigValue = new ReportValue($oTargetObj, $sAttCode, true); - if (isset($aErrors[$sAttCode])) { - $aResults[$iCol]= new CellStatus_Issue($aRowData[$iCol], $sOrigValue, $aErrors[$sAttCode]); - } - elseif (array_key_exists($sAttCode, $aChangedFields)){ - if ($oTargetObj->IsNew()) { - $aResults[$iCol]= new CellStatus_Void($sCurValue); - } - else { - $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue); - } - } - else { - // By default... nothing happens - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oAttDef instanceof AttributeDateTime) { - $aResults[$iCol]= new CellStatus_Void($oAttDef->GetFormat()->Format($aRowData[$iCol])); - } - else { - $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); - } - } - } - } - - // Checks - // - $res = $oTargetObj->CheckConsistency(); - if ($res !== true) - { - // $res contains the error description - $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res); - } - return $aResults; - } - - /** - * search with current permissions did not match - * let's search why and give some more feedbacks to the user through proper labels - * - * @param DBObjectSearch $oDbSearchWithConditions search used to find external key - * - * @return \CellStatus_SearchIssue - * @throws \CoreException - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - * - * @since 3.1.0 N°5305 - */ - protected function GetCellSearchIssue($oDbSearchWithConditions) : CellStatus_SearchIssue { - //current search with current permissions did not match - //let's search why and give some more feedback to the user - - $sSerializedSearch = $oDbSearchWithConditions->serialize(); - - // Count all objects with all permissions without any condition - $oDbSearchWithoutAnyCondition = new DBObjectSearch($oDbSearchWithConditions->GetClass()); - $oDbSearchWithoutAnyCondition->AllowAllData(true); - $oExtObjectSet = new CMDBObjectSet($oDbSearchWithoutAnyCondition); - $iAllowAllDataObjectCount = $oExtObjectSet->Count(); - - if ($iAllowAllDataObjectCount === 0) { - $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject', $oDbSearchWithConditions->GetClass()); - return new CellStatus_SearchIssue($sSerializedSearch, $sReason); - } - - // Count all objects with current user permissions - $oDbSearchWithoutAnyCondition->AllowAllData(false); - $oExtObjectSetWithCurrentUserPermissions = new CMDBObjectSet($oDbSearchWithoutAnyCondition); - $iCurrentUserRightsObjectCount = $oExtObjectSetWithCurrentUserPermissions->Count(); - $sAllowedValuesOql = $oDbSearchWithoutAnyCondition->serialize(); - - if ($iCurrentUserRightsObjectCount === 0){ - // No objects visible by current user - $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass()); - return new CellStatus_SearchIssue($sSerializedSearch, $sReason); - } - - try{ - $aDisplayedAllowedValues = []; - // Possibles values are displayed to UI user. we have to limit the amount of displayed values - $oExtObjectSetWithCurrentUserPermissions->SetLimit(4); - for($i = 0; $i < 3; $i++){ - /** @var DBObject $oVisibleObject */ - $oVisibleObject = $oExtObjectSetWithCurrentUserPermissions->Fetch(); - if (is_null($oVisibleObject)){ - break; - } - - $aCurrentAllowedValueFields = []; - foreach ($oDbSearchWithConditions->GetInternalParams() as $sForeignAttCode => $sValue){ - $aCurrentAllowedValueFields[] = $oVisibleObject->Get($sForeignAttCode); - } - $aDisplayedAllowedValues[] = implode(" ", $aCurrentAllowedValueFields); - - } - $allowedValues = implode(", ", $aDisplayedAllowedValues); - if ($oExtObjectSetWithCurrentUserPermissions->Count() > 3){ - $allowedValues .= "..."; - } - } catch(Exception $e) { - IssueLog::Error("failure during CSV import when fetching few visible objects: ", null, - [ 'target_class' => $oDbSearchWithConditions->GetClass(), 'criteria' => $oDbSearchWithConditions->GetCriteria(), 'message' => $e->getMessage()] - ); - $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass()); - return new CellStatus_SearchIssue($sSerializedSearch, $sReason); - } - - if ($iAllowAllDataObjectCount != $iCurrentUserRightsObjectCount) { - // No match and some objects NOT visible by current user. including current search maybe... - $sReason = Dict::Format('UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser', $oDbSearchWithConditions->GetClass()); - return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues, $sAllowedValuesOql); - } - - // No match. This is not linked to any right issue - // Possible values: DD,DD - $aCurrentValueFields = []; - foreach ($oDbSearchWithConditions->GetInternalParams() as $sValue){ - $aCurrentValueFields[] = $sValue; - } - $value =implode(" ", $aCurrentValueFields); - $sReason = Dict::Format('UI:CSVReport-Value-NoMatch', $value); - return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues, $sAllowedValuesOql); - } - - protected function PrepareMissingObject(&$oTargetObj, &$aErrors) - { - $aResults = array(); - $aErrors = array(); - - // External keys - // - foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) - { - //$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); - $aResults[$sAttCode]= new CellStatus_Void($oTargetObj->Get($sAttCode)); - - foreach ($aKeyConfig as $sForeignAttCode => $iCol) - { - $aResults[$iCol] = new CellStatus_Void('?'); - } - } - - // Update attributes - // - foreach($this->m_aOnDisappear as $sAttCode => $value) - { - if (!MetaModel::IsValidAttCode(get_class($oTargetObj), $sAttCode)) - { - throw new BulkChangeException('Invalid attribute code', array('class' => get_class($oTargetObj), 'attcode' => $sAttCode)); - } - $oTargetObj->Set($sAttCode, $value); - } - - // Reporting on fields - // - $aChangedFields = $oTargetObj->ListChanges(); - foreach ($this->m_aAttList as $sAttCode => $iCol) - { - if ($sAttCode == 'id') - { - $aResults[$iCol]= new CellStatus_Void($oTargetObj->GetKey()); - } - if (array_key_exists($sAttCode, $aChangedFields)) - { - $aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode)); - } - else - { - // By default... nothing happens - $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode)); - } - } - - // Checks - // - $res = $oTargetObj->CheckConsistency(); - if ($res !== true) - { - // $res contains the error description - $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res); - } - return $aResults; - } - - - protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null) - { - $oTargetObj = MetaModel::NewObject($this->m_sClass); - - // Populate the cache for hierarchical keys (only if in verify mode) - if (is_null($oChange)) - { - // 1. determine if a hierarchical key exists - foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) - { - $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); - if (!$this->IsNullExternalKeySpec($aRowData, $sAttCode) && MetaModel::IsParentClass(get_class($oTargetObj), $this->m_sClass)) - { - // 2. Populate the cache for further checks - $aCacheKeys = array(); - foreach ($aKeyConfig as $sForeignAttCode => $iCol) - { - // The foreign attribute is one of our reconciliation key - if ($sForeignAttCode == 'id') - { - $value = $aRowData[$iCol]; - } - else - { - if (!isset($this->m_aAttList[$sForeignAttCode]) || !isset($aRowData[$this->m_aAttList[$sForeignAttCode]])) - { - // the key is not in the import - break 2; - } - $value = $aRowData[$this->m_aAttList[$sForeignAttCode]]; - } - $aCacheKeys[] = $value; - } - $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query... - $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array( - 'c' => 1, - 'k' => -1, - 'oql' => '', - 'h' => 0, // number of hits on this cache entry - ); - } - } - } - - $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); - - if (count($aErrors) > 0) - { - $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); - //__ERRORS__ used by tests only - $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors); - return $oTargetObj; - } - - // Check that any external key will have a value proposed - $aMissingKeys = array(); - foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey) - { - if (!$oExtKey->IsNullAllowed()) - { - if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList)) - { - $aMissingKeys[] = $oExtKey->GetLabel(); - } - } - } - if (count($aMissingKeys) > 0) - { - $sMissingKeys = implode(', ', $aMissingKeys); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-MissingExtKey', $sMissingKeys)); - return $oTargetObj; - } - - // Optionally record the results - // - if ($oChange) - { - $newID = $oTargetObj->DBInsert(); - } - else - { - $newID = 0; - } - - $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj(); - $aResult[$iRow]["finalclass"] = get_class($oTargetObj); - $aResult[$iRow]["id"] = new CellStatus_Void($newID); - - return $oTargetObj; - } - - /** - * @param array $aResult - * @param int $iRow - * @param \CMDBObject $oTargetObj - * @param array $aRowData - * @param \CMDBChange $oChange - * - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null) - { - $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); - - // Reporting - // - $aResult[$iRow]["finalclass"] = get_class($oTargetObj); - $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey()); - - if (count($aErrors) > 0) - { - $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); - //__ERRORS__ used by tests only - $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors); - return; - } - - $aChangedFields = $oTargetObj->ListChanges(); - if (count($aChangedFields) > 0) - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields)); - - // Optionaly record the results - // - if ($oChange) - { - try - { - $oTargetObj->DBUpdate(); - } - catch(CoreException $e) - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage()); - } - } - } - else - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange(); - } - } - - /** - * @param array $aResult - * @param int $iRow - * @param \CMDBObject $oTargetObj - * @param \CMDBChange $oChange - * - * @throws \BulkChangeException - */ - protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null) - { - $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors); - - // Reporting - // - $aResult[$iRow]["finalclass"] = get_class($oTargetObj); - $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey()); - - if (count($aErrors) > 0) - { - $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); - //__ERRORS__ used by tests only - $aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors); - return; - } - - $aChangedFields = $oTargetObj->ListChanges(); - if (count($aChangedFields) > 0) - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields)); - - // Optionaly record the results - // - if ($oChange) - { - try - { - $oTargetObj->DBUpdate(); - } - catch(CoreException $e) - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage()); - } - } - } - else - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0); - } - } - - public function Process(CMDBChange $oChange = null) - { - if ($oChange) - { - CMDBObject::SetCurrentChange($oChange); - } - - // Note: $oChange can be null, in which case the aim is to check what would be done - - // Debug... - // - if (false) - { - echo "
\n";
-			echo "Attributes:\n";
-			print_r($this->m_aAttList);
-			echo "ExtKeys:\n";
-			print_r($this->m_aExtKeys);
-			echo "Reconciliation:\n";
-			print_r($this->m_aReconcilKeys);
-			echo "Synchro scope:\n";
-			print_r($this->m_sSynchroScope);
-			echo "Synchro changes:\n";
-			print_r($this->m_aOnDisappear);
-			//echo "Data:\n";
-			//print_r($this->m_aData);
-			echo "
\n"; - exit; - } - - $aResult = array(); - - if (!is_null($this->m_sDateFormat) && (strlen($this->m_sDateFormat) > 0)) - { - $sDateTimeFormat = $this->m_sDateFormat; // the specified format is actually the date AND time format - $oDateTimeFormat = new DateTimeFormat($sDateTimeFormat); - $sDateFormat = $oDateTimeFormat->ToDateFormat(); - AttributeDateTime::SetFormat($oDateTimeFormat); - AttributeDate::SetFormat(new DateTimeFormat($sDateFormat)); - // Translate dates from the source data - // - foreach ($this->m_aAttList as $sAttCode => $iCol) - { - if ($sAttCode == 'id') continue; - - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime - { - foreach($this->m_aData as $iRow => $aRowData) - { - $sFormat = $sDateTimeFormat; - if(!isset($this->m_aData[$iRow][$iCol])){ - continue; - } - $sValue = $this->m_aData[$iRow][$iCol]; - if (!empty($sValue)) - { - if ($oAttDef instanceof AttributeDate) - { - $sFormat = $sDateFormat; - } - $oFormat = new DateTimeFormat($sFormat); - $sDateExample = $oFormat->Format(new DateTime('2022-10-23 16:25:33')); - $sRegExp = $oFormat->ToRegExpr('/'); - $sErrorMsg = Dict::Format('UI:CSVReport-Row-Issue-ExpectedDateFormat', $sDateExample); - if (!preg_match($sRegExp, $sValue)) - { - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); - $aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg); - - } - else - { - $oDate = DateTime::createFromFormat($sFormat, $sValue); - if ($oDate !== false) - { - $sNewDate = $oDate->format($oAttDef->GetInternalFormat()); - $this->m_aData[$iRow][$iCol] = $sNewDate; - } - else - { - // almost impossible ti reproduce since even incorrect dates with correct formats are formated and $oDate will not be false - // Leave the cell unchanged - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); - $aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg); - } - } - } - else - { - $this->m_aData[$iRow][$iCol] = ''; - } - } - } - } - } - - // Compute the results - // - if (!is_null($this->m_sSynchroScope)) - { - $aVisited = array(); - } - $iPreviousTimeLimit = ini_get('max_execution_time'); - $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); - - // Avoid too many events - cmdbAbstractObject::SetEventDBLinksChangedBlocked(true); - try { - foreach ($this->m_aData as $iRow => $aRowData) { - set_time_limit(intval($iLoopTimeLimit)); - // Stop if not the good number of cols in $aRowData - if($this->m_iNbCol>0 && count($aRowData) != $this->m_iNbCol){ - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-NbField',count($aRowData),$this->m_iNbCol) ); - continue; - } - - if (isset($aResult[$iRow]["__STATUS__"])) { - // An issue at the earlier steps - skip the rest - continue; - } - try { - $oReconciliationFilter = new DBObjectSearch($this->m_sClass); - $bSkipQuery = false; - foreach ($this->m_aReconcilKeys as $sAttCode) { - $valuecondition = null; - if (array_key_exists($sAttCode, $this->m_aExtKeys)) { - if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) { - $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oExtKey->IsNullAllowed()) { - $valuecondition = $oExtKey->GetNullValue(); - $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue()); - } else { - $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue(); - } - } else { - // The value has to be found or verified - - /** var DBObjectSearch $oReconFilter */ - list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]); - - if (count($aMatches) == 1) { - $oRemoteObj = reset($aMatches); // first item - $valuecondition = $oRemoteObj->GetKey(); - $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey()); - } elseif (count($aMatches) == 0) { - $oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter); - $aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue; - } else { - $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize()); - } - } - } else { - // The value is given in the data row - $iCol = $this->m_aAttList[$sAttCode]; - if ($sAttCode == 'id') { - $valuecondition = $aRowData[$iCol]; - } else { - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - $valuecondition = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - } - } - if (is_null($valuecondition)) { - $bSkipQuery = true; - } else { - $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=', true); - } - } - if ($bSkipQuery) { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Reconciliation')); - } else { - $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter); - switch ($oReconciliationSet->Count()) { - case 0: - $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange); - // $aResult[$iRow]["__STATUS__"]=> set in CreateObject - $aVisited[] = $oTargetObj->GetKey(); - break; - case 1: - $oTargetObj = $oReconciliationSet->Fetch(); - $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange); - // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject - if (!is_null($this->m_sSynchroScope)) { - $aVisited[] = $oTargetObj->GetKey(); - } - break; - default: - // Found several matches, ambiguous - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous')); - $aResult[$iRow]["id"] = new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize()); - $aResult[$iRow]["finalclass"] = 'n/a'; - } - } - } catch (Exception $e) { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-Internal', get_class($e), $e->getMessage())); - } - } - - if (!is_null($this->m_sSynchroScope)) { - // Compute the delta between the scope and visited objects - $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope); - $oScopeSet = new DBObjectSet($oScopeSearch); - while ($oObj = $oScopeSet->Fetch()) { - $iObj = $oObj->GetKey(); - if (!in_array($iObj, $aVisited)) { - set_time_limit(intval($iLoopTimeLimit)); - $iRow++; - $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange); - } - } - } - } finally { - // Send all the retained events for further computations - cmdbAbstractObject::SetEventDBLinksChangedBlocked(false); - cmdbAbstractObject::FireEventDbLinksChangedForAllObjects(); - } - - set_time_limit(intval($iPreviousTimeLimit)); - - // Fill in the blanks - the result matrix is expected to be 100% complete - // - foreach($this->m_aData as $iRow => $aRowData) - { - foreach($this->m_aAttList as $iCol) - { - if (!array_key_exists($iCol, $aResult[$iRow])) - { - if(isset($aRowData[$iCol])) { - $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); - } else { - $aResult[$iRow][$iCol] = new CellStatus_Issue('', null, Dict::S('UI:CSVReport-Value-Issue-NoValue')); - } - } - } - foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts) - { - if (!array_key_exists($sAttCode, $aResult[$iRow])) - { - $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a'); - } - foreach ($aForeignAtts as $sForeignAttCode => $iCol) - { - if (!array_key_exists($iCol, $aResult[$iRow])) - { - // The foreign attribute is one of our reconciliation key - if(isset($aRowData[$iCol])) { - $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); - } else { - $aResult[$iRow][$iCol] = new CellStatus_Issue('', null, 'UI:CSVReport-Value-Issue-NoValue'); - } - } - } - } - } - - return $aResult; - } - - /** - * Display the history of bulk imports - */ - static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false) - { - $sAjaxDivId = "CSVImportHistory"; - if (!$bFromAjax) - { - $oPage->add('
'); - } - - $oPage->p(Dict::S('UI:History:BulkImports+').' '); - - $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE origin IN ('csv-interactive', 'csv-import.php')"); - - $iQueryLimit = $bShowAll ? 0 : appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); - $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit); - - $oAppContext = new ApplicationContext(); - - $bLimitExceeded = false; - if ($oBulkChanges->Count() > (appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()))) - { - $bLimitExceeded = true; - if (!$bShowAll) - { - $iMaxObjects = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); - $oBulkChanges->SetLimit($iMaxObjects); - } - } - $oBulkChanges->Seek(0); - - $aDetails = array(); - while ($oChange = $oBulkChanges->Fetch()) - { - $sDate = ''.$oChange->Get('date').''; - $sUser = $oChange->GetUserName(); - if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches)) - { - $sUser = $aMatches[1]; - } - else - { - $sUser = $oChange->Get('userinfo'); - } - - $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id"); - $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey())); - $iCreated = $oOpSet->Count(); - - // Get the class from the first item found (assumption: a CSV load is done for a single class) - if ($oCreateOp = $oOpSet->Fetch()) - { - $sClass = $oCreateOp->Get('objclass'); - } - - $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id"); - $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey())); - - $aModified = array(); - $aAttList = array(); - while ($oModified = $oOpSet->Fetch()) - { - // Get the class (if not done earlier on object creation) - $sClass = $oModified->Get('objclass'); - $iKey = $oModified->Get('objkey'); - $sAttCode = $oModified->Get('attcode'); - - $aAttList[$sClass][$sAttCode] = true; - $aModified["$sClass::$iKey"] = true; - } - $iModified = count($aModified); - - // Assumption: there is only one class of objects being loaded - // Then the last class found gives us the class for every object - if ( ($iModified > 0) || ($iCreated > 0)) - { - $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified); - } - } - - $aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')), - 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')), - 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')), - 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')), - 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')), - ); - - if ($bLimitExceeded) - { - if ($bShowAll) - { - // Collapsible list - $oPage->add('

'.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).'  '.Dict::S('UI:CollapseList').'

'); - } - else - { - // Truncated list - $iMinDisplayLimit = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); - $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count()); - $sLinkLabel = Dict::S('UI:DisplayAll'); - $oPage->add('

'.$sCollapsedLabel.'  '.$sLinkLabel.'

'); - - $oPage->add_ready_script( - <<GetForLink(); - $oPage->add_script( - <<'); - $.get(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?$sAppContext', {operation: 'displayCSVHistory', showall: bShowAll}, function(data) - { - $('#$sAjaxDivId').html(data); - } - ); - } -EOF - ); - } - } - else - { - // Normal display - full list without any decoration - } - - $oPage->table($aConfig, $aDetails); - - if (!$bFromAjax) - { - $oPage->add('
'); - } - } - - /** - * Display the details of an import - * @param iTopWebPage $oPage - * @param $iChange - * @throws Exception - */ - static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange) - { - if ($iChange == 0) - { - throw new Exception("Missing parameter changeid"); - } - $oChange = MetaModel::GetObject('CMDBChange', $iChange, false); - if (is_null($oChange)) - { - throw new Exception("Unknown change: $iChange"); - } - $oPage->add("

".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."

\n"); - - // Assumption : change made one single class of objects - $aObjects = array(); - $aAttributes = array(); // array of attcode => occurences - - $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id"); - $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange)); - while ($oOperation = $oOpSet->Fetch()) - { - $sClass = $oOperation->Get('objclass'); - $iKey = $oOperation->Get('objkey'); - $iObjId = "$sClass::$iKey"; - if (!isset($aObjects[$iObjId])) - { - $aObjects[$iObjId] = array(); - $aObjects[$iObjId]['__class__'] = $sClass; - $aObjects[$iObjId]['__id__'] = $iKey; - } - if (get_class($oOperation) == 'CMDBChangeOpCreate') - { - $aObjects[$iObjId]['__created__'] = true; - } - elseif ($oOperation instanceof CMDBChangeOpSetAttribute) - { - $sAttCode = $oOperation->Get('attcode'); - - if ((get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar') || (get_class($oOperation) == 'CMDBChangeOpSetAttributeURL')) - { - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef->IsExternalKey()) - { - $sOldValue = Dict::S('UI:UndefinedObject'); - if ($oOperation->Get('oldvalue') != 0) - { - $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue')); - $sOldValue = $oOldTarget->GetHyperlink(); - } - - $sNewValue = Dict::S('UI:UndefinedObject'); - if ($oOperation->Get('newvalue') != 0) - { - $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue')); - $sNewValue = $oNewTarget->GetHyperlink(); - } - } - else - { - $sOldValue = $oOperation->GetAsHTML('oldvalue'); - $sNewValue = $oOperation->GetAsHTML('newvalue'); - } - $aObjects[$iObjId][$sAttCode] = $sOldValue.' -> '.$sNewValue; - } - else - { - $aObjects[$iObjId][$sAttCode] = 'n/a'; - } - - if (isset($aAttributes[$sAttCode])) - { - $aAttributes[$sAttCode]++; - } - else - { - $aAttributes[$sAttCode] = 1; - } - } - } - - $aDetails = array(); - foreach($aObjects as $iUId => $aObjData) - { - $aRow = array(); - $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false); - if (is_null($oObject)) - { - $aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)'; - } - else - { - $aRow['object'] = $oObject->GetHyperlink(); - } - if (isset($aObjData['__created__'])) - { - $aRow['operation'] = Dict::S('Change:ObjectCreated'); - } - else - { - $aRow['operation'] = Dict::S('Change:ObjectModified'); - } - foreach ($aAttributes as $sAttCode => $iOccurences) - { - if (isset($aObjData[$sAttCode])) - { - $aRow[$sAttCode] = $aObjData[$sAttCode]; - } - elseif (!is_null($oObject)) - { - // This is the current vaslue: $oObject->GetAsHtml($sAttCode) - // whereas we are displaying the value that was set at the time - // the object was created - // This requires addtional coding...let's do that later - $aRow[$sAttCode] = ''; - } - else - { - $aRow[$sAttCode] = ''; - } - } - $aDetails[] = $aRow; - } - - $aConfig = array(); - $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass)); - $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+')); - foreach ($aAttributes as $sAttCode => $iOccurences) - { - $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode)); - } - $oPage->table($aConfig, $aDetails); - } -} - + protected $m_iCount; + /** + * @since 3.1.0 N°5305 + * @var string + */ + protected $sSerializedSearch; + + /** + * @param $previousValue + * @param int $iCount + * @param string $sSerializedSearch + * + * @since 3.1.0 N°5305 + * + */ + public function __construct($previousValue, $iCount, $sSerializedSearch) + { + $this->m_iCount = $iCount; + $this->sSerializedSearch = $sSerializedSearch; + parent::__construct(null, $previousValue, ''); + } + + public function GetDescription() + { + $sCount = $this->m_iCount; + return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount); + } + + /** + * @return string + * @since 3.1.0 N°5305 + */ + public function GetSearchLinkUrl() + { + return sprintf("UI.php?operation=search&filter=%s", + rawurlencode($this->sSerializedSearch ?? "") + ); + } +} \ No newline at end of file