diff --git a/core/apc-emulation.php b/core/apc-emulation.php index 7ccde34b2..6690ad0be 100644 --- a/core/apc-emulation.php +++ b/core/apc-emulation.php @@ -60,14 +60,15 @@ function apc_store($key, $var = NULL, $ttl = 0) */ function apc_fetch($key) { - if (is_array($key)) - { - $aResult = array(); - foreach($key as $sKey) - { + if (is_array($key)) { + $aResult = []; + foreach ($key as $sKey) { $aResult[$sKey] = apcFile::FetchOneFile($sKey); } + return $aResult; + } elseif (is_null($key)) { + return false; } return apcFile::FetchOneFile($key); } @@ -246,21 +247,16 @@ class apcFile */ static public function StoreOneFile($sKey, $value, $iTTL) { - if (empty($sKey)) - { + if (empty($sKey)) { return false; } - - if (is_file(self::GetCacheFileName($sKey))) - { + if (is_file(self::GetCacheFileName($sKey))) { @unlink(self::GetCacheFileName($sKey)); } - if (is_file(self::GetCacheFileName('-'.$sKey))) - { + if (is_file(self::GetCacheFileName('-'.$sKey))) { @unlink(self::GetCacheFileName('-'.$sKey)); } - if ($iTTL > 0) - { + if ($iTTL > 0) { // hint for ttl management $sKey = '-'.$sKey; } @@ -268,15 +264,14 @@ class apcFile $sFilename = self::GetCacheFileName($sKey); // try to create the folder $sDirname = dirname($sFilename); - if (!file_exists($sDirname)) - { - if (!@mkdir($sDirname, 0755, true)) - { + if (!is_dir($sDirname)) { + if (!@mkdir($sDirname, 0755, true)) { return false; } } $bRes = !(@file_put_contents($sFilename, serialize($value), LOCK_EX) === false); self::AddFile($sFilename); + return $bRes; } @@ -360,19 +355,15 @@ class apcFile */ static protected function ReadCacheLocked($sFilename) { - if (!is_file($sFilename)) - { - return false; - } + $sContent = false; $file = @fopen($sFilename, 'r'); - if ($file === false) - { - return false; + if ($file !== false) { + if (flock($file, LOCK_SH)) { + $sContent = file_get_contents($sFilename); + flock($file, LOCK_UN); + } + fclose($file); } - flock($file, LOCK_SH); - $sContent = @fread($file, @filesize($sFilename)); - flock($file, LOCK_UN); - fclose($file); return $sContent; } diff --git a/core/bulkchange.class.inc.php b/core/bulkchange.class.inc.php index ada67eb26..2e4e60945 100644 --- a/core/bulkchange.class.inc.php +++ b/core/bulkchange.class.inc.php @@ -35,11 +35,37 @@ abstract class CellChangeSpec return $this->m_proposedValue; } - public function GetDisplayableValue() + /** + * @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 */ @@ -54,12 +80,12 @@ abstract class CellChangeSpec } /** - * @since 3.1.0 N°5305 + * @since 3.2.0 */ - public function GetDisplayableValueAndDescription(): string + public function GetCLIValueAndDescription(): string { return sprintf("%s%s", - $this->GetDisplayableValue(), + $this->GetCLIValue(), $this->GetDescription() ); } @@ -108,13 +134,25 @@ class CellStatus_Issue extends CellStatus_Modify parent::__construct($proposedValue, $previousValue); } - public function GetDisplayableValue() + 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'); } - return Dict::Format('UI:CSVReport-Value-ChangeIssue', \utils::EscapeHtml($this->m_proposedValue)); + 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() @@ -122,12 +160,12 @@ class CellStatus_Issue extends CellStatus_Modify return $this->m_sReason; } /* - * @since 3.1.0 N°5305 + * @since 3.2.0 */ - public function GetDisplayableValueAndDescription(): string + public function GetCLIValueAndDescription(): string { return sprintf("%s. %s", - $this->GetDisplayableValue(), + $this->GetCLIValue(), $this->GetDescription() ); } @@ -172,7 +210,7 @@ class CellStatus_SearchIssue extends CellStatus_Issue $this->sAllowedValuesSearch = $sAllowedValuesSearch; } - public function GetDisplayableValue() + public function GetCLIValue(bool $bLocalizedValues = false): string { if (null === $this->m_sReason) { return Dict::Format('UI:CSVReport-Value-NoMatch', ''); @@ -181,6 +219,15 @@ class CellStatus_SearchIssue extends CellStatus_Issue 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) || @@ -227,6 +274,33 @@ class CellStatus_NullIssue extends CellStatus_Issue } } +/** + * 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 { @@ -413,22 +487,6 @@ class BulkChange $this->m_aExtKeysMappingCache = array(); } - protected $m_bReportHtml = false; - protected $m_sReportCsvSep = ','; - protected $m_sReportCsvDelimiter = '"'; - - public function SetReportHtml() - { - $this->m_bReportHtml = true; - } - - public function SetReportCsv($sSeparator = ',', $sDelimiter = '"') - { - $this->m_bReportHtml = false; - $this->m_sReportCsvSep = $sSeparator; - $this->m_sReportCsvDelimiter = $sDelimiter; - } - protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults) { $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); @@ -446,7 +504,7 @@ class BulkChange $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); } $oReconFilter->AddCondition($sReconKeyAttCode, $value, '='); - $aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol])); + $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); } $oExtObjects = new CMDBObjectSet($oReconFilter); @@ -470,7 +528,7 @@ class BulkChange } /** - * @param \DBObject $oTargetObj + * @param DBObject $oTargetObj * @param array $aRowData * @param array $aErrors * @@ -533,7 +591,7 @@ class BulkChange } $aCacheKeys[] = $value; $oReconFilter->AddCondition($sReconKeyAttCode, $value, '='); - $aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol])); + $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); } $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query... $iForeignKey = null; @@ -602,7 +660,7 @@ class BulkChange foreach ($aReconKeys as $sReconKeyAttCode => $iCol) { // Report the change on reconciliation values as well - $aResults[$iCol] = new CellStatus_Modify(utils::HtmlEntities($aRowData[$iCol])); + $aResults[$iCol] = new CellStatus_Modify($aRowData[$iCol]); } } } @@ -680,50 +738,32 @@ class BulkChange // Reporting on fields // $aChangedFields = $oTargetObj->ListChanges(); - foreach ($this->m_aAttList as $sAttCode => $iCol) - { - if ($sAttCode == 'id') - { - $aResults[$iCol]= new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol])); + foreach ($this->m_aAttList as $sAttCode => $iCol) { + if ($sAttCode == 'id') { + $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); } - else - { - if ($this->m_bReportHtml) - { - $sCurValue = $oTargetObj->GetAsHTML($sAttCode, $this->m_bLocalizedValues); - $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode, $this->m_bLocalizedValues); + 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]); } - else - { - $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter, $this->m_bLocalizedValues); - $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter, $this->m_bLocalizedValues); - } - if (isset($aErrors[$sAttCode])) - { - $aResults[$iCol]= new CellStatus_Issue(utils::HtmlEntities($aRowData[$iCol]), $sOrigValue, $aErrors[$sAttCode]); - } - elseif (array_key_exists($sAttCode, $aChangedFields)) - { - if ($oTargetObj->IsNew()) - { + elseif (array_key_exists($sAttCode, $aChangedFields)){ + if ($oTargetObj->IsNew()) { $aResults[$iCol]= new CellStatus_Void($sCurValue); } - else - { + else { $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue); } } - else - { + else { // By default... nothing happens $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oAttDef instanceof AttributeDateTime) - { + if ($oAttDef instanceof AttributeDateTime) { $aResults[$iCol]= new CellStatus_Void($oAttDef->GetFormat()->Format($aRowData[$iCol])); } - else - { - $aResults[$iCol]= new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol])); + else { + $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); } } } @@ -788,7 +828,7 @@ class BulkChange // 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 */ + /** @var DBObject $oVisibleObject */ $oVisibleObject = $oExtObjectSetWithCurrentUserPermissions->Fetch(); if (is_null($oVisibleObject)){ break; @@ -1155,7 +1195,7 @@ class BulkChange if (!preg_match($sRegExp, $sValue)) { $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); - $aResult[$iRow][$iCol] = new CellStatus_Issue(utils::HtmlEntities($sValue), null, $sErrorMsg); + $aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg); } else @@ -1168,6 +1208,7 @@ class BulkChange } 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); @@ -1307,7 +1348,7 @@ class BulkChange { if (!array_key_exists($iCol, $aResult[$iRow])) { - $aResult[$iRow][$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol])); + $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); } } foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts) @@ -1321,7 +1362,7 @@ class BulkChange if (!array_key_exists($iCol, $aResult[$iRow])) { // The foreign attribute is one of our reconciliation key - $aResult[$iRow][$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol])); + $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); } } } diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index c5bc07a67..6e7dc505f 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -493,6 +493,7 @@ return array( 'Combodo\\iTop\\Service\\Events\\EventService' => $baseDir . '/sources/Service/Events/EventService.php', 'Combodo\\iTop\\Service\\Events\\EventServiceLog' => $baseDir . '/sources/Service/Events/EventServiceLog.php', 'Combodo\\iTop\\Service\\Events\\iEventServiceSetup' => $baseDir . '/sources/Service/Events/iEventServiceSetup.php', + 'Combodo\\iTop\\Service\\Import\\CSVImportPageProcessor' => $baseDir . '/sources/Service/Import/CSVImportPageProcessor.php', 'Combodo\\iTop\\Service\\Links\\LinkSetDataTransformer' => $baseDir . '/sources/Service/Links/LinkSetDataTransformer.php', 'Combodo\\iTop\\Service\\Links\\LinkSetModel' => $baseDir . '/sources/Service/Links/LinkSetModel.php', 'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => $baseDir . '/sources/Service/Links/LinkSetRepository.php', @@ -1489,6 +1490,7 @@ return array( 'RelationObjectNode' => $baseDir . '/core/relationgraph.class.inc.php', 'RelationRedundancyNode' => $baseDir . '/core/relationgraph.class.inc.php', 'RelationTypeIterator' => $baseDir . '/core/simplegraph.class.inc.php', + 'ReportValue' => $baseDir . '/core/bulkchange.class.inc.php', 'RestDelete' => $baseDir . '/core/restservices.class.inc.php', 'RestResult' => $baseDir . '/application/applicationextension.inc.php', 'RestResultWithObjects' => $baseDir . '/core/restservices.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index ce35cd24a..1841222f1 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -868,6 +868,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Service\\Events\\EventService' => __DIR__ . '/../..' . '/sources/Service/Events/EventService.php', 'Combodo\\iTop\\Service\\Events\\EventServiceLog' => __DIR__ . '/../..' . '/sources/Service/Events/EventServiceLog.php', 'Combodo\\iTop\\Service\\Events\\iEventServiceSetup' => __DIR__ . '/../..' . '/sources/Service/Events/iEventServiceSetup.php', + 'Combodo\\iTop\\Service\\Import\\CSVImportPageProcessor' => __DIR__ . '/../..' . '/sources/Service/Import/CSVImportPageProcessor.php', 'Combodo\\iTop\\Service\\Links\\LinkSetDataTransformer' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetDataTransformer.php', 'Combodo\\iTop\\Service\\Links\\LinkSetModel' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetModel.php', 'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetRepository.php', @@ -1864,6 +1865,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'RelationObjectNode' => __DIR__ . '/../..' . '/core/relationgraph.class.inc.php', 'RelationRedundancyNode' => __DIR__ . '/../..' . '/core/relationgraph.class.inc.php', 'RelationTypeIterator' => __DIR__ . '/../..' . '/core/simplegraph.class.inc.php', + 'ReportValue' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php', 'RestDelete' => __DIR__ . '/../..' . '/core/restservices.class.inc.php', 'RestResult' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'RestResultWithObjects' => __DIR__ . '/../..' . '/core/restservices.class.inc.php', diff --git a/pages/csvimport.php b/pages/csvimport.php index d064b7291..b131087cc 100644 --- a/pages/csvimport.php +++ b/pages/csvimport.php @@ -4,11 +4,9 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ -use Combodo\iTop\Application\Helper\WebResourcesHelper; use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\CollapsibleSection\CollapsibleSectionUIBlockFactory; -use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Field\FieldUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSetUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory; @@ -31,6 +29,7 @@ use Combodo\iTop\Application\WebPage\AjaxPage; use Combodo\iTop\Application\WebPage\ErrorPage; use Combodo\iTop\Application\WebPage\iTopWebPage; use Combodo\iTop\Application\WebPage\WebPage; +use Combodo\iTop\Service\Import\CSVImportPageProcessor; use Combodo\iTop\Core\CMDBChange\CMDBChangeOrigin; use Combodo\iTop\Renderer\BlockRenderer; @@ -216,18 +215,6 @@ try { $oP->AddSubBlock(AlertUIBlockFactory::MakeForInformation(MetaModel::GetClassIcon($sClass)." ".Dict::Format('UI:Title:BulkSynchro_nbItem_ofClass_class', $iCount, MetaModel::GetName($sClass)))); } - /** - * Add a paragraph to the body of the page - * - * @param string $s_html - * @param ?string $sLinkUrl - * - * @return string - */ - function GetDivAlert($s_html) - { - return "
$s_html
\n"; - } /** * Process the CSV data, for real or as a simulation * @param WebPage $oPage The page used to display the wizard @@ -253,544 +240,25 @@ try { $sSeparator = utils::ReadParam('separator', ',', false, 'raw_data'); $sTextQualifier = utils::ReadParam('text_qualifier', '"', false, 'raw_data'); $bHeaderLine = (utils::ReadParam('header_line', '0') == 1); - $iSkippedLines = 0; - if (utils::ReadParam('box_skiplines', '0') == 1) { - $iSkippedLines = utils::ReadParam('nb_skipped_lines', '0'); - } + $iNbSkippedLines = utils::ReadParam('nb_skipped_lines', '0'); + $iBoxSkipLines = utils::ReadParam('box_skiplines', '0'); $aFieldsMapping = utils::ReadParam('field', array(), false, 'raw_data'); $aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name'); $iCurrentStep = $bSimulate ? 4 : 5; $bAdvanced = utils::ReadParam('advanced', 0); $sEncoding = utils::ReadParam('encoding', 'UTF-8'); $sSynchroScope = utils::ReadParam('synchro_scope', '', false, 'raw_data'); + $aSynchroUpdate = utils::ReadParam('synchro_update', array()); $sDateTimeFormat = utils::ReadParam('date_time_format', 'default'); $sCustomDateTimeFormat = utils::ReadParam('custom_date_time_format', (string)AttributeDateTime::GetFormat(), false, 'raw_data'); - $sChosenDateFormat = ($sDateTimeFormat == 'default') ? (string)AttributeDateTime::GetFormat() : $sCustomDateTimeFormat; - - if (!empty($sSynchroScope)) - { - $oSearch = DBObjectSearch::FromOQL($sSynchroScope); - $sClassName = $oSearch->GetClass(); // If a synchronization scope is set, then the class is fixed ! - $oSet = new DBObjectSet($oSearch); - $iCount = $oSet->Count(); - DisplaySynchroBanner($oPage, $sClassName, $iCount); - $aSynchroUpdate = utils::ReadParam('synchro_update', array()); - } - else - { - $sSynchroScope = ''; - $aSynchroUpdate = null; - } - - // Parse the data set - $oCSVParser = new CSVParser($sCSVData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop')); - $aData = $oCSVParser->ToArray($iSkippedLines); - $iRealSkippedLines = $iSkippedLines; - if ($bHeaderLine) - { - $aResult[] = $sTextQualifier.implode($sTextQualifier.$sSeparator.$sTextQualifier, array_shift($aData)).$sTextQualifier; // Remove the first line and store it in case of error - $iRealSkippedLines++; - } - - // Format for the line numbers - $sMaxLen = (strlen(''.count($aData)) < 3) ? 3 : strlen(''.count($aData)); // Pad line numbers to the appropriate number of chars, but at least 3 - - // Compute the list of search/reconciliation criteria - $aSearchKeys = array(); - foreach($aSearchFields as $index => $sDummy) - { - $sSearchField = $aFieldsMapping[$index]; - $aMatches = array(); - if (preg_match('/(.+)->(.+)/', $sSearchField, $aMatches) > 0) - { - $sSearchField = $aMatches[1]; - $aSearchKeys[$aMatches[1]] = ''; - } - else - { - $aSearchKeys[$sSearchField] = ''; - } - if (!MetaModel::IsValidFilterCode($sClassName, $sSearchField)) - { - // Remove invalid or unmapped search fields - $aSearchFields[$index] = null; - unset($aSearchKeys[$sSearchField]); - } - } - - // Compute the list of fields and external keys to process - $aExtKeys = array(); - $aAttributes = array(); - $aExternalKeysByColumn = array(); - foreach($aFieldsMapping as $iNumber => $sAttCode) - { - $iIndex = $iNumber-1; - if (!empty($sAttCode) && ($sAttCode != ':none:') && ($sAttCode != 'finalclass')) - { - if (preg_match('/(.+)->(.+)/', $sAttCode, $aMatches) > 0) - { - $sAttribute = $aMatches[1]; - $sField = $aMatches[2]; - $aExtKeys[$sAttribute][$sField] = $iIndex; - $aExternalKeysByColumn[$iIndex] = $sAttribute; - } - else - { - if ($sAttCode == 'id') - { - $aAttributes['id'] = $iIndex; - } - else - { - $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode); - if ($oAttDef->IsExternalKey()) - { - $aExtKeys[$sAttCode]['id'] = $iIndex; - $aExternalKeysByColumn[$iIndex] = $sAttCode; - } - else - { - $aAttributes[$sAttCode] = $iIndex; - } - } - } - } - } - - $oMyChange = null; - if (!$bSimulate) - { - // We're doing it for real, let's create a change - $sUserString = CMDBChange::GetCurrentUserName().' (CSV)'; - CMDBObject::SetCurrentChangeFromParams($sUserString, CMDBChangeOrigin::CSV_INTERACTIVE); - $oMyChange = CMDBObject::GetCurrentChange(); - } - - $oBulk = new BulkChange( - $sClassName, - $aData, - $aAttributes, - $aExtKeys, - array_keys($aSearchKeys), - empty($sSynchroScope) ? null : $sSynchroScope, - $aSynchroUpdate, - $sChosenDateFormat, // date format - true // localize - ); - $oBulk->SetReportHtml(); - - $oPage->AddSubBlock(InputUIBlockFactory::MakeForHidden("csvdata_truncated", $sCSVDataTruncated, "csvdata_truncated")); - $aRes = $oBulk->Process($oMyChange); - - $aColumns = []; - $aColumns ["line"] = ["label" => "Line"]; - $aColumns ["status"] = ["label" => "Status"]; - $aColumns ["object"] = ["label" => "Object"]; - foreach ($aFieldsMapping as $iNumber => $sAttCode) { - if (!empty($sAttCode) && ($sAttCode != ':none:') && ($sAttCode != 'finalclass')) { - $aColumns[$sClassName.'/'.$sAttCode] = ["label" => MetaModel::GetLabel($sClassName, $sAttCode)]; - } - } - $aColumns["message"] = ["label" => "Message"]; - - $iErrors = 0; - $iCreated = 0; - $iModified = 0; - $iUnchanged = 0; - - $aTableData = []; - $sAppRootUrl = utils::GetAbsoluteUrlAppRoot(); - - foreach ($aRes as $iLine => $aResRow) { - $aTableRow = []; - $oStatus = $aResRow['__STATUS__']; - $sUrl = ''; - $sMessage = ''; - $sCSSRowClass = ''; - $sCSSMessageClass = 'cell_ok'; - switch (get_class($oStatus)) { - case 'RowStatus_NoChange': - $iUnchanged++; - $sFinalClass = $aResRow['finalclass']; - $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); - $sUrl = $oObj->GetHyperlink(); - $sStatus = ''; - $sCSSRowClass = 'ibo-csv-import--row-unchanged'; - break; - - case 'RowStatus_Modify': - $iModified++; - $sFinalClass = $aResRow['finalclass']; - $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); - $sUrl = $oObj->GetHyperlink(); - $sStatus = ''; - $sCSSRowClass = 'ibo-csv-import--row-modified'; - break; - - case 'RowStatus_Disappeared': - $iModified++; - $sFinalClass = $aResRow['finalclass']; - $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); - $sUrl = $oObj->GetHyperlink(); - $sStatus = ''; - $sCSSRowClass = 'ibo-csv-import--row-modified'; - if ($bSimulate) { - $sMessage = Dict::S('UI:CSVReport-Object-MissingToUpdate'); - } else { - $sMessage = Dict::S('UI:CSVReport-Object-MissingUpdated'); - } - break; - - case 'RowStatus_NewObj': - $iCreated++; - $sStatus = ''; - $sCSSRowClass = 'ibo-csv-import--row-added'; - if ($bSimulate) { - $sMessage = Dict::S('UI:CSVReport-Object-ToCreate'); - } else { - $sFinalClass = $aResRow['finalclass']; - $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); - $sUrl = $oObj->GetHyperlink(); - $sMessage = Dict::S('UI:CSVReport-Object-Created'); - } - break; - - case 'RowStatus_Issue': - $iErrors++; - $sMessage .= GetDivAlert($oStatus->GetDescription()); - $sStatus = '
';//translate - $sCSSMessageClass = 'ibo-csv-import--cell-error'; - $sCSSRowClass = 'ibo-csv-import--row-error'; - if (array_key_exists($iLine, $aData)) { - $aRow = $aData[$iLine]; - $aResult[] = $sTextQualifier.implode($sTextQualifier.$sSeparator.$sTextQualifier, $aRow).$sTextQualifier; // Remove the first line and store it in case of error - } - break; - } - $aTableRow['@class'] = $sCSSRowClass; - $aTableRow['line'] = sprintf("%0{$sMaxLen}d", 1 + $iLine + $iRealSkippedLines); - $aTableRow['status'] = $sStatus; - $aTableRow['object'] = $sUrl; - - foreach ($aFieldsMapping as $iNumber => $sAttCode) { - if (!empty($sAttCode) && ($sAttCode != ':none:') && ($sAttCode != 'finalclass')) { - $oCellStatus = $aResRow[$iNumber - 1]; - $sCellMessage = ''; - if (isset($aExternalKeysByColumn[$iNumber - 1])) { - $sExtKeyName = $aExternalKeysByColumn[$iNumber - 1]; - $oExtKeyCellStatus = $aResRow[$sExtKeyName]; - $oExtKeyCellStatus->SetDisplayableValue($oCellStatus->GetDisplayableValue()); - $oCellStatus = $oExtKeyCellStatus; - } - $sHtmlValue = $oCellStatus->GetDisplayableValue(); - switch (get_class($oCellStatus)) { - case 'CellStatus_Issue': - case 'CellStatus_NullIssue': - $sCellMessage .= GetDivAlert($oCellStatus->GetDescription()); - $aTableRow[$sClassName.'/'.$sAttCode] = '
'.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.'
'; - break; - - case 'CellStatus_SearchIssue': - $sMessage = Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue); - $sDivAlert = GetDivAlert($oCellStatus->GetDescription()); - $sAllowedValuesLinkUrl = $oCellStatus->GetAllowedValuesLinkUrl(); - $sAllowedValuesLinkLabel = Dict::S('UI:CSVImport:ViewAllPossibleValues'); - $aTableRow[$sClassName.'/'.$sAttCode] = - << - $sMessage - $sDivAlert -  $sAllowedValuesLinkLabel - -HTML; - break; - - case 'CellStatus_Ambiguous': - $sMessage = Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue); - $sDivAlert = GetDivAlert($oCellStatus->GetDescription()); - $sSearchLinkUrl = $oCellStatus->GetSearchLinkUrl(); - $sSearchLinkLabel = Dict::S('UI:CSVImport:ViewAllAmbiguousValues'); - $aTableRow[$sClassName.'/'.$sAttCode] = - << - $sMessage - $sDivAlert -  $sSearchLinkLabel - -HTML; - break; - - case 'CellStatus_Modify': - $aTableRow[$sClassName.'/'.$sAttCode] = '
'.$sHtmlValue.'
'; - break; - - default: - $aTableRow[$sClassName.'/'.$sAttCode] = $sHtmlValue.$sCellMessage; - } - } - } - $aTableRow['message'] = "
$sMessage
"; - - $aTableData[] = $aTableRow; - } - - $iUnchanged = count($aRes) - $iErrors - $iModified - $iCreated; - $oContainer = UIContentBlockUIBlockFactory::MakeStandard(); - $oContainer->AddCSSClass("wizContainer"); - $oPage->AddSubBlock($oContainer); - - $oForm = FormUIBlockFactory::MakeStandard('wizForm'); - $oContainer->AddSubBlock($oForm); - - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("transaction_id", utils::GetNewTransactionId())); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("step", ($iCurrentStep + 1))); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("separator", $sSeparator)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("text_qualifier", $sTextQualifier)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("header_line", $bHeaderLine)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("nb_skipped_lines", utils::ReadParam('nb_skipped_lines', '0'))); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("box_skiplines", utils::ReadParam('box_skiplines', '0'))); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("csvdata_truncated", $sCSVDataTruncated, "csvdata_truncated")); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("csvdata", $sCSVData)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("encoding", $sEncoding)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("synchro_scope", $sSynchroScope)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("class_name", $sClassName)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("advanced", $bAdvanced)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("date_time_format", $sDateTimeFormat)); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("custom_date_time_format", $sCustomDateTimeFormat)); - - if (!empty($sSynchroScope)) { - foreach ($aSynchroUpdate as $sKey => $value) { - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("synchro_update[$sKey]", $value)); - } - } - foreach ($aFieldsMapping as $iNumber => $sAttCode) { - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("field[$iNumber]", $sAttCode)); - } - foreach ($aSearchFields as $index => $sDummy) { - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("search_field[$index]", "1")); - } - $aDisplayFilters = array(); - if ($bSimulate) { - $aDisplayFilters['unchanged'] = Dict::S('UI:CSVImport:ObjectsWillStayUnchanged'); - $aDisplayFilters['modified'] = Dict::S('UI:CSVImport:ObjectsWillBeModified'); - $aDisplayFilters['added'] = Dict::S('UI:CSVImport:ObjectsWillBeAdded'); - $aDisplayFilters['errors'] = Dict::S('UI:CSVImport:ObjectsWillHaveErrors'); - } else { - $aDisplayFilters['unchanged'] = Dict::S('UI:CSVImport:ObjectsRemainedUnchanged'); - $aDisplayFilters['modified'] = Dict::S('UI:CSVImport:ObjectsWereModified'); - $aDisplayFilters['added'] = Dict::S('UI:CSVImport:ObjectsWereAdded'); - $aDisplayFilters['errors'] = Dict::S('UI:CSVImport:ObjectsHadErrors'); - } - $oMulticolumn = MultiColumnUIBlockFactory::MakeStandard(); - $oMulticolumn->AddCSSClass('ml-1'); - $oForm->AddSubBlock($oMulticolumn); - - $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' '.sprintf($aDisplayFilters['unchanged'], $iUnchanged), '', "1", "show_unchanged", "checkbox"); - $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); - $oCheckBoxUnchanged->SetBeforeInput(false); - $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); - $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); - $oPage->add_ready_script("$('#show_unchanged').on('click', function(){ToggleRows('ibo-csv-import--row-unchanged')})"); - - $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' '.sprintf($aDisplayFilters['modified'], $iModified), '', "1", "show_modified", "checkbox"); - $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); - $oCheckBoxUnchanged->SetBeforeInput(false); - $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); - $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); - $oPage->add_ready_script("$('#show_modified').on('click', function(){ToggleRows('ibo-csv-import--row-modified')})"); - - $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' '.sprintf($aDisplayFilters['added'], $iCreated), '', "1", "show_created", "checkbox"); - $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); - $oCheckBoxUnchanged->SetBeforeInput(false); - $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); - $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); - $oPage->add_ready_script("$('#show_created').on('click', function(){ToggleRows('ibo-csv-import--row-added')})"); - - $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' '.sprintf($aDisplayFilters['errors'], $iErrors) . '', '', "1", "show_errors", "checkbox"); - $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); - $oCheckBoxUnchanged->SetBeforeInput(false); - $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); - $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); - $oPage->add_ready_script("$('#show_errors').on('click', function(){ToggleRows('ibo-csv-import--row-error')})"); - - $oPanel = PanelUIBlockFactory::MakeNeutral(''); - $oPanel->AddCSSClasses(['ibo-datatable-panel', 'mb-5']); - $oForm->AddSubBlock($oPanel); - - $oTable = DataTableUIBlockFactory::MakeForForm("csvImport", $aColumns, $aTableData); - $oTable->AddOption('bFullscreen', true); - $oPanel->AddSubBlock($oTable); - - - if ($bSimulate) { - $oForm->AddSubBlock(ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Restart'))->SetOnClickJsCode("CSVRestart()")); - } - $oForm->AddSubBlock(ButtonUIBlockFactory::MakeForSecondaryAction(Dict::S('UI:Button:Back'))->SetOnClickJsCode("CSVGoBack()")); - - $bShouldConfirm = false; - if ($bSimulate) { - // if there are *too many* changes, we should ask the user for a confirmation - if (count($aRes) >= MetaModel::GetConfig()->Get('csv_import_min_object_confirmation')) { - $fErrorsPercentage = (100.0 * $iErrors) / count($aRes); - if ($fErrorsPercentage >= MetaModel::GetConfig()->Get('csv_import_errors_percentage')) { - $sMessage = Dict::Format('UI:CSVReport-Stats-Errors', $fErrorsPercentage); - $bShouldConfirm = true; - } - $fCreatedPercentage = (100.0 * $iCreated) / count($aRes); - if ($fCreatedPercentage >= MetaModel::GetConfig()->Get('csv_import_creations_percentage')) { - $sMessage = Dict::Format('UI:CSVReport-Stats-Created', $fCreatedPercentage); - $bShouldConfirm = true; - } - $fModifiedPercentage = (100.0 * $iModified) / count($aRes); - if ($fModifiedPercentage >= MetaModel::GetConfig()->Get('csv_import_modifications_percentage')) { - $sMessage = Dict::Format('UI:CSVReport-Stats-Modified', $fModifiedPercentage); - $bShouldConfirm = true; - } - - } - $sConfirm = $bShouldConfirm ? 'true' : 'false'; - $oForm->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:DoImport'))->SetOnClickJsCode("return DoSubmit({$sConfirm})")); - - } else { - $oForm->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Done'), "", "", true)); - } - - if ($bShouldConfirm) { - $sYesButton = Dict::S('UI:Button:Ok'); - $sNoButton = Dict::S('UI:Button:Cancel'); - $oDlg = UIContentBlockUIBlockFactory::MakeStandard("dlg_confirmation")->SetHasForcedDiv(true); - $oPage->AddSubBlock($oDlg); - $oDlg->AddSubBlock(new Html($sMessage)); - $oDlg->AddSubBlock(new Html(utils::EscapeHtml(Dict::S('UI:CSVImportConfirmMessage')))); - - $oDlgConfirm = UIContentBlockUIBlockFactory::MakeStandard("confirmation_chart")->SetHasForcedDiv(true); - $oDlg->AddSubBlock($oDlgConfirm); - - $sDlgTitle = Dict::S('UI:CSVImportConfirmTitle'); - - $oPage->add_ready_script( - <<add_script( -<<< EOF -function CSVGoBack() -{ - $('input[name=step]').val($iCurrentStep-1); - $('#wizForm').submit(); - -} - -function CSVRestart() -{ - $('input[name=step]').val(1); - $('#wizForm').submit(); - -} - -function ToggleRows(sCSSClass) -{ - $('.'+sCSSClass).toggle(); -} - -function DoSubmit(bConfirm) -{ - if (bConfirm) //Ask for a confirmation - { - $('#dlg_confirmation').dialog('open'); - - var chart = c3.generate({ - bindto: '#confirmation_chart', - data: { - columns: [ - ['errors', $iErrors], - ['created', $iCreated], - ['modified', $iModified], - ['unchanged', $iUnchanged] - ], - colors: { - errors: '#FF6666', - created: '#66FF66', - modified: '#6666FF', - unchanged: '#666666' - }, - names: { - errors: $sErrors, - created: $sCreated, - modified: $sModified, - unchanged: $sUnchanged - }, - type: 'donut' - }, - legend: { - show: true, - } - }); - } - else - { - // Submit the form - $('#wizForm').block(); - $('#wizForm').submit(); - } - return false; -} - -function CancelImport() -{ - $('#dlg_confirmation').dialog('close'); -} - -function RunImport() -{ - $('#dlg_confirmation').dialog('close'); - // Submit the form - $('#wizForm').block(); - $('#wizForm').submit(); -} -EOF - ); - if ($iErrors > 0) - { - return $aResult; - } - else - { - return null; - } + return CSVImportPageProcessor::ProcessData($iBoxSkipLines, $iNbSkippedLines, $sDateTimeFormat, $sCustomDateTimeFormat, $sClassName, $oPage, $aSynchroUpdate, $sCSVData, $sSeparator, $sTextQualifier, $bHeaderLine, $aResult, $aSearchFields, $aFieldsMapping, $bSimulate, $sCSVDataTruncated, + $iCurrentStep, $sEncoding, + $bAdvanced, $sSynchroScope); } + + /** * Perform the actual load of the CSV data and display the results * @param WebPage $oPage The web page to display the wizard diff --git a/sources/Service/Import/CSVImportPageProcessor.php b/sources/Service/Import/CSVImportPageProcessor.php new file mode 100644 index 000000000..0089a110f --- /dev/null +++ b/sources/Service/Import/CSVImportPageProcessor.php @@ -0,0 +1,610 @@ +GetClass(); // If a synchronization scope is set, then the class is fixed ! + $oSet = new DBObjectSet($oSearch); + $iCount = $oSet->Count(); + DisplaySynchroBanner($oPage, $sClassName, $iCount); + } else { + $sSynchroScope = ''; + $aSynchroUpdate = null; + } + + // Parse the data set + $oCSVParser = new CSVParser($sCSVData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop')); + $aData = $oCSVParser->ToArray($iSkippedLines); + $iRealSkippedLines = $iSkippedLines; + if ($bHeaderLine) { + $aResult[] = $sTextQualifier . implode($sTextQualifier . $sSeparator . $sTextQualifier, array_shift($aData)) . $sTextQualifier; // Remove the first line and store it in case of error + $iRealSkippedLines++; + } + + // Format for the line numbers + $sMaxLen = (strlen('' . count($aData)) < 3) ? 3 : strlen('' . count($aData)); // Pad line numbers to the appropriate number of chars, but at least 3 + + // Compute the list of search/reconciliation criteria + $aSearchKeys = []; + foreach ($aSearchFields as $index => $sDummy) { + $sSearchField = $aFieldsMapping[$index]; + $aMatches = []; + if (preg_match('/(.+)->(.+)/', $sSearchField, $aMatches) > 0) { + $sSearchField = $aMatches[1]; + $aSearchKeys[$aMatches[1]] = ''; + } else { + $aSearchKeys[$sSearchField] = ''; + } + if (!MetaModel::IsValidFilterCode($sClassName, $sSearchField)) { + // Remove invalid or unmapped search fields + $aSearchFields[$index] = null; + unset($aSearchKeys[$sSearchField]); + } + } + + // Compute the list of fields and external keys to process + $aExtKeys = []; + $aAttributes = []; + $aExternalKeysByColumn = []; + foreach ($aFieldsMapping as $iNumber => $sAttCode) { + $iIndex = $iNumber - 1; + if (!empty($sAttCode) && ($sAttCode != ':none:') && ($sAttCode != 'finalclass')) { + if (preg_match('/(.+)->(.+)/', $sAttCode, $aMatches) > 0) { + $sAttribute = $aMatches[1]; + $sField = $aMatches[2]; + $aExtKeys[$sAttribute][$sField] = $iIndex; + $aExternalKeysByColumn[$iIndex] = $sAttribute; + } else { + if ($sAttCode == 'id') { + $aAttributes['id'] = $iIndex; + } else { + $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode); + if ($oAttDef->IsExternalKey()) { + $aExtKeys[$sAttCode]['id'] = $iIndex; + $aExternalKeysByColumn[$iIndex] = $sAttCode; + } else { + $aAttributes[$sAttCode] = $iIndex; + } + } + } + } + } + + $oMyChange = null; + if (!$bSimulate) { + // We're doing it for real, let's create a change + $sUserString = CMDBChange::GetCurrentUserName() . ' (CSV)'; + CMDBObject::SetCurrentChangeFromParams($sUserString, CMDBChangeOrigin::CSV_INTERACTIVE); + $oMyChange = CMDBObject::GetCurrentChange(); + } + + $oBulk = new BulkChange( + $sClassName, + $aData, + $aAttributes, + $aExtKeys, + array_keys($aSearchKeys), + empty($sSynchroScope) ? null : $sSynchroScope, + $aSynchroUpdate, + $sChosenDateFormat, // date format + true // localize + ); + + $oPage->AddSubBlock(InputUIBlockFactory::MakeForHidden("csvdata_truncated", $sCSVDataTruncated, "csvdata_truncated")); + $aRes = $oBulk->Process($oMyChange); + + $aColumns = []; + $aColumns ["line"] = ["label" => "Line"]; + $aColumns ["status"] = ["label" => "Status"]; + $aColumns ["object"] = ["label" => "Object"]; + foreach ($aFieldsMapping as $sAttCode) { + if (!empty($sAttCode) && ($sAttCode != ':none:') && ($sAttCode != 'finalclass')) { + $aColumns[$sClassName . '/' . $sAttCode] = ["label" => MetaModel::GetLabel($sClassName, $sAttCode)]; + } + } + $aColumns["message"] = ["label" => "Message"]; + + $iErrors = 0; + $iCreated = 0; + $iModified = 0; + + $aTableData = []; + $sAppRootUrl = utils::GetAbsoluteUrlAppRoot(); + + foreach ($aRes as $iLine => $aResRow) { + /** @var string[]|CellChangeSpec[] $aResRow */ + $aTableRow = []; + $oStatus = $aResRow['__STATUS__']; + $sUrl = ''; + $sMessage = ''; + $sCSSRowClass = ''; + $sCSSMessageClass = 'cell_ok'; + switch (get_class($oStatus)) { + case 'RowStatus_NoChange': + $sFinalClass = $aResRow['finalclass']; + $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); + $sUrl = $oObj->GetHyperlink(); + $sStatus = ''; + $sCSSRowClass = 'ibo-csv-import--row-unchanged'; + break; + + case 'RowStatus_Modify': + $iModified++; + $sFinalClass = $aResRow['finalclass']; + $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); + $sUrl = $oObj->GetHyperlink(); + $sStatus = ''; + $sCSSRowClass = 'ibo-csv-import--row-modified'; + break; + + case 'RowStatus_Disappeared': + $iModified++; + $sFinalClass = $aResRow['finalclass']; + $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); + $sUrl = $oObj->GetHyperlink(); + $sStatus = ''; + $sCSSRowClass = 'ibo-csv-import--row-modified'; + if ($bSimulate) { + $sMessage = Dict::S('UI:CSVReport-Object-MissingToUpdate'); + } else { + $sMessage = Dict::S('UI:CSVReport-Object-MissingUpdated'); + } + break; + + case 'RowStatus_NewObj': + $iCreated++; + $sStatus = ''; + $sCSSRowClass = 'ibo-csv-import--row-added'; + if ($bSimulate) { + $sMessage = Dict::S('UI:CSVReport-Object-ToCreate'); + } else { + $sFinalClass = $aResRow['finalclass']; + $oObj = MetaModel::GetObject($sFinalClass, $aResRow['id']->GetPureValue()); + $sUrl = $oObj->GetHyperlink(); + $sMessage = Dict::S('UI:CSVReport-Object-Created'); + } + break; + + case 'RowStatus_Issue': + $iErrors++; + $sMessage = self::GetDivAlert($oStatus->GetDescription()); + $sStatus = '
';//translate + $sCSSMessageClass = 'ibo-csv-import--cell-error'; + $sCSSRowClass = 'ibo-csv-import--row-error'; + if (array_key_exists($iLine, $aData)) { + $aRow = $aData[$iLine]; + $aResult[] = $sTextQualifier . implode($sTextQualifier . $sSeparator . $sTextQualifier, $aRow) . $sTextQualifier; // Remove the first line and store it in case of error + } + break; + } + $aTableRow['@class'] = $sCSSRowClass; + $aTableRow['line'] = sprintf("%0{$sMaxLen}d", 1 + $iLine + $iRealSkippedLines); + $aTableRow['status'] = $sStatus; + $aTableRow['object'] = $sUrl; + + foreach ($aFieldsMapping as $iNumber => $sAttCode) { + if (!empty($sAttCode) && ($sAttCode != ':none:') && ($sAttCode != 'finalclass')) { + $oCellStatus = $aResRow[$iNumber - 1]; + $sCellMessage = ''; + if (isset($aExternalKeysByColumn[$iNumber - 1])) { + $sExtKeyName = $aExternalKeysByColumn[$iNumber - 1]; + $oExtKeyCellStatus = $aResRow[$sExtKeyName]; + $oExtKeyCellStatus->SetDisplayableValue($oCellStatus->GetCLIValue()); + $oCellStatus = $oExtKeyCellStatus; + } + $sHtmlValue = $oCellStatus->GetHTMLValue(); + switch (get_class($oCellStatus)) { + case 'CellStatus_Issue': + case 'CellStatus_NullIssue': + $sCellMessage .= self::GetDivAlert($oCellStatus->GetDescription()); + $aTableRow[$sClassName . '/' . $sAttCode] = '
' . Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue) . $sCellMessage . '
'; + break; + + case 'CellStatus_SearchIssue': + $sMessage = Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue); + $sDivAlert = self::GetDivAlert($oCellStatus->GetDescription()); + $sAllowedValuesLinkUrl = $oCellStatus->GetAllowedValuesLinkUrl(); + $sAllowedValuesLinkLabel = Dict::S('UI:CSVImport:ViewAllPossibleValues'); + $aTableRow[$sClassName . '/' . $sAttCode] = + << + $sMessage + $sDivAlert +  $sAllowedValuesLinkLabel + +HTML; + break; + + case 'CellStatus_Ambiguous': + $sMessage = Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue); + $sDivAlert = self::GetDivAlert($oCellStatus->GetDescription()); + $sSearchLinkUrl = $oCellStatus->GetSearchLinkUrl(); + $sSearchLinkLabel = Dict::S('UI:CSVImport:ViewAllAmbiguousValues'); + $aTableRow[$sClassName . '/' . $sAttCode] = + << + $sMessage + $sDivAlert +  $sSearchLinkLabel + +HTML; + break; + + case 'CellStatus_Modify': + $aTableRow[$sClassName . '/' . $sAttCode] = '
' . $sHtmlValue . '
'; + break; + + default: + $aTableRow[$sClassName . '/' . $sAttCode] = $sHtmlValue . $sCellMessage; + } + } + } + $aTableRow['message'] = "
$sMessage
"; + + $aTableData[] = $aTableRow; + } + + $iUnchanged = count($aRes) - $iErrors - $iModified - $iCreated; + $oContainer = UIContentBlockUIBlockFactory::MakeStandard(); + $oContainer->AddCSSClass("wizContainer"); + $oPage->AddSubBlock($oContainer); + + $oForm = FormUIBlockFactory::MakeStandard('wizForm'); + $oContainer->AddSubBlock($oForm); + + self::addHiddenInputToForm($oForm, "transaction_id", utils::GetNewTransactionId()); + self::addHiddenInputToForm($oForm, "step", ($iCurrentStep + 1)); + self::addHiddenInputToForm($oForm, "separator", $sSeparator); + self::addHiddenInputToForm($oForm, "text_qualifier", $sTextQualifier); + self::addHiddenInputToForm($oForm, "header_line", $bHeaderLine); + self::addHiddenInputToForm($oForm, "nb_skipped_lines", $iNbSkippedLines); + self::addHiddenInputToForm($oForm, "box_skiplines", $iBoxSkipLines); + self::addHiddenInputToForm($oForm, "csvdata_truncated", $sCSVDataTruncated, "csvdata_truncated"); + self::addHiddenInputToForm($oForm, "csvdata", $sCSVData); + self::addHiddenInputToForm($oForm, "encoding", $sEncoding); + self::addHiddenInputToForm($oForm, "synchro_scope", $sSynchroScope); + self::addHiddenInputToForm($oForm, "class_name", $sClassName); + self::addHiddenInputToForm($oForm, "advanced", $bAdvanced); + self::addHiddenInputToForm($oForm, "date_time_format", $sDateTimeFormat); + self::addHiddenInputToForm($oForm, "custom_date_time_format", $sCustomDateTimeFormat); + if (!empty($sSynchroScope)) { + foreach ($aSynchroUpdate as $sKey => $value) { + $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("synchro_update[$sKey]", $value)); + } + } + foreach ($aFieldsMapping as $iNumber => $sAttCode) { + $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("field[$iNumber]", $sAttCode)); + } + foreach ($aSearchFields as $index => $sDummy) { + $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("search_field[$index]", "1")); + } + $aDisplayFilters = []; + if ($bSimulate) { + $aDisplayFilters['unchanged'] = Dict::S('UI:CSVImport:ObjectsWillStayUnchanged'); + $aDisplayFilters['modified'] = Dict::S('UI:CSVImport:ObjectsWillBeModified'); + $aDisplayFilters['added'] = Dict::S('UI:CSVImport:ObjectsWillBeAdded'); + $aDisplayFilters['errors'] = Dict::S('UI:CSVImport:ObjectsWillHaveErrors'); + } else { + $aDisplayFilters['unchanged'] = Dict::S('UI:CSVImport:ObjectsRemainedUnchanged'); + $aDisplayFilters['modified'] = Dict::S('UI:CSVImport:ObjectsWereModified'); + $aDisplayFilters['added'] = Dict::S('UI:CSVImport:ObjectsWereAdded'); + $aDisplayFilters['errors'] = Dict::S('UI:CSVImport:ObjectsHadErrors'); + } + $oMulticolumn = MultiColumnUIBlockFactory::MakeStandard(); + $oMulticolumn->AddCSSClass('ml-1'); + $oForm->AddSubBlock($oMulticolumn); + + $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' ' . sprintf($aDisplayFilters['unchanged'], $iUnchanged), '', "1", "show_unchanged", "checkbox"); + $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); + $oCheckBoxUnchanged->SetBeforeInput(false); + $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); + $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); + $oPage->add_ready_script("$('#show_unchanged').on('click', function(){ToggleRows('ibo-csv-import--row-unchanged')})"); + + $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' ' . sprintf($aDisplayFilters['modified'], $iModified), '', "1", "show_modified", "checkbox"); + $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); + $oCheckBoxUnchanged->SetBeforeInput(false); + $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); + $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); + $oPage->add_ready_script("$('#show_modified').on('click', function(){ToggleRows('ibo-csv-import--row-modified')})"); + + $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' ' . sprintf($aDisplayFilters['added'], $iCreated), '', "1", "show_created", "checkbox"); + $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); + $oCheckBoxUnchanged->SetBeforeInput(false); + $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); + $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); + $oPage->add_ready_script("$('#show_created').on('click', function(){ToggleRows('ibo-csv-import--row-added')})"); + + $oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel(' ' . sprintf($aDisplayFilters['errors'], $iErrors) . '', '', "1", "show_errors", "checkbox"); + $oCheckBoxUnchanged->GetInput()->SetIsChecked(true); + $oCheckBoxUnchanged->SetBeforeInput(false); + $oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox'); + $oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged)); + $oPage->add_ready_script("$('#show_errors').on('click', function(){ToggleRows('ibo-csv-import--row-error')})"); + + $oPanel = PanelUIBlockFactory::MakeNeutral(''); + $oPanel->AddCSSClasses(['ibo-datatable-panel', 'mb-5']); + $oForm->AddSubBlock($oPanel); + + $oTable = DataTableUIBlockFactory::MakeForForm("csvImport", $aColumns, $aTableData); + $oTable->AddOption('bFullscreen', true); + $oPanel->AddSubBlock($oTable); + + + if ($bSimulate) { + $oForm->AddSubBlock(ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Restart'))->SetOnClickJsCode("CSVRestart()")); + } + $oForm->AddSubBlock(ButtonUIBlockFactory::MakeForSecondaryAction(Dict::S('UI:Button:Back'))->SetOnClickJsCode("CSVGoBack()")); + + $bShouldConfirm = false; + if ($bSimulate) { + // if there are *too many* changes, we should ask the user for a confirmation + if (count($aRes) >= MetaModel::GetConfig()->Get('csv_import_min_object_confirmation')) { + $fErrorsPercentage = (100.0 * $iErrors) / count($aRes); + if ($fErrorsPercentage >= MetaModel::GetConfig()->Get('csv_import_errors_percentage')) { + $sConfirmationMessage = Dict::Format('UI:CSVReport-Stats-Errors', $fErrorsPercentage); + $bShouldConfirm = true; + } + $fCreatedPercentage = (100.0 * $iCreated) / count($aRes); + if ($fCreatedPercentage >= MetaModel::GetConfig()->Get('csv_import_creations_percentage')) { + $sConfirmationMessage = Dict::Format('UI:CSVReport-Stats-Created', $fCreatedPercentage); + $bShouldConfirm = true; + } + $fModifiedPercentage = (100.0 * $iModified) / count($aRes); + if ($fModifiedPercentage >= MetaModel::GetConfig()->Get('csv_import_modifications_percentage')) { + $sConfirmationMessage = Dict::Format('UI:CSVReport-Stats-Modified', $fModifiedPercentage); + $bShouldConfirm = true; + } + + } + $sConfirm = $bShouldConfirm ? 'true' : 'false'; + $oForm->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:DoImport'))->SetOnClickJsCode("return DoSubmit($sConfirm)")); + + } else { + $oForm->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Done'), "", "", true)); + } + + if ($bShouldConfirm) { + $sYesButton = Dict::S('UI:Button:Ok'); + $sNoButton = Dict::S('UI:Button:Cancel'); + $oDlg = UIContentBlockUIBlockFactory::MakeStandard("dlg_confirmation")->SetHasForcedDiv(true); + $oPage->AddSubBlock($oDlg); + $oDlg->AddSubBlock(new Html($sConfirmationMessage)); + $oDlg->AddSubBlock(new Html(utils::EscapeHtml(Dict::S('UI:CSVImportConfirmMessage')))); + + $oDlgConfirm = UIContentBlockUIBlockFactory::MakeStandard("confirmation_chart")->SetHasForcedDiv(true); + $oDlg->AddSubBlock($oDlgConfirm); + + $sDlgTitle = Dict::S('UI:CSVImportConfirmTitle'); + + $oPage->add_ready_script( + <<add_script( + <<< EOF +function CSVGoBack() +{ + $('input[name=step]').val($iCurrentStep-1); + $('#wizForm').submit(); + +} + +function CSVRestart() +{ + $('input[name=step]').val(1); + $('#wizForm').submit(); + +} + +function ToggleRows(sCSSClass) +{ + $('.'+sCSSClass).toggle(); +} + +function DoSubmit(bConfirm) +{ + if (bConfirm) //Ask for a confirmation + { + $('#dlg_confirmation').dialog('open'); + + var chart = c3.generate({ + bindto: '#confirmation_chart', + data: { + columns: [ + ['errors', $iErrors], + ['created', $iCreated], + ['modified', $iModified], + ['unchanged', $iUnchanged] + ], + colors: { + errors: '#FF6666', + created: '#66FF66', + modified: '#6666FF', + unchanged: '#666666' + }, + names: { + errors: $sErrors, + created: $sCreated, + modified: $sModified, + unchanged: $sUnchanged + }, + type: 'donut' + }, + legend: { + show: true, + } + }); + } + else + { + // Submit the form + $('#wizForm').block(); + $('#wizForm').submit(); + } + return false; +} + +function CancelImport() +{ + $('#dlg_confirmation').dialog('close'); +} + +function RunImport() +{ + $('#dlg_confirmation').dialog('close'); + // Submit the form + $('#wizForm').block(); + $('#wizForm').submit(); +} +EOF + ); + if ($iErrors > 0) { + return $aResult; + } else { + return null; + } + } + + /** + * @param string $message + * + * @return string + */ + private static function GetDivAlert(string $message): string + { + return "
$message
\n"; + } + + /** + * @param Form $oForm + * @param string $name + * @param mixed $value + * @param null $id + * + * @return void + */ + public static function AddHiddenInputToForm(Form $oForm, string $name, mixed $value, $id = null): void + { + $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden($name, (string)$value, $id)); + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php index a288a404d..a4df1d4b7 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -10,6 +10,7 @@ use CMDBSource; use DeprecatedCallsLog; use MySQLTransactionNotClosedException; use PHPUnit\Framework\TestCase; +use ReflectionMethod; use SetupUtils; use const DEBUG_BACKTRACE_IGNORE_ARGS; @@ -77,7 +78,54 @@ abstract class ItopTestCase extends TestCase } } - protected function setUp(): void { + /** + * @param array $args + * @param string $sExportFileName relative to log folder + * @param array $aExcludedParams + * Function that aims to export the values of the parameters of a function in a file + * You can call the function anywhere like following : + * ``` + * require __DIR__ . '/../../../tests/php-unit-tests/vendor/autoload.php'; // required to include phpunit autoload + * ItopTestCase::ExportFunctionParameterValues(func_get_args(), "parameters.txt"); + * ``` + * Useful to generate realistic data for tests providers + * + * @return string + * @throws \ReflectionException + */ + public static function ExportFunctionParameterValues(array $args, string $sExportFileName, array $aExcludedParams = []): string + { + // get sclass et function dans la callstrack + + // in the callstack get the call function name + $aCallStack = debug_backtrace(); + $sCallFunction = $aCallStack[1]['function']; + // in the casll stack get the call class name + $sCallClass = $aCallStack[1]['class']; + $reflectionFunc = new ReflectionMethod($sCallClass, $sCallFunction); + $parameters = $reflectionFunc->getParameters(); + + $aParamValues = []; + foreach ($parameters as $index => $param) { + $aParamValues[$param->getName()] = $args[$index] ?? null; + } + + $paramValues = $aParamValues; + foreach ($aExcludedParams as $sExcludedParam) { + unset($paramValues[$sExcludedParam]); + } + + // extract oPage from the array in parameters and make a foreach on exlucded parameters + foreach ($aExcludedParams as $sExcludedParam) { + unset($paramValues[$sExcludedParam]); + } + + $var_export = var_export($paramValues, true); + file_put_contents(APPROOT.'/log/' .$sExportFileName, $var_export); + return $var_export; + } + + protected function setUp(): void { parent::setUp(); $this->debug("\n----------\n---------- ".$this->getName()."\n----------\n"); diff --git a/tests/php-unit-tests/unitary-tests/core/BulkChangeTest.php b/tests/php-unit-tests/unitary-tests/core/BulkChangeTest.php index a5630ae49..7ca006bfa 100644 --- a/tests/php-unit-tests/unitary-tests/core/BulkChangeTest.php +++ b/tests/php-unit-tests/unitary-tests/core/BulkChangeTest.php @@ -2,7 +2,7 @@ namespace Combodo\iTop\Test\UnitTest\Core; -use CMDBSource; +use BulkChange; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use MetaModel; @@ -43,7 +43,7 @@ class BulkChangeTest extends ItopDataTestCase "contactid" => array("first_name" => 0, "name" => 1, "email" => 2), ); - $oBulk = new \BulkChange( + $oBulk = new BulkChange( "UserLocal", $aData, $aAttributes, @@ -70,18 +70,19 @@ class BulkChangeTest extends ItopDataTestCase /** * test $oBulk->Process with server 1 from demo datas - * @dataProvider BulkChangeProvider * - * @param $aData + * @dataProvider bulkChangeWithoutInitDataProvider + * + * @param $aCSVData * @param $aAttributes * @param $aExtKeys * @param $aReconcilKeys */ - public function testBulkChangeIssue($aData, $aAttributes, $aExtKeys, $aReconcilKeys, $aResult) { + public function testBulkChangeWithoutInitData($aCSVData, $aAttributes, $aExtKeys, $aReconcilKeys, $aResult, array $aResultHTML = null) { $this->debug("aReconcilKeys:".$aReconcilKeys[0]); - $oBulk = new \BulkChange( + $oBulk = new BulkChange( "Server", - $aData, + $aCSVData, $aAttributes, $aExtKeys, $aReconcilKeys, @@ -102,67 +103,117 @@ class BulkChangeTest extends ItopDataTestCase foreach ($aRow as $i => $oCell) { if ($i !== "finalclass" && $i !== "__STATUS__" && $i !== "__ERRORS__" && array_key_exists($i, $aResult)) { $this->debug("i:".$i); - $this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue()); + $this->debug('GetCLIValue:'.$oCell->GetCLIValue()); $this->debug("aResult:".$aResult[$i]); - $this->assertEquals($aResult[$i], $oCell->GetDisplayableValue()); + $this->assertEquals($aResult[$i], $oCell->GetCLIValue()); + if (null !== $aResultHTML) { + $this->assertEquals($aResultHTML[$i], $oCell->GetHTMLValue()); + } } } } } } - public function BulkChangeProvider() { + public function bulkChangeWithoutInitDataProvider() { return [ "Case 3, 5 et 8 : unchanged" => [ - [["Demo", "Server1", "1", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [0 => "Demo", "org_id" => "3", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "unchanged"], + "csvData" => + [["Demo", "Server1", "1", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconciliation Keys"=> + ["id"], + "expectedResult"=> + [0 => "Demo", "org_id" => "3", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "unchanged"], + "expectedResultHTML"=> + [0 => "Demo", "org_id" => "3", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "unchanged"], ], "Case 9 : wrong date format" => [ - [["Demo", "Server1", "1", "production", "date"]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "'date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], - ], + "csvData" => + [["Demo", "Server1", "1", "production", " + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconciliation Keys"=> + ["id"], + "expectedResult"=> + [ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "' 1, "__STATUS__" => "Issue: wrong date format"], + "expectedResultHTML"=> + [ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "'<date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], + ], "Case 1 : no match" => [ - [["Bad", "Server1", "1", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [0 => 'Bad', "org_id" => "No match for value 'Bad'",1 => "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + "csvData" => + [[" + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconciliation Keys"=> + ["id"], + "expectedResult"=> + [0 => ' "No match for value ' "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + "expectedResultHTML"=> + [0 => '<Bad', "org_id" => "No match for value '<Bad'",1 => "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], ], "Case 10 : Missing mandatory value" => [ - [["", "Server1", "1", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [0 => null, "org_id" => "Invalid value for attribute", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], - ], + "csvData" => + [["", "Server1", "1", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconciliation Keys"=> + ["id"], + "expectedResult"=> + [0 => null, "org_id" => "Invalid value for attribute", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + "expectedResultHTML"=> + [0 => null, "org_id" => "Invalid value for attribute", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"], + ], "Case 6 : Unexpected value" => [ - [["Demo", "Server1", "1", "", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ - 0 => "Demo", - "org_id" => "3", - 1 => "Server1", - 2 => "1", - 3 => "'<svg onclick"alert(1)">' is an invalid value", - 4 => "", - "id" => 1, - "__STATUS__" => "Issue: Unexpected attribute value(s)", - "__ERRORS__" => "Unexpected value for attribute 'status': no match found, check spelling"], + "csvData" => + [["Demo", "Server1", "1", "", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconciliation Keys"=> + ["id"], + "expectedResult"=> + [ + 0 => "Demo", + "org_id" => "3", + 1 => "Server1", + 2 => "1", + 3 => '\'\' is an invalid value', + 4 => "", + "id" => 1, + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Unexpected value for attribute 'status': no match found, check spelling" + ], + "expectedResultHTML"=> + [ + 0 => "Demo", + "org_id" => "3", + 1 => "Server1", + 2 => "1", + 3 => '\'<svg onclick"alert(1)">\' is an invalid value', + 4 => "", + "id" => 1, + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Unexpected value for attribute 'status': no match found, check spelling" + ], ], ]; } /** * test $oBulk->Process with new server datas - * @dataProvider CSVImportProvider + * + * @dataProvider bulkChangeWithExistingDataProvider * * @param $aInitData * @param $aCsvData @@ -170,7 +221,7 @@ class BulkChangeTest extends ItopDataTestCase * @param $aExtKeys * @param $aReconcilKeys */ - public function testCas1BulkChangeIssue($aInitData, $aCsvData, $aAttributes, $aExtKeys, $aReconcilKeys, $aResult) { + public function testBulkChangeWithExistingData($aInitData, $aCsvData, $aAttributes, $aExtKeys, $aReconcilKeys, $aResult, $aResultHTML= null) { //change value during the test $db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled'); MetaModel::GetConfig()->Set('db_core_transactions_enabled',false); @@ -185,10 +236,16 @@ class BulkChangeTest extends ItopDataTestCase )); $aCsvData[0][2]=$oServer->GetKey(); $aResult[2]=$oServer->GetKey(); - $aResult["id"]=$oServer->GetKey(); + if ($aResult["id"]==="{Id of the server created by the test}") { + $aResult["id"]=$oServer->GetKey(); + if ($aResultHTML!==null){ + $aResultHTML[2]=$oServer->GetKey(); + $aResultHTML["id"]=$oServer->GetKey(); + } + } $this->debug("oServer->GetKey():".$oServer->GetKey()); } - $oBulk = new \BulkChange( + $oBulk = new BulkChange( "Server", $aCsvData, $aAttributes, @@ -209,145 +266,318 @@ class BulkChangeTest extends ItopDataTestCase foreach ($aRow as $i => $oCell) { if ($i !== "finalclass" && $i !== "__STATUS__" && $i !== "__ERRORS__" && array_key_exists($i, $aResult)) { $this->debug("i:".$i); - $this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue()); + $this->debug('GetCLIValue:'.$oCell->GetCLIValue()); $this->debug("aResult:".$aResult[$i]); - $this->assertEquals( $aResult[$i], $oCell->GetDisplayableValue(), "failure on " . get_class($oCell) . ' cell type'); + $this->assertEquals( $aResult[$i], $oCell->GetCLIValue(), "failure on " . get_class($oCell) . ' cell type for cell number ' . $i ); + if (null !== $aResultHTML) { + $this->assertEquals($aResultHTML[$i], $oCell->GetHTMLValue(), "failure on " . get_class($oCell) . ' cell type for cell number ' . $i); + } } else if ($i === "__ERRORS__") { $sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : ""; $this->assertEquals( $sErrors, $oCell->GetDescription()); } } - $this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue()); + $this->assertEquals( $aResult[0], $aRow[0]->GetCLIValue()); + if (null !== $aResultHTML) { + $this->assertEquals($aResultHTML[0], $aRow[0]->GetHTMLValue()); + } } } MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled); } - public function CSVImportProvider() { + public function bulkChangeWithExistingDataProvider() { return [ + "Ambigous case on reconciliation" => [ + "initData"=> + ["1", "Server1", "production", ""], + "csvData" => + [[">Demo", "Server1"]], + "attributes"=> + ["name" => 1], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["name"], + "expectedResult"=> + [ + 0 => ">Demo", + "org_id" => "n/a", + 1 => "Server1", + "id" => "Invalid value for attribute", + "__STATUS__" => "Issue: ambiguous reconciliation", + "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", + ], + "expectedResultHTML"=> + [ + 0 => ">Demo", + "org_id" => "n/a", + 1 => "Server1", + "id" => "Invalid value for attribute", + "__STATUS__" => "Issue: ambiguous reconciliation", + "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", + ], + ], "Case 6 - 1 : Unexpected value (update)" => [ - ["1", "ServerTest", "production", ""], - [["Demo", "ServerTest", "key", "BadValue", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ - 0 => "Demo", - "org_id" => "3", - 1 => "ServerTest", - 2 => "1", - 3 => "'BadValue' is an invalid value", - 4 => "", - "id" => 1, - "__STATUS__" => "Issue: Unexpected attribute value(s)", - "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", - ], + "initData"=> + ["1", ">ServerTest", "production", ""], + "csvData"=> + [["Demo", ">ServerTest", "key - will be automatically overwritten by test", ">BadValue", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ + "id" => "{Id of the server created by the test}", + 0 => "Demo", + "org_id" => "3", + 1 => ">ServerTest", + 2 => "1", + 3 => "'>BadValue' is an invalid value", + 4 => "", + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", + ], + "expectedResultHTML"=> + [ + "id" => "{Id of the server created by the test}", + 0 => "Demo", + "org_id" => "3", + 1 => ">ServerTest", + 2 => "1", + 3 => "'>BadValue' is an invalid value", + 4 => "", + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", + ], + ], "Case 6 - 2 : Unexpected value (update)" => [ - ["1", "ServerTest", "production", ""], - [["Demo", "ServerTest", "key", "", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ - 0 => "Demo", - "org_id" => "3", - 1 => "ServerTest", - 2 => "1", - 3 => "'<svg onclick"alert(1)">' is an invalid value", - 4 => "", - "id" => 1, - "__STATUS__" => "Issue: Unexpected attribute value(s)", - "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", - ], + "initData"=> + ["1", ">ServerTest", "production", ""], + "csvData"=> + [["Demo", ">ServerTest", "key", "", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ + "id" => "{Id of the server created by the test}", + 0 => "Demo", + "org_id" => "3", + 1 => ">ServerTest", + 2 => "1", + 3 => '\'\' is an invalid value', + 4 => "", + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", + ], + "expectedResultHTML"=> + [ + "id" => "{Id of the server created by the test}", + 0 => "Demo", + "org_id" => "3", + 1 => ">ServerTest", + 2 => "1", + 3 => "'<svg onclick"alert(1)">' is an invalid value", + 4 => "", + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", + ], ], "Case 6 - 3 : Unexpected value (creation)" => [ - [], - [["Demo", "ServerTest", "", ""]], - ["name" => 1, "status" => 2, "purchase_date" => 3], - ["org_id" => ["name" => 0]], - ["name"], - [ - 0 => "Demo", - "org_id" => "3", - 1 => "\"ServerTest\"", - 2 => "'<svg onclick"alert(1)">' is an invalid value", - 3 => "", - "id" => 1, - "__STATUS__" => "Issue: Unexpected attribute value(s)", - "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", - ], + "initData"=> + [], + "csvData"=> + [["Demo", ">ServerTest", "", ""]], + "attributes"=> + ["name" => 1, "status" => 2, "purchase_date" => 3], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["name"], + "expectedResult"=> + [ + "id" => "{Id of the server created by the test}", + 0 => "Demo", + "org_id" => "3", + 1 => "\">ServerTest\"", + 2 => '\'\' is an invalid value', + 3 => "", + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", + ], + "expectedResultHTML"=> + [ + "id" => "{Id of the server created by the test}", + 0 => "Demo", + "org_id" => "3", + 1 => ">ServerTest", + 2 => "'<svg onclick"alert(1)">' is an invalid value", + 3 => "", + "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Allowed 'status' value(s): stock,implementation,production,obsolete", + ], ], "Case 8 : unchanged name" => [ - ["1", "", "production", ""], - [["Demo", "", "key", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [0 => "Demo", "org_id" => "3", 1 => "<svg onclick"alert(1)">", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "updated 1 cols"], + "initData"=> + ["1", "", "production", ""], + "csvData"=> + [["Demo", "", "key", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "3", 1 => '', 2 => "1", 3 => "production", 4 => "", "__STATUS__" => "updated 1 cols"], + "expectedResultHTML"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "3", 1 => '<svg onclick"alert(1)">', 2 => "1", 3 => "production", 4 => "", "__STATUS__" => "updated 1 cols"], ], "Case 3, 5 et 8 : unchanged 2" => [ - ["1", "ServerTest", "production", ""], - [["Demo", "ServerTest", "1", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [0 => "Demo", "org_id" => "3", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "updated 1 cols"], + "initData"=> + ["1", ">ServerTest", "production", ""], + "csvData"=> + [["Demo", ">ServerTest", "1", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "3", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => "", "__STATUS__" => "updated 1 cols"], + "expectedResultHTML"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "3", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => "", "__STATUS__" => "updated 1 cols"], ], "Case 9 - 1: wrong date format" => [ - ["1", "ServerTest", "production", ""], - [["Demo", "ServerTest", "1", "production", "date"]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], - ], + "initData"=> + ["1", ">ServerTest", "production", ""], + "csvData"=> + [["Demo", ">ServerTest", "1", "production", "date>"]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "n/a", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => "'date>' is an invalid value", "__STATUS__" => "Issue: wrong date format"], + "expectedResultHTML"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "n/a", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => "'date>' is an invalid value", "__STATUS__" => "Issue: wrong date format"], + ], "Case 9 - 2: wrong date format" => [ - ["1", "ServerTest", "production", ""], - [["Demo", "ServerTest", "1", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'<svg onclick"alert(1)">' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], + "initData"=> + ["1", ">ServerTest", "production", ""], + "csvData"=> + [["Demo", ">ServerTest", "1", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "n/a", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => '\'\' is an invalid value',"__STATUS__" => "Issue: wrong date format"], + "expectedResultHTML"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "n/a", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => '\'<svg onclick"alert(1)">\' is an invalid value',"__STATUS__" => "Issue: wrong date format"], + ], "Case 1 - 1 : no match" => [ - ["1", "ServerTest", "production", ""], - [["Bad", "ServerTest", "1", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ 0 => "Bad", "org_id" => "No match for value 'Bad'",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)", - "__ERRORS__" => "Object not found", - ], + "initData"=> + ["1", ">ServerTest", "production", ""], + "csvData"=> + [[">Bad", ">ServerTest", "1", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ "id" => "{Id of the server created by the test}", + 0 => ">Bad", "org_id" => "No match for value '>Bad'",1 => ">ServerTest",2 => "1", 3 => "production", 4 => "", "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Object not found", + ], + "expectedResultHTML"=> + [ "id" => "{Id of the server created by the test}", + 0 => ">Bad", "org_id" => "No match for value '>Bad'",1 => ">ServerTest",2 => "1", 3 => "production", 4 => "", "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Object not found", + ], ], "Case 1 - 2 : no match" => [ - ["1", "ServerTest", "production", ""], - [["", "ServerTest", "1", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ 0 => "<svg fonclick"alert(1)">", "org_id" => "No match for value ''",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)", - "__ERRORS__" => "Object not found", - ], + "initData"=> + ["1", ">ServerTest", "production", ""], + "csvData"=> + [["", ">ServerTest", "1", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ "id" => "{Id of the server created by the test}", + 0 => '', "org_id" => "No match for value ''",1 => ">ServerTest",2 => "1", 3 => "production", 4 => "", "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Object not found", + ], + "expectedResultHTML"=> + [ "id" => "{Id of the server created by the test}", + 0 => '<svg onclick"alert(1)">', "org_id" => "No match for value '<svg onclick"alert(1)">'",1 => ">ServerTest",2 => "1", 3 => "production", 4 => "", "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Object not found", + ], ], "Case 10 : Missing mandatory value" => [ - ["1", "ServerTest", "production", ""], - [["", "ServerTest", "1", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ 0 => "", "org_id" => "Invalid value for attribute", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)", - "__ERRORS__" => "Null not allowed", - ], + "initData"=> + ["1", ">ServerTest", "production", ""], + "csvData"=> + [["", ">ServerTest", "1", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ "id" => "{Id of the server created by the test}", + 0 => "", "org_id" => "Invalid value for attribute", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => "", "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Null not allowed", + ], + "expectedResultHTML"=> + [ "id" => "{Id of the server created by the test}", + 0 => "", "org_id" => "Invalid value for attribute", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => "", "__STATUS__" => "Issue: Unexpected attribute value(s)", + "__ERRORS__" => "Null not allowed", + ], ], - - "Case 0 : Date format" => [ - ["1", "ServerTest", "production", "2020-02-01"], - [["Demo", "ServerTest", "1", "production", "2020-20-03"]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'2020-20-03' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], + "Case 0 : Date format ok but incorrect date" => [ + "initData"=> + ["1", ">ServerTest", "production", "2020-02-01"], + "csvData"=> + [["Demo", ">ServerTest", "1", "production", "2020-20-03"]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [ "id" => "{Id of the server created by the test}", + 0 => "Demo", "org_id" => "n/a", 1 => ">ServerTest", 2 => "1", 3 => "production", 4 => "'2020-20-03' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"], ], ]; } @@ -356,7 +586,8 @@ class BulkChangeTest extends ItopDataTestCase /** * test $oBulk->Process with new server and new organization datas - * @dataProvider CSVImportProvider2 + * + * @dataProvider bulkChangeWithExistingDataAndSpecificOrgProvider * * @param $aInitData * @param $aCsvData @@ -364,27 +595,39 @@ class BulkChangeTest extends ItopDataTestCase * @param $aExtKeys * @param $aReconcilKeys */ - public function testCas2BulkChangeIssue($aInitData, $aCsvData, $aAttributes, $aExtKeys, $aReconcilKeys, $aResult) { + public function testBulkChangeWithExistingDataAndSpecificOrg($aInitData, $aCsvData, $aAttributes, $aExtKeys, $aReconcilKeys, $aResult, $aResultHTML = null) { //change value during the test $db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled'); MetaModel::GetConfig()->Set('db_core_transactions_enabled',false); if (is_array($aInitData) && sizeof($aInitData) != 0) { /** @var Server $oServer */ $oOrganisation = $this->createObject('Organization', array( - 'name' => $aInitData[0] + 'name' => $aInitData["orgName"] )); - $aResult["org_id"] = $oOrganisation->GetKey(); + if ($aResult["org_id"]==="{org id of the server created by the test}"){ + $aResult["org_id"] = $oOrganisation->GetKey(); + if ($aResultHTML!==null){ + $aResultHTML["org_id"]=$oOrganisation->GetKey(); + } + } + $oServer = $this->createObject('Server', array( - 'name' => $aInitData[1], - 'status' => $aInitData[2], + 'name' => $aInitData["serverName"], + 'status' => $aInitData["serverStatus"], 'org_id' => $oOrganisation->GetKey(), - 'purchase_date' => $aInitData[3], + 'purchase_date' => $aInitData["serverPurchaseDate"], )); $aCsvData[0][2]=$oServer->GetKey(); $aResult[2]=$oServer->GetKey(); - $aResult["id"]=$oServer->GetKey(); + if ($aResult["id"]==="{Id of the server created by the test}") { + $aResult["id"]=$oServer->GetKey(); + if ($aResultHTML!==null){ + $aResultHTML[2]=$oServer->GetKey(); + $aResultHTML["id"]=$oServer->GetKey(); + } + } } - $oBulk = new \BulkChange( + $oBulk = new BulkChange( "Server", $aCsvData, $aAttributes, @@ -401,9 +644,12 @@ class BulkChangeTest extends ItopDataTestCase foreach ($aRow as $i => $oCell) { if ($i !== "finalclass" && $i !== "__STATUS__" && $i !== "__ERRORS__" && array_key_exists($i, $aResult)) { $this->debug("i:".$i); - $this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue()); + $this->debug('GetCLIValue:'.$oCell->GetCLIValue()); $this->debug("aResult:".$aResult[$i]); - $this->assertEquals($aResult[$i], $oCell->GetDisplayableValue()); + $this->assertEquals($aResult[$i], $oCell->GetCLIValue(), "$i cell is incorrect"); + if (null !== $aResultHTML) { + $this->assertEquals($aResultHTML[$i], $oCell->GetHTMLValue()); + } } elseif ($i === "__STATUS__") { $sStatus = $aRow['__STATUS__']; $this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription()); @@ -412,36 +658,109 @@ class BulkChangeTest extends ItopDataTestCase $this->assertEquals( $sErrors, $oCell->GetDescription()); } } - $this->assertEquals($aResult[0], $aRow[0]->GetDisplayableValue()); + $this->assertEquals($aResult[0], $aRow[0]->GetCLIValue()); + if (null !== $aResultHTML) { + $this->assertEquals($aResultHTML[0], $aRow[0]->GetHTMLValue()); + } } MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled); } - public function CSVImportProvider2() { + public function bulkChangeWithExistingDataAndSpecificOrgProvider() { return [ + "Ambigous case " => [ + "initData"=> + ["orgName" => "Demo", "serverName" => ">ServerYO", "serverStatus" => "production", "serverPurchaseDate" =>""], + "csvData" => + [["Demo", ">Server1"]], + "attributes"=> + ["name" => 1], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["org_id"], + "expectedResult"=> + [ + 0 => "Demo", + "org_id" => "Invalid value for attribute", + 1 => ">Server1", + "id" => "Invalid value for attribute", + "__STATUS__" => "Issue: failed to reconcile", + "__ERRORS__" => "Allowed 'status' value(s): stock,implemfentation,production,obsolete", + ], + "expectedResultHTML"=> + [ + 0 => "Demo", + "org_id" => "Invalid value for attribute", + 1 => ">Server1", + "id" => "Invalid value for attribute", + "__STATUS__" => "Issue: failed to reconcile", + "__ERRORS__" => "Allowed 'status' value(s): stock,implemfentation,production,obsolete", + ], + ], "Case 3 : unchanged name" => [ - ["dodo","ServerYO", "production", ""], - [["dodo", "ServerYO", "key", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [0 => "dodo", "org_id" => "3", 1 => "ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "unchanged"], + "initData"=> + ["orgName" => ">dodo", "serverName" => ">ServerYO", "serverStatus" => "production", "serverPurchaseDate" =>""], + "csvData"=> + [[">dodo", ">ServerYO", "key will be set by the test", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [0 => ">dodo", "org_id" => "{org id of the server created by the test}", 1 => ">ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => "{Id of the server created by the test}", "__STATUS__" => "unchanged"], + "expectedResultHTML"=> + [0 => ">dodo", "org_id" => "{org id of the server created by the test}", 1 => ">ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => "{Id of the server created by the test}", "__STATUS__" => "unchanged"], ], "Case 3 bis : unchanged name" => [ - ["","ServerYO", "production", ""], - [["", "ServerYO", "key", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [0 => "<svg >", "org_id" => "3", 1 => "ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "unchanged"], + "initData"=> + ["orgName" =>"", "serverName" => ">ServerYO", "serverStatus" => "production", "serverPurchaseDate" =>""], + "csvData"=> + [["", ">ServerYO", "key", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [0 => "", "org_id" => "{org id of the server created by the test}", 1 => ">ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => "{Id of the server created by the test}", "__STATUS__" => "unchanged"], + "expectedResultHTML"=> + [0 => "<svg >", "org_id" => "{org id of the server created by the test}", 1 => ">ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => "{Id of the server created by the test}", "__STATUS__" => "unchanged"], ], "Case 3 ter : unchanged name" => [ - ["","ServerYO", "production", ""], - [["", "ServerYO", "key", "production", ""]], - ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], - ["org_id" => ["name" => 0]], - ["id"], - [0 => "<svg onclick"alert(1)" >", "org_id" => "3", 1 => "ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "unchanged"], + "initData"=> + ["orgName" => "", "serverName" => ">ServerYO", "serverStatus" => "production", "serverPurchaseDate" =>""], + "csvData"=> + [["", ">ServerYO", "key", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["id"], + "expectedResult"=> + [0 => '', "org_id" => "{org id of the server created by the test}", 1 => ">ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => "{Id of the server created by the test}", "__STATUS__" => "unchanged"], + "expectedResultHTML"=> + [0 => '<svg onclick"alert(1)" >', "org_id" => "{org id of the server created by the test}", 1 => ">ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => "{Id of the server created by the test}", "__STATUS__" => "unchanged"], + ], + "Case reconciliation on external key" => [ + "initData"=> + ["orgName" => "", "serverName" => ">ServerYO", "serverStatus" => "production", "serverPurchaseDate" =>""], + "csvData"=> + [["", ">ServerYO", "key", "production", ""]], + "attributes"=> + ["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4], + "extKeys"=> + ["org_id" => ["name" => 0]], + "reconcilKeys"=> + ["org_id", "name"], + "expectedResult"=> + [0 => '', "org_id" => "{org id of the server created by the test}", 1 => ">ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => "{Id of the server created by the test}", "__STATUS__" => "unchanged"], + "expectedResultHTML"=> + [0 => '<svg onclick"alert(1)" >', "org_id" => "{org id of the server created by the test}", 1 => ">ServerYO", 2 => "1", 3 => "production", 4 => "", "id" => "{Id of the server created by the test}", "__STATUS__" => "unchanged"], ], ]; } diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-config/BulkChangeExtKeyTest.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-config/BulkChangeExtKeyTest.php index e85b9c472..a6106580b 100644 --- a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-config/BulkChangeExtKeyTest.php +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-config/BulkChangeExtKeyTest.php @@ -299,7 +299,6 @@ class BulkChangeExtKeyTest extends ItopDataTestCase { $this->debug("Process:"); static::assertNotNull($aRes); $this->debug("assertNotNull:"); - var_dump($aRes); foreach ($aRes as $aRow) { if (array_key_exists('__STATUS__', $aRow)) { $sStatus = $aRow['__STATUS__']; @@ -308,12 +307,11 @@ class BulkChangeExtKeyTest extends ItopDataTestCase { foreach ($aRow as $i => $oCell) { if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") { $this->debug("i:" . $i); - $this->debug('GetDisplayableValue:' . $oCell->GetDisplayableValue()); if (array_key_exists($i, $aResult)) { $this->debug("aResult:" . var_export($aResult[$i])); if ($oCell instanceof \CellStatus_SearchIssue || $oCell instanceof \CellStatus_Ambiguous) { - $this->assertEquals($aResult[$i][0], $oCell->GetDisplayableValue(), + $this->assertEquals($aResult[$i][0], $oCell->GetCLIValue(), "failure on " . get_class($oCell) . ' cell type'); $this->assertEquals($sSearchLinkUrl, $oCell->GetSearchLinkUrl(), "failure on " . get_class($oCell) . ' cell type'); @@ -326,7 +324,7 @@ class BulkChangeExtKeyTest extends ItopDataTestCase { $this->assertEquals($sErrors, $oCell->GetDescription()); } } - $this->assertEquals($aResult[0], $aRow[0]->GetDisplayableValue()); + $this->assertEquals($aResult[0], $aRow[0]->GetCLIValue()); } } MetaModel::GetConfig()->Set('db_core_transactions_enabled', $db_core_transactions_enabled); diff --git a/webservices/import.php b/webservices/import.php index 7b0ac2869..cb9738771 100644 --- a/webservices/import.php +++ b/webservices/import.php @@ -884,7 +884,7 @@ try if (isset($aRowData["finalclass"]) && isset($aRowData["id"])) { $aRowDisp["__OBJECT_CLASS__"] = $aRowData["finalclass"]; - $aRowDisp["__OBJECT_ID__"] = $aRowData["id"]->GetDisplayableValue(); + $aRowDisp["__OBJECT_ID__"] = $aRowData["id"]->GetCLIValue(); } else { @@ -903,7 +903,7 @@ try if (is_object($value)) { - $aRowDisp["$sKey"] = $value->GetDisplayableValueAndDescription(); + $aRowDisp["$sKey"] = $value->GetCLIValueAndDescription(); } else {