mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
1074 lines
50 KiB
PHP
1074 lines
50 KiB
PHP
<?php
|
|
|
|
/**
|
|
* 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<string, string> attcode as key, iCol as value */
|
|
protected $m_aAttList;
|
|
/** @var array<string, array<string, string>> 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<string, mixed> 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 "<pre>\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 "</pre>\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(\Combodo\iTop\Application\WebPage\WebPage $oPage, $bFromAjax = false, $bShowAll = false)
|
|
{
|
|
$sAjaxDivId = "CSVImportHistory";
|
|
if (!$bFromAjax) {
|
|
$oPage->add('<div id="' . $sAjaxDivId . '">');
|
|
}
|
|
|
|
$oPage->p(Dict::S('UI:History:BulkImports+') . ' <span id="csv_history_reload"></span>');
|
|
|
|
$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 = '<a href="csvimport.php?step=10&changeid=' . $oChange->GetKey() . $oAppContext->GetForLink(true) . '">' . $oChange->Get('date') . '</a>';
|
|
$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('<p>' . Dict::Format('UI:CountOfResults', $oBulkChanges->Count()) . ' <a class="truncated" onclick="OnTruncatedHistoryToggle(false);">' . Dict::S('UI:CollapseList') . '</a></p>');
|
|
} 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('<p>' . $sCollapsedLabel . ' <a class="truncated" onclick="OnTruncatedHistoryToggle(true);">' . $sLinkLabel . '</p>');
|
|
|
|
$oPage->add_ready_script(
|
|
<<<EOF
|
|
$('#$sAjaxDivId table.listResults').addClass('truncated');
|
|
$('#$sAjaxDivId table.listResults tr:last td').addClass('truncated');
|
|
EOF
|
|
);
|
|
|
|
|
|
$sAppContext = $oAppContext->GetForLink();
|
|
$oPage->add_script(
|
|
<<<EOF
|
|
function OnTruncatedHistoryToggle(bShowAll)
|
|
{
|
|
$('#csv_history_reload').html('<img src="' + GetAbsoluteUrlAppRoot() + 'images/indicator.gif"/>');
|
|
$.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('</div>');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display the details of an import
|
|
*
|
|
* @param \Combodo\iTop\Application\WebPage\iTopWebPage $oPage
|
|
* @param $iChange
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
static function DisplayImportHistoryDetails(\Combodo\iTop\Application\WebPage\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("<div><p><h1>" . Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName()) . "</h1></p></div>\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);
|
|
}
|
|
} |