mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-26 13:44:19 +01:00
Compare commits
62 Commits
designer-3
...
8413-fix-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9699846f2 | ||
|
|
3ff3dcba54 | ||
|
|
657fc912bf | ||
|
|
5ae5221f6f | ||
|
|
b15ca2fbc9 | ||
|
|
cb382eab4e | ||
|
|
9723cde24c | ||
|
|
7ae49e2cf4 | ||
|
|
cb13a7a5b4 | ||
|
|
9618e47045 | ||
|
|
d84506ea9e | ||
|
|
13239c2751 | ||
|
|
8b30e36dd1 | ||
|
|
80b290ab88 | ||
|
|
81b20ee583 | ||
|
|
a5545b0084 | ||
|
|
d72e861dfe | ||
|
|
92385273ff | ||
|
|
61c25f85e7 | ||
|
|
b1cf2ec137 | ||
|
|
7549ded51d | ||
|
|
38683c20b1 | ||
|
|
81791dd253 | ||
|
|
e77e0eec9f | ||
|
|
f5ddbbbe0e | ||
|
|
6811a82e1a | ||
|
|
960133c0df | ||
|
|
544c4ae888 | ||
|
|
9ee18c2f36 | ||
|
|
43a10e6944 | ||
|
|
bf8269fee1 | ||
|
|
c99995563e | ||
|
|
9a895a7fbd | ||
|
|
f5011bb200 | ||
|
|
29c75f626b | ||
|
|
0562563cbb | ||
|
|
40068bd913 | ||
|
|
1ec139782e | ||
|
|
056dce4d78 | ||
|
|
9b1395db03 | ||
|
|
8fd9eb6a84 | ||
|
|
1142bf327c | ||
|
|
278496eaf6 | ||
|
|
77ba0b398f | ||
|
|
04ca7bf603 | ||
|
|
5aee7f4722 | ||
|
|
292701b71c | ||
|
|
533b57ab99 | ||
|
|
be8d348b25 | ||
|
|
6c7a98fe3d | ||
|
|
ec2203229b | ||
|
|
2f699d355a | ||
|
|
da4457f5b4 | ||
|
|
97848cea4f | ||
|
|
2ccd883c9a | ||
|
|
eabd68ff77 | ||
|
|
2b493787b1 | ||
|
|
d74c850621 | ||
|
|
4e575e17a3 | ||
|
|
94d6eca0c1 | ||
|
|
355da8ec0a | ||
|
|
5f006c45db |
@@ -93,6 +93,12 @@ gitGraph
|
||||
checkout support/3.2
|
||||
commit id: "2024-06-25" tag: "3.2.0-beta1" type: REVERSE
|
||||
commit id: "2024-08-07" tag: "3.2.0"
|
||||
checkout support/2.7
|
||||
commit id: "2025-02-25" tag: "2.7.12"
|
||||
checkout support/3.1
|
||||
commit id: "2025-02-25 " tag: "3.1.3"
|
||||
checkout support/3.2
|
||||
commit id: "2025-02-25 " tag: "3.2.1"
|
||||
```
|
||||
|
||||
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).
|
||||
|
||||
@@ -106,6 +106,7 @@ We would like to give a special thank you 🤗 to the people from the community
|
||||
- Raenker, Martin
|
||||
- Roháč, Richard (a.k.a [@RohacRichard](https://github.com/RohacRichard))
|
||||
- Rosenke, Stephan
|
||||
- Rossi, Tommaso (a.k.a [@tomrss](https://www.github.com/tomrss))
|
||||
- Rudner, Björn (a.k.a [@rudnerbjoern](https://github.com/rudnerbjoern))
|
||||
- Šafránek, Jaroslav (a.k.a [jkcinik](https://sourceforge.net/u/jkcinik/profile/) on SourceForge)
|
||||
- Seki, Shoji
|
||||
@@ -115,6 +116,7 @@ We would like to give a special thank you 🤗 to the people from the community
|
||||
- Tarjányi, Csaba (a.k.a [@tacsaby](https://github.com/tacsaby))
|
||||
- Tulio, Marco
|
||||
- Turrubiates, Miguel
|
||||
- Vlk, Karel (a.k.a [@vlk-charles](https://www.github.com/vlk-charles))
|
||||
|
||||
### Aliases
|
||||
|
||||
|
||||
@@ -3079,7 +3079,7 @@ JS
|
||||
} else {
|
||||
$sCancelButtonOnClickScript .= "function() { BackToDetails('$sClass', $iKey, '$sDefaultUrl', $sJSToken)};";
|
||||
}
|
||||
$sCancelButtonOnClickScript .= "$('#form_{$this->m_iFormId} button.cancel').on('click', fOnClick{$this->m_iFormId}CancelButton);";
|
||||
$sCancelButtonOnClickScript .= "$('#form_{$this->m_iFormId} button.cancel').on('click.navigation.itop', fOnClick{$this->m_iFormId}CancelButton);";
|
||||
$oPage->add_ready_script($sCancelButtonOnClickScript);
|
||||
|
||||
$iFieldsCount = count($aFieldsMap);
|
||||
|
||||
@@ -524,9 +524,7 @@ EOF
|
||||
*/
|
||||
public function Render($oPage, $bEditMode = false, $aExtraParams = array(), $bCanEdit = true)
|
||||
{
|
||||
if (!array_key_exists('dashboard_div_id', $aExtraParams)) {
|
||||
$aExtraParams['dashboard_div_id'] = utils::Sanitize($this->GetId(), '', 'element_identifier');
|
||||
}
|
||||
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
|
||||
/** @var \DashboardLayoutMultiCol $oLayout */
|
||||
$oLayout = new $this->sLayoutClass();
|
||||
@@ -1052,7 +1050,7 @@ EOF
|
||||
$sSelectorHtml .= '</div>';
|
||||
|
||||
$sFile = addslashes($this->GetDefinitionFile());
|
||||
$sReloadURL = $this->GetReloadURL();
|
||||
$sReloadURL = json_encode($this->GetReloadURL());
|
||||
|
||||
$bFromDashboardPage = isset($aAjaxParams['from_dashboard_page']) ? isset($aAjaxParams['from_dashboard_page']) : false;
|
||||
if ($bFromDashboardPage) {
|
||||
@@ -1141,7 +1139,6 @@ JS
|
||||
->AddCSSClass('ibo-action-button');
|
||||
|
||||
$oToolbar->AddSubBlock($oActionButton);
|
||||
|
||||
$aActions = array();
|
||||
$sFile = addslashes(utils::LocalPath($this->sDefinitionFile));
|
||||
$sJSExtraParams = json_encode($aExtraParams);
|
||||
@@ -1166,7 +1163,7 @@ JS
|
||||
$oToolbar->AddSubBlock($oActionButton)
|
||||
->AddSubBlock($oActionsMenu);
|
||||
|
||||
$sReloadURL = $this->GetReloadURL();
|
||||
$sReloadURL = json_encode($this->GetReloadURL());
|
||||
$oPage->add_script(
|
||||
<<<EOF
|
||||
function EditDashboard(sId, sDashboardFile, aExtraParams)
|
||||
@@ -1273,7 +1270,7 @@ EOF
|
||||
$sTitle = json_encode($this->sTitle);
|
||||
$sFile = json_encode($this->GetDefinitionFile());
|
||||
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
|
||||
$sReloadURL = $this->GetReloadURL();
|
||||
$sReloadURL = json_encode($this->GetReloadURL());
|
||||
|
||||
$sExitConfirmationMessage = addslashes(Dict::S('UI:NavigateAwayConfirmationMessage'));
|
||||
$sCancelConfirmationMessage = addslashes(Dict::S('UI:CancelConfirmationMessage'));
|
||||
|
||||
@@ -250,7 +250,7 @@ class UIExtKeyWidget
|
||||
foreach ($aAdditionalField as $sAdditionalField) {
|
||||
array_push($aArguments, $oObj->Get($sAdditionalField));
|
||||
}
|
||||
$aOption['additional_field'] = utils::HtmlEntities(vsprintf($sFormatAdditionalField, $aArguments));
|
||||
$aOption['additional_field'] = utils::HtmlEntities(utils::VSprintf($sFormatAdditionalField, $aArguments));
|
||||
}
|
||||
if (!empty($sObjectImageAttCode)) {
|
||||
// Try to retrieve image for contact
|
||||
|
||||
@@ -521,8 +521,8 @@ class utils
|
||||
|
||||
// For URL
|
||||
case static::ENUM_SANITIZATION_FILTER_URL:
|
||||
// N°6350 - returns only valid URLs
|
||||
$retValue = filter_var($value, FILTER_VALIDATE_URL);
|
||||
$retValue = filter_var($value, FILTER_SANITIZE_URL);
|
||||
$retValue = filter_var($retValue, FILTER_VALIDATE_URL);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -1555,6 +1555,11 @@ class utils
|
||||
}
|
||||
$aResult[] = new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL', '$sContext')");
|
||||
$aResult[] = new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')");
|
||||
if (ApplicationMenu::IsMenuIdEnabled('RunQueriesMenu')) {
|
||||
$oMenuItemPlay = new JSPopupMenuItem('UI:Menu:OpenOQL', Dict::S('UI:Edit:TestQuery'), "OpenOql('$sOQL')");
|
||||
$oMenuItemPlay->SetIconClass('fas fa-play');
|
||||
$aResult[] = $oMenuItemPlay;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -2075,6 +2080,127 @@ SQL;
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a string using vsprintf with safety checks to avoid ValueError
|
||||
*
|
||||
* This method fills missing arguments with their original format specifiers,
|
||||
* then calls vsprintf with the complete array.
|
||||
*
|
||||
* @param string $sFormat The format string
|
||||
* @param array $aArgs The arguments to format
|
||||
* @param bool $bLogErrors Whether to log errors (defaults to true)
|
||||
*
|
||||
* @return string The formatted string
|
||||
* @since 3.2.2
|
||||
*/
|
||||
public static function VSprintf(string $sFormat, array $aArgs, bool $bLogErrors = true): string
|
||||
{
|
||||
// Extract all format specifiers
|
||||
$sPattern = '/%(?:(?:[1-9][0-9]*)\$)?[-+\'0# ]*(?:[0-9]*|\*)?(?:\.(?:[0-9]*|\*))?(?:[hlL])?[diouxXeEfFgGcrs%]/';
|
||||
preg_match_all($sPattern, $sFormat, $aMatches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
// Process matches, keeping track of their positions and excluding escaped percent signs (%%)
|
||||
$aSpecifierMatches = [];
|
||||
foreach ($aMatches[0] as $sMatch) {
|
||||
if ($sMatch[0] !== '%%') {
|
||||
$aSpecifierMatches[] = $sMatch;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for positional specifiers and build position map
|
||||
$bHasPositional = false;
|
||||
$iMaxPosition = 0;
|
||||
$aPositions = [];
|
||||
$aUniquePositions = [];
|
||||
|
||||
foreach ($aSpecifierMatches as $index => $match) {
|
||||
$sSpec = $match[0];
|
||||
if (preg_match('/^%([1-9][0-9]*)\$/', $sSpec, $posMatch)) {
|
||||
$bHasPositional = true;
|
||||
$iPosition = (int)$posMatch[1] - 1; // Convert to 0-based
|
||||
$aPositions[$index] = $iPosition;
|
||||
$aUniquePositions[$iPosition] = true;
|
||||
$iMaxPosition = max($iMaxPosition, $iPosition + 1);
|
||||
} else {
|
||||
$aPositions[$index] = $index;
|
||||
$aUniquePositions[$index] = true;
|
||||
$iMaxPosition = max($iMaxPosition, $index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Count unique positions, this tells us how many arguments we actually need
|
||||
$iExpectedCount = count($aUniquePositions);
|
||||
$iActualCount = count($aArgs);
|
||||
|
||||
// If we have enough arguments, just use vsprintf
|
||||
if ($iActualCount >= $iExpectedCount) {
|
||||
return vsprintf($sFormat, $aArgs);
|
||||
}
|
||||
// else log the error if needed
|
||||
if ($bLogErrors) {
|
||||
IssueLog::Warning("Format string requires $iExpectedCount arguments, but only $iActualCount provided. Format: '$sFormat'" );
|
||||
}
|
||||
|
||||
// Create a replacement map
|
||||
if ($bHasPositional) {
|
||||
// For positional, we need to handle the exact positions
|
||||
$aReplacements = array_fill(0, $iMaxPosition, null);
|
||||
|
||||
// Fill in the real arguments first
|
||||
foreach ($aArgs as $index => $sValue) {
|
||||
if ($index < $iMaxPosition) {
|
||||
$aReplacements[$index] = $sValue;
|
||||
}
|
||||
}
|
||||
|
||||
// For null values in the replacement map, use the original specifier
|
||||
foreach ($aSpecifierMatches as $index => $sMatch) {
|
||||
$iPosition = $aPositions[$index];
|
||||
if ($aReplacements[$iPosition] === null) {
|
||||
// Use the original format specifier when we don't have an argument
|
||||
$aReplacements[$iPosition] = $sMatch[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any remaining nulls (for positions that weren't referenced)
|
||||
$aReplacements = array_filter($aReplacements, static function($val) { return $val !== null; });
|
||||
} else {
|
||||
// For non-positional, we need to map each position
|
||||
$aReplacements = [];
|
||||
$iUsed = 0;
|
||||
|
||||
// Create a map of what values to use for each position
|
||||
$aPositionValues = [];
|
||||
for ($i = 0; $i < $iMaxPosition; $i++) {
|
||||
if (isset($aUniquePositions[$i])) {
|
||||
if ($iUsed < $iActualCount) {
|
||||
// We have an actual argument for this position
|
||||
$aPositionValues[$i] = $aArgs[$iUsed++];
|
||||
} else {
|
||||
// Mark this position to use the original specifier
|
||||
$aPositionValues[$i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the replacements array preserving the original order
|
||||
foreach ($aSpecifierMatches as $index => $sMatch) {
|
||||
$iPosition = $aPositions[$index];
|
||||
if (isset($aPositionValues[$iPosition])) {
|
||||
$aReplacements[] = $aPositionValues[$iPosition];
|
||||
} else {
|
||||
// Use the original format specifier when we don't have an argument
|
||||
$aReplacements[] = $sMatch[0];
|
||||
// Mark this position as used, so if it appears again, it gets the same replacement
|
||||
$aPositionValues[$iPosition] = $sMatch[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the format string with our filled-in arguments
|
||||
return vsprintf($sFormat, $aReplacements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string containing some (valid) HTML markup to plain text
|
||||
*
|
||||
|
||||
@@ -206,7 +206,7 @@ class Dict
|
||||
}
|
||||
|
||||
try{
|
||||
return vsprintf($sLocalizedFormat, $aArguments);
|
||||
return utils::VSprintf($sLocalizedFormat, $aArguments);
|
||||
} catch(\Throwable $e){
|
||||
\IssueLog::Error("Cannot format dict key", null, ["sFormatCode" => $sFormatCode, "sLangCode" => $sLangCode, 'exception_msg' => $e->getMessage() ]);
|
||||
return $sFormatCode.' - '.implode(', ', $aArguments);
|
||||
|
||||
@@ -77,7 +77,7 @@ abstract class ModelReflection
|
||||
return $sFormatCode.' - '.implode(', ', $aArguments);
|
||||
}
|
||||
|
||||
return vsprintf($sLocalizedFormat, $aArguments);
|
||||
return utils::VSprintf($sLocalizedFormat, $aArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,6 +76,52 @@ class ObjectResult
|
||||
$this->fields = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ObjectResult from a DBObject.
|
||||
*
|
||||
* @param DBObject $oObj The object.
|
||||
* @param array|null $aFieldSpec An array of class => attribute codes (Cf. RestUtils::GetFieldList). List of the attributes to be reported.
|
||||
* @param boolean $bExtendedOutput Output all of the link set attributes ?
|
||||
* @param integer $iCode An error code (RestResult::OK is no issue has been found)
|
||||
* @param string $sMessage Description of the error if any, an empty string otherwise
|
||||
*
|
||||
* @return ObjectResult
|
||||
*/
|
||||
public static function FromDBObject(DBObject $oObj, ?array $aFieldSpec = null, $bExtendedOutput = false, $iCode = 0, $sMessage = '') : ObjectResult {
|
||||
|
||||
$oObjRes = new ObjectResult($oObj::class, $oObj->GetKey());
|
||||
$oObjRes->code = $iCode;
|
||||
$oObjRes->message = $sMessage;
|
||||
|
||||
$aFields = null;
|
||||
if (!is_null($aFieldSpec))
|
||||
{
|
||||
// Enum all classes in the hierarchy, starting with the current one
|
||||
foreach (MetaModel::EnumParentClasses($oObj::class, ENUM_PARENT_CLASSES_ALL, false) as $sRefClass)
|
||||
{
|
||||
if (array_key_exists($sRefClass, $aFieldSpec))
|
||||
{
|
||||
$aFields = $aFieldSpec[$sRefClass];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_null($aFields))
|
||||
{
|
||||
// No fieldspec given, or not found...
|
||||
$aFields = array('id', 'friendlyname');
|
||||
}
|
||||
|
||||
foreach ($aFields as $sAttCode)
|
||||
{
|
||||
$oObjRes->AddField($oObj, $sAttCode, $bExtendedOutput);
|
||||
}
|
||||
|
||||
return $oObjRes;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper to make an output value for a given attribute
|
||||
*
|
||||
@@ -204,34 +250,7 @@ class RestResultWithObjects extends RestResult
|
||||
*/
|
||||
public function AddObject($iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
|
||||
{
|
||||
$sClass = get_class($oObject);
|
||||
$oObjRes = new ObjectResult($sClass, $oObject->GetKey());
|
||||
$oObjRes->code = $iCode;
|
||||
$oObjRes->message = $sMessage;
|
||||
|
||||
$aFields = null;
|
||||
if (!is_null($aFieldSpec))
|
||||
{
|
||||
// Enum all classes in the hierarchy, starting with the current one
|
||||
foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sRefClass)
|
||||
{
|
||||
if (array_key_exists($sRefClass, $aFieldSpec))
|
||||
{
|
||||
$aFields = $aFieldSpec[$sRefClass];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_null($aFields))
|
||||
{
|
||||
// No fieldspec given, or not found...
|
||||
$aFields = array('id', 'friendlyname');
|
||||
}
|
||||
|
||||
foreach ($aFields as $sAttCode)
|
||||
{
|
||||
$oObjRes->AddField($oObject, $sAttCode, $bExtendedOutput);
|
||||
}
|
||||
$oObjRes = ObjectResult::FromDBObject($oObject, $aFieldSpec, $bExtendedOutput, $iCode, $sMessage);
|
||||
|
||||
$sObjKey = get_class($oObject).'::'.$oObject->GetKey();
|
||||
$this->objects[$sObjKey] = $oObjRes;
|
||||
|
||||
@@ -435,7 +435,7 @@ class ValueSetObjects extends ValueSetDefinition
|
||||
foreach ($aAdditionalField as $sAdditionalField) {
|
||||
array_push($aArguments, $oObject->Get($sAdditionalField));
|
||||
}
|
||||
$aData['additional_field'] = vsprintf($sFormatAdditionalField, $aArguments);
|
||||
$aData['additional_field'] = utils::VSprintf($sFormatAdditionalField, $aArguments);
|
||||
} else {
|
||||
$aData['additional_field'] = '';
|
||||
}
|
||||
|
||||
@@ -53,14 +53,7 @@ class DBRestore extends DBBackup
|
||||
$sUser = self::EscapeShellArg($this->sDBUser);
|
||||
$sPwd = self::EscapeShellArg($this->sDBPwd);
|
||||
$sDBName = self::EscapeShellArg($this->sDBName);
|
||||
if (empty($this->sMySQLBinDir))
|
||||
{
|
||||
$sMySQLExe = 'mysql';
|
||||
}
|
||||
else
|
||||
{
|
||||
$sMySQLExe = '"'.$this->sMySQLBinDir.'/mysql"';
|
||||
}
|
||||
$sMySQLExe = DBBackup::MakeSafeMySQLCommand($this->sMySQLBinDir, 'mysql');
|
||||
if (is_null($this->iDBPort))
|
||||
{
|
||||
$sPortOption = '';
|
||||
|
||||
@@ -95,12 +95,7 @@ try {
|
||||
//
|
||||
$sMySQLBinDir = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '');
|
||||
$sMySQLBinDir = utils::ReadParam('mysql_bindir', $sMySQLBinDir, true);
|
||||
if (empty($sMySQLBinDir)) {
|
||||
$sMySQLDump = 'mysqldump';
|
||||
} else {
|
||||
//echo 'Info - Found mysql_bindir: '.$sMySQLBinDir;
|
||||
$sMySQLDump = '"'.$sMySQLBinDir.'/mysqldump"';
|
||||
}
|
||||
$sMySQLDump = DBBackup::MakeSafeMySQLCommand($sMySQLBinDir, 'mysqldump');
|
||||
$sCommand = "$sMySQLDump -V 2>&1";
|
||||
|
||||
$aOutput = array();
|
||||
|
||||
@@ -53,13 +53,21 @@ $(document).ready(function()
|
||||
}, 1000);
|
||||
|
||||
// Hide tooltips when a modal is opening, otherwise it might be overlapping it
|
||||
oBodyElem.on('show.bs.modal', function ()
|
||||
oBodyElem.on('show.bs.modal', '.modal', function ()
|
||||
{
|
||||
$(this).find('.tooltip.in').tooltip('hide');
|
||||
|
||||
// Set the z-index of the modal and its backdrop in case we have several modals opened
|
||||
let zIndex = 1050 + (10 * $('.modal:visible').length);
|
||||
$(this).css('z-index', zIndex);
|
||||
// Set the z-index of the backdrop later because it is created after the modal
|
||||
setTimeout(function() {
|
||||
$('.modal-backdrop').not('.modal-stack').css('z-index', zIndex - 1).addClass('modal-stack');
|
||||
}, 10);
|
||||
});
|
||||
|
||||
/*
|
||||
* Display a error message on modal if the content could not be loaded.
|
||||
* Display an error message on modal if the content could not be loaded.
|
||||
*
|
||||
* Note : As of now, we can't display a more detailed message based on the response because Bootstrap doesn't pass response data with the loaded event.
|
||||
*/
|
||||
|
||||
@@ -168,6 +168,13 @@ CombodoModal._InstantiateModal = function(oModalElem, oOptions) {
|
||||
|
||||
// Show modal
|
||||
if (oOptions.auto_open) {
|
||||
|
||||
// Append modal to body if not already in DOM, this is also done when the modal is shown, but it happens after show.bs.modal event is triggered
|
||||
// As we put this event listener on the body, it is not triggered when the modal is not in the DOM yet
|
||||
if (oModalElem.parent().length === 0) {
|
||||
$('body').append(oModalElem);
|
||||
}
|
||||
|
||||
oModalElem.modal('show');
|
||||
}
|
||||
|
||||
|
||||
@@ -335,7 +335,7 @@ class BrowseBrickHelper
|
||||
$aRow[$key] = array(
|
||||
'level_alias' => $key,
|
||||
'id' => $sCurrentObjectId,
|
||||
'name' => $value->Get($sNameAttCode),
|
||||
'name' => utils::EscapeHtml($value->Get($sNameAttCode)),
|
||||
'class' => $sCurrentObjectClass,
|
||||
'action_rules_token' => $this->PrepareActionRulesForItems($aItems, $key, $aLevelsProperties),
|
||||
'metadata' => array(
|
||||
@@ -513,7 +513,7 @@ class BrowseBrickHelper
|
||||
$aItems[$sCurrentIndex] = array(
|
||||
'level_alias' => $aCurrentRowKeys[0],
|
||||
'id' => $aCurrentRowValues[0]->GetKey(),
|
||||
'name' => $aCurrentRowValues[0]->Get($aLevelsProperties[$aCurrentRowKeys[0]]['name_att']),
|
||||
'name' => utils::EscapeHtml($aCurrentRowValues[0]->Get($aLevelsProperties[$aCurrentRowKeys[0]]['name_att'])),
|
||||
'class' => get_class($aCurrentRowValues[0]),
|
||||
'subitems' => array(),
|
||||
'filter_data' => $this->GetFilterData($aLevelsProperties[$aCurrentRowKeys[0]], $aCurrentRowKeys[0], $aCurrentRowValues[0]),
|
||||
|
||||
@@ -156,8 +156,17 @@
|
||||
return row.attributes[attribute_code].sort_value;
|
||||
},
|
||||
filter: function (attribute_code, type, row) {
|
||||
return $.text($.parseHTML(row.attributes[attribute_code]['value_html']));
|
||||
},
|
||||
// Check if the attribute and value_html exist
|
||||
if (!row.attributes[attribute_code] || !row.attributes[attribute_code]['value_html']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Create a temporary div outside the DOM to filter out XSS
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.textContent = row.attributes[attribute_code]['value_html'];
|
||||
|
||||
return tempDiv.textContent;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -717,8 +717,11 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
|
||||
window[sPromiseId].then(function () {
|
||||
$('#ac_create_'+me.id).dialog('open');
|
||||
$('#ac_create_'+me.id).dialog("option", "close", me.OnCloseCreateObject);
|
||||
// Modify the action of the cancel button
|
||||
$('#ac_create_'+me.id+' button.cancel').off('click').on('click', me.CloseCreateObject);
|
||||
// Modify the action of the cancel button and the close button
|
||||
$('#ac_create_'+me.id+' button.cancel').off('click.navigation.itop').on('click.navigation.itop', me.CloseCreateObject);
|
||||
$('.ui-dialog-titlebar:has(+ #ac_create_'+me.id+') button.ui-dialog-titlebar-close').off('click').on('click', function() {
|
||||
$('#ac_create_'+me.id+' button.cancel').trigger('click');
|
||||
});
|
||||
me.ajax_request = null;
|
||||
me.sTargetClass = sLocalTargetClass;
|
||||
// Adjust the dialog's size to fit into the screen
|
||||
|
||||
18
js/utils.js
18
js/utils.js
@@ -369,6 +369,24 @@ function DashletCreationDlg(sOQL, sContext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function OpenOql(sOQL) {
|
||||
sBaseUrl = GetAbsoluteUrlAppRoot() + 'pages/run_query.php';
|
||||
var form = document.createElement("form");
|
||||
form.setAttribute("method", "post");
|
||||
form.setAttribute("action", sBaseUrl);
|
||||
form.setAttribute("target", '_blank');
|
||||
form.setAttribute("id", 'run_query_form');
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'expression';
|
||||
input.value = sOQL;
|
||||
form.appendChild(input);
|
||||
document.body.appendChild(form);
|
||||
// form.submit() is blocked by the browser
|
||||
$('#run_query_form').submit();
|
||||
document.body.removeChild(form);
|
||||
}
|
||||
|
||||
function ShortcutListDlg(sOQL, sDataTableId, sContext) {
|
||||
var sDataTableName = 'datatable_'+sDataTableId;
|
||||
var oTableSettings = {
|
||||
|
||||
@@ -1560,6 +1560,7 @@ return array(
|
||||
'Sabberworm\\CSS\\Value\\URL' => $vendorDir . '/sabberworm/php-css-parser/src/Value/URL.php',
|
||||
'Sabberworm\\CSS\\Value\\Value' => $vendorDir . '/sabberworm/php-css-parser/src/Value/Value.php',
|
||||
'Sabberworm\\CSS\\Value\\ValueList' => $vendorDir . '/sabberworm/php-css-parser/src/Value/ValueList.php',
|
||||
'SanitizeTrait' => $baseDir . '/core/restservices.class.inc.php',
|
||||
'ScalarExpression' => $baseDir . '/core/oql/expression.class.inc.php',
|
||||
'ScalarOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
|
||||
'ScssPhp\\ScssPhp\\Base\\Range' => $vendorDir . '/scssphp/scssphp/src/Base/Range.php',
|
||||
@@ -3201,6 +3202,7 @@ return array(
|
||||
'iPreferencesExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'iProcess' => $baseDir . '/core/backgroundprocess.inc.php',
|
||||
'iQueryModifier' => $baseDir . '/core/querymodifier.class.inc.php',
|
||||
'iRestInputSanitizer' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'iRestServiceProvider' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'iScheduledProcess' => $baseDir . '/core/backgroundprocess.inc.php',
|
||||
'iSelfRegister' => $baseDir . '/core/userrights.class.inc.php',
|
||||
@@ -3228,5 +3230,5 @@ return array(
|
||||
'privUITransactionFile' => $baseDir . '/application/transaction.class.inc.php',
|
||||
'privUITransactionSession' => $baseDir . '/application/transaction.class.inc.php',
|
||||
'utils' => $baseDir . '/application/utils.inc.php',
|
||||
'©' => $vendorDir . '/symfony/cache/Traits/ValueWrapper.php',
|
||||
'<EFBFBD>' => $vendorDir . '/symfony/cache/Traits/ValueWrapper.php',
|
||||
);
|
||||
|
||||
@@ -1950,6 +1950,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Sabberworm\\CSS\\Value\\URL' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/URL.php',
|
||||
'Sabberworm\\CSS\\Value\\Value' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/Value.php',
|
||||
'Sabberworm\\CSS\\Value\\ValueList' => __DIR__ . '/..' . '/sabberworm/php-css-parser/src/Value/ValueList.php',
|
||||
'SanitizeTrait' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
|
||||
'ScalarExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
|
||||
'ScalarOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
|
||||
'ScssPhp\\ScssPhp\\Base\\Range' => __DIR__ . '/..' . '/scssphp/scssphp/src/Base/Range.php',
|
||||
@@ -3591,6 +3592,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'iPreferencesExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'iProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
|
||||
'iQueryModifier' => __DIR__ . '/../..' . '/core/querymodifier.class.inc.php',
|
||||
'iRestInputSanitizer' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'iRestServiceProvider' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'iScheduledProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
|
||||
'iSelfRegister' => __DIR__ . '/../..' . '/core/userrights.class.inc.php',
|
||||
@@ -3618,7 +3620,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'privUITransactionFile' => __DIR__ . '/../..' . '/application/transaction.class.inc.php',
|
||||
'privUITransactionSession' => __DIR__ . '/../..' . '/application/transaction.class.inc.php',
|
||||
'utils' => __DIR__ . '/../..' . '/application/utils.inc.php',
|
||||
'©' => __DIR__ . '/..' . '/symfony/cache/Traits/ValueWrapper.php',
|
||||
'<EFBFBD>' => __DIR__ . '/..' . '/symfony/cache/Traits/ValueWrapper.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
|
||||
@@ -1520,7 +1520,7 @@ catch (Exception $e) {
|
||||
$oErrorPage->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
}
|
||||
$sErrorDetails = ($e instanceof CoreException) ? $e->getHtmlDesc() : $e->getMessage();
|
||||
$oErrorPage->error(Dict::Format('UI:Error_Details', $sErrorDetails));
|
||||
$oErrorPage->error(Dict::Format('UI:Error_Details', utils::EscapeHtml($sErrorDetails)));
|
||||
$oErrorPage->output();
|
||||
|
||||
$sErrorStackTrace = ($e instanceof CoreException) ? $e->getFullStackTraceAsString() : $e->getTraceAsString();
|
||||
|
||||
@@ -89,9 +89,9 @@ try {
|
||||
$oOption = SelectOptionUIBlockFactory::MakeForSelectOption("", Dict::S('UI:CSVImport:ClassesSelectOne'), false);
|
||||
$oSelectBlock->AddSubBlock($oOption);
|
||||
$aValidClasses = array();
|
||||
$aClassCategories = array('bizmodel', 'addon/authentication');
|
||||
$aClassCategories = array('bizmodel', 'addon/authentication', 'grant_by_profile');
|
||||
if (UserRights::IsAdministrator()) {
|
||||
$aClassCategories = array('bizmodel', 'application', 'addon/authentication');
|
||||
$aClassCategories = array('bizmodel', 'application', 'addon/authentication', 'grant_by_profile');
|
||||
}
|
||||
foreach ($aClassCategories as $sClassCategory) {
|
||||
foreach (MetaModel::GetClasses($sClassCategory) as $sClassName) {
|
||||
|
||||
@@ -106,6 +106,8 @@ class DBBackup
|
||||
/** @var string */
|
||||
protected $sDBName;
|
||||
/** @var string */
|
||||
protected $sMySQLBinDir = '';
|
||||
/** @var string */
|
||||
protected $sDBSubName;
|
||||
|
||||
/**
|
||||
@@ -133,7 +135,6 @@ class DBBackup
|
||||
$this->sDBSubName = $oConfig->get('db_subname');
|
||||
}
|
||||
|
||||
protected $sMySQLBinDir = '';
|
||||
|
||||
/**
|
||||
* Create a normalized backup name, depending on the current date/time and Database
|
||||
@@ -362,8 +363,9 @@ class DBBackup
|
||||
}
|
||||
|
||||
$this->LogInfo("Starting backup of $this->sDBHost/$this->sDBName(suffix:'$this->sDBSubName')");
|
||||
$sMySQLBinDir = utils::ReadParam('mysql_bindir', $this->sMySQLBinDir, true);
|
||||
|
||||
$sMySQLDump = $this->GetMysqldumpCommand();
|
||||
$sMySQLDump = $this->MakeSafeMySQLCommand($sMySQLBinDir, 'mysqldump');
|
||||
|
||||
// Store the results in a temporary file
|
||||
$sTmpFileName = self::EscapeShellArg($sBackupFileName);
|
||||
@@ -624,20 +626,22 @@ EOF;
|
||||
|
||||
/**
|
||||
* @return string the command to launch mysqldump (without its params)
|
||||
* @throws \BackupException
|
||||
*/
|
||||
private function GetMysqldumpCommand()
|
||||
public static function MakeSafeMySQLCommand($sMySQLBinDir, string $sCmd)
|
||||
{
|
||||
$sMySQLBinDir = utils::ReadParam('mysql_bindir', $this->sMySQLBinDir, true);
|
||||
if (empty($sMySQLBinDir))
|
||||
{
|
||||
$sMysqldumpCommand = 'mysqldump';
|
||||
if (empty($sMySQLBinDir)) {
|
||||
$sMySQLCommand = $sCmd;
|
||||
}
|
||||
else
|
||||
{
|
||||
$sMysqldumpCommand = '"'.$sMySQLBinDir.'/mysqldump"';
|
||||
else {
|
||||
$sMySQLBinDir = escapeshellcmd($sMySQLBinDir);
|
||||
$sMySQLCommand = '"'.$sMySQLBinDir.'/$sCmd"';
|
||||
if (!file_exists($sMySQLCommand)) {
|
||||
throw new BackupException("$sCmd not found in $sMySQLBinDir");
|
||||
}
|
||||
}
|
||||
|
||||
return $sMysqldumpCommand;
|
||||
return $sMySQLCommand;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
class ModuleInstallation extends cmdbAbstractObject
|
||||
class ModuleInstallation extends DBObject
|
||||
{
|
||||
public static function Init()
|
||||
{
|
||||
|
||||
@@ -552,14 +552,15 @@ class SetupUtils
|
||||
if (empty($sMySQLBinDir) && null != MetaModel::GetConfig()) {
|
||||
$sMySQLBinDir = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '');
|
||||
}
|
||||
|
||||
if (empty($sMySQLBinDir)) {
|
||||
$sMySQLDump = 'mysqldump';
|
||||
}
|
||||
else {
|
||||
$aResult[] = new CheckResult(CheckResult::TRACE, 'Info - Found mysql_bindir: '.$sMySQLBinDir);
|
||||
$sMySQLDump = '"'.$sMySQLBinDir.'/mysqldump"';
|
||||
try {
|
||||
$sMySQLDump = DBBackup::MakeSafeMySQLCommand($sMySQLBinDir, 'mysqldump');
|
||||
} catch (Exception $e) {
|
||||
$aResult[] = new CheckResult(CheckResult::ERROR, $e->getMessage());
|
||||
return $aResult;
|
||||
}
|
||||
if (!empty($sMySQLBinDir)) {
|
||||
$aResult[] = new CheckResult(CheckResult::TRACE, 'Info - Found mysql_bindir: '.$sMySQLBinDir);
|
||||
}
|
||||
$sCommand = "$sMySQLDump -V 2>&1";
|
||||
|
||||
$aOutput = array();
|
||||
|
||||
@@ -143,6 +143,8 @@ $sDBUser = $aDBXmlSettings['user'];
|
||||
$sDBPwd = $aDBXmlSettings['pwd'];
|
||||
$sDBName = $aDBXmlSettings['name'];
|
||||
$sDBPrefix = $aDBXmlSettings['prefix'];
|
||||
$bDBTlsEnabled = $aDBXmlSettings['db_tls_enabled'];
|
||||
$sDBTlsCa = $aDBXmlSettings['db_tls_ca'];
|
||||
|
||||
if ($sMode == 'install')
|
||||
{
|
||||
@@ -219,13 +221,10 @@ if ($sMode == 'install')
|
||||
die("Cleanup not implemented for a partial database (prefix= '$sDBPrefix')\nExiting.");
|
||||
}
|
||||
|
||||
$oMysqli = new mysqli($sDBServer, $sDBUser, $sDBPwd);
|
||||
if ($oMysqli->connect_errno)
|
||||
{
|
||||
die("Cannot connect to the MySQL server (".$oMysqli->connect_errno . ") ".$oMysqli->connect_error."\nExiting");
|
||||
}
|
||||
else
|
||||
try
|
||||
{
|
||||
$oMysqli = CMDBSource::GetMysqliInstance($sDBServer, $sDBUser, $sDBPwd, null, $bDBTlsEnabled, $sDBTlsCa, true);
|
||||
|
||||
if ($oMysqli->select_db($sDBName))
|
||||
{
|
||||
echo "Deleting database '$sDBName'\n";
|
||||
@@ -236,6 +235,10 @@ if ($sMode == 'install')
|
||||
echo "The database '$sDBName' does not seem to exist. Nothing to cleanup.\n";
|
||||
}
|
||||
}
|
||||
catch (MySQLException $e)
|
||||
{
|
||||
die($e->getMessage()."\nExiting");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -312,9 +315,9 @@ if ($bInstall)
|
||||
}
|
||||
else
|
||||
{
|
||||
$oMysqli = new mysqli($sDBServer, $sDBUser, $sDBPwd);
|
||||
if (!$oMysqli->connect_errno)
|
||||
try
|
||||
{
|
||||
$oMysqli = CMDBSource::GetMysqliInstance($sDBServer, $sDBUser, $sDBPwd, null, $bDBTlsEnabled, $sDBTlsCa, true);
|
||||
if ($oMysqli->select_db($sDBName))
|
||||
{
|
||||
// Check the presence of a table to record information about the MTP (from the Designer)
|
||||
@@ -357,6 +360,10 @@ if ($bInstall)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (MySQLException $e)
|
||||
{
|
||||
// Continue anyway
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -82,7 +82,9 @@ class ObjectController extends AbstractController
|
||||
{
|
||||
throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class'));
|
||||
}
|
||||
|
||||
if (!is_subclass_of($sClass, cmdbAbstractObject::class)) {
|
||||
throw new SecurityException('The class "'.$sClass.'" is not a subclass of cmdbAbstractObject so it can\'t be created by the user');
|
||||
}
|
||||
// If the specified class has subclasses, ask the user an instance of which class to create
|
||||
$aSubClasses = MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself
|
||||
$aPossibleClasses = array();
|
||||
|
||||
@@ -435,7 +435,7 @@ class EMailLaminas extends Email
|
||||
|
||||
// Add body content to as a new part
|
||||
$oNewPart = new Part($sBody);
|
||||
$oNewPart->encoding = Mime::ENCODING_8BIT;
|
||||
$oNewPart->encoding = Mime::ENCODING_BASE64;
|
||||
$oNewPart->type = $sMimeType;
|
||||
$oNewPart->charset = 'UTF-8';
|
||||
$oBody->addPart($oNewPart);
|
||||
@@ -463,7 +463,7 @@ class EMailLaminas extends Email
|
||||
}
|
||||
$this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType);
|
||||
$oNewPart = new Part($sText);
|
||||
$oNewPart->encoding = Mime::ENCODING_8BIT;
|
||||
$oNewPart->encoding = Mime::ENCODING_BASE64;
|
||||
$oNewPart->type = $sMimeType;
|
||||
|
||||
// setBody called only to refresh Content-Type to multipart/mixed
|
||||
|
||||
@@ -893,7 +893,7 @@ JS
|
||||
} else if ($oAttDef->IsExternalKey()) {
|
||||
|
||||
/** @var \AttributeExternalKey $oAttDef */
|
||||
$aAttProperties['value_html'] = $oItem->Get($sAttCode.'_friendlyname');
|
||||
$aAttProperties['value_html'] = utils::EscapeHtml($oItem->Get($sAttCode.'_friendlyname'));
|
||||
|
||||
// Checking if user can access object's external key
|
||||
$sObjectUrl = ApplicationContext::MakeObjectUrl($oAttDef->GetTargetClass(), $oItem->Get($sAttCode));
|
||||
|
||||
@@ -213,7 +213,7 @@ class ObjectRepository
|
||||
foreach ($aComplementAttributeSpec[1] as $sAdditionalField) {
|
||||
$aArguments[] = $oDbObject->Get($sAdditionalField);
|
||||
}
|
||||
$aData['additional_field'] = vsprintf($aComplementAttributeSpec[0], $aArguments);
|
||||
$aData['additional_field'] = utils::VSprintf($aComplementAttributeSpec[0], $aArguments);
|
||||
$sAdditionalFieldForHtml = utils::EscapeHtml($aData['additional_field']);
|
||||
$aData['full_description'] = "{$sFriendlyNameForHtml}<br><i><small>{$sAdditionalFieldForHtml}</small></i>";
|
||||
} else {
|
||||
|
||||
@@ -775,7 +775,7 @@ try
|
||||
$oP->add_comment('Objects updated: '.$oStatLog->Get('stats_nb_obj_updated').' ('.$oStatLog->Get('stats_nb_obj_updated_warnings')." warnings)");
|
||||
$oP->add_comment('Objects update errors: '.$oStatLog->Get('stats_nb_obj_updated_errors'));
|
||||
$oP->add_comment('Objects reconciled (updated): '.$oStatLog->Get('stats_nb_obj_new_updated').' ('.$oStatLog->Get('stats_nb_obj_new_updated_warnings').' warnings)');
|
||||
$oP->add_comment('Objects reconciled (unchanged): '.$oStatLog->Get('stats_nb_obj_new_unchanged').' ('.$oStatLog->Get('stats_nb_obj_new_updated_warnings').' warnings)');
|
||||
$oP->add_comment('Objects reconciled (unchanged): '.$oStatLog->Get('stats_nb_obj_new_unchanged').' ('.$oStatLog->Get('stats_nb_obj_new_unchanged_warnings').' warnings)');
|
||||
$oP->add_comment('Objects reconciliation errors: '.$oStatLog->Get('stats_nb_replica_reconciled_errors'));
|
||||
$oP->add_comment('Replica disappeared, no action taken: '.$oStatLog->Get('stats_nb_replica_disappeared_no_action'));
|
||||
}
|
||||
|
||||
@@ -2440,7 +2440,9 @@ class SynchroReplica extends DBObject implements iDisplay
|
||||
// Really modified ?
|
||||
if ($oDestObj->IsModified())
|
||||
{
|
||||
$oDestObj::SetCurrentChange($oChange);
|
||||
if(method_exists(get_class($oDestObj), "SetCurrentChange")){
|
||||
$oDestObj::SetCurrentChange($oChange);
|
||||
}
|
||||
$oDestObj->DBUpdate();
|
||||
$bModified = true;
|
||||
$oStatLog->AddTrace('Updated object - Values: {'.implode(', ', $aValueTrace).'}', $this);
|
||||
@@ -2499,7 +2501,11 @@ class SynchroReplica extends DBObject implements iDisplay
|
||||
$aValueTrace[] = "$sAttCode: $value";
|
||||
}
|
||||
}
|
||||
$oDestObj::SetCurrentChange($oChange);
|
||||
|
||||
if(method_exists(get_class($oDestObj), "SetCurrentChange")){
|
||||
//N°8413 - Make data synchro work on DBObject
|
||||
$oDestObj::SetCurrentChange($oChange);
|
||||
}
|
||||
$iNew = $oDestObj->DBInsert();
|
||||
|
||||
$this->Set('dest_id', $oDestObj->GetKey());
|
||||
@@ -2552,7 +2558,10 @@ class SynchroReplica extends DBObject implements iDisplay
|
||||
$oDestObj->Set($sAttCode, $value);
|
||||
}
|
||||
$this->Set('info_last_modified', date(AttributeDateTime::GetSQLFormat()));
|
||||
$oDestObj::SetCurrentChange($oChange);
|
||||
if(method_exists(get_class($oDestObj), "SetCurrentChange")){
|
||||
//N°8413 - Make data synchro work on DBObject
|
||||
$oDestObj::SetCurrentChange($oChange);
|
||||
}
|
||||
$oDestObj->DBUpdate();
|
||||
$oStatLog->AddTrace('Replica marked as obsolete', $this);
|
||||
$oStatLog->Inc('stats_nb_obj_obsoleted');
|
||||
@@ -2590,7 +2599,10 @@ class SynchroReplica extends DBObject implements iDisplay
|
||||
$oCheckDeletionPlan = new DeletionPlan();
|
||||
if ($oDestObj->CheckToDelete($oCheckDeletionPlan))
|
||||
{
|
||||
$oDestObj::SetCurrentChange($oChange);
|
||||
if(method_exists(get_class($oDestObj), "SetCurrentChange")){
|
||||
//N°8413 - Make data synchro work on DBObject
|
||||
$oDestObj::SetCurrentChange($oChange);
|
||||
}
|
||||
$oDestObj->DBDelete();
|
||||
$this->DBDelete();
|
||||
$oStatLog->Inc('stats_nb_obj_deleted');
|
||||
@@ -2636,7 +2648,7 @@ class SynchroReplica extends DBObject implements iDisplay
|
||||
}
|
||||
|
||||
// $sExtAttCode is a valid attribute code
|
||||
//
|
||||
//
|
||||
$sClass = $this->Get('base_class');
|
||||
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sExtAttCode);
|
||||
@@ -2745,7 +2757,7 @@ class SynchroReplica extends DBObject implements iDisplay
|
||||
public function GetHilightClass()
|
||||
{
|
||||
// Possible return values are:
|
||||
// HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE
|
||||
// HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE
|
||||
return HILIGHT_CLASS_NONE; // Not hilighted by default
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class DictionariesConsistencyAfterSetupTest extends ItopTestCase
|
||||
],
|
||||
'traduction that breaks expected nb of arguments' => [
|
||||
'sTemplate' => 'toto %1$s titi %2$s',
|
||||
'sExpectedTraduction' => 'ITOP::DICT:FORMAT:BROKEN:KEY - 1',
|
||||
'sExpectedTraduction' => 'toto 1 titi %2$s',
|
||||
],
|
||||
'traduction ok' => [
|
||||
'sTemplate' => 'toto %1$s titi',
|
||||
|
||||
@@ -834,9 +834,10 @@ HTML,
|
||||
'bad context_param' => [utils::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM, '%dssD,25_=%:+-', null],
|
||||
'good element_identifier' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, 'AD05nb', 'AD05nb'],
|
||||
'bad element_identifier' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, 'AD05nb+', 'AD05nb'],
|
||||
'array' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, ['AD05nb+','apply_modify'], ['AD05nb','apply_modify']],
|
||||
'good url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://www.w3schools.com', 'https://www.w3schools.com'],
|
||||
'bad url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://www.w3schoo<EFBFBD><EFBFBD>ls.co<EFBFBD>m', null],
|
||||
'url with injection' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://demo.combodo.com/simple/pages/UI.php?operation=full_text&text=<img zzz src=x onerror=alert(1) //>', null],
|
||||
'bad url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https//www.w3schools.com', null],
|
||||
'url with injection' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://demo.combodo.com/simple/pages/UI.php?operation=full_text&text=<img zzz src=x onerror=alert(1) //>', 'https://demo.combodo.com/simple/pages/UI.php?operation=full_text&text=<imgzzzsrc=xonerror=alert(1)//>'],
|
||||
'raw_data' => ['raw_data', '<Test>\s😃😃😃', '<Test>\s😃😃😃'],
|
||||
];
|
||||
}
|
||||
@@ -906,4 +907,94 @@ HTML,
|
||||
$this->assertFalse(utils::GetDocumentFromSelfURL($sURL));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider VSprintfProvider
|
||||
*/
|
||||
public function testVSprintf($sFormat, $aArgs, $sExpected)
|
||||
{
|
||||
$sTested = utils::VSprintf($sFormat, $aArgs, false);
|
||||
$this->assertEquals($sExpected, $sTested);
|
||||
}
|
||||
|
||||
public function VSprintfProvider()
|
||||
{
|
||||
return [
|
||||
// Basic positional specifier tests
|
||||
'Basic positional with enough args' => [
|
||||
'Format: %1$s, %2$d, %3$s',
|
||||
['Hello', 42, 'World'],
|
||||
'Format: Hello, 42, World'
|
||||
],
|
||||
'Basic positional with args in different order' => [
|
||||
'Format: %2$s, %1$d, %3$s',
|
||||
[42, 'Hello', 'World'],
|
||||
'Format: Hello, 42, World'
|
||||
],
|
||||
'Positional with reused specifiers' => [
|
||||
'Format: %1$s, %2$d, %1$s again',
|
||||
['Hello', 42],
|
||||
'Format: Hello, 42, Hello again'
|
||||
],
|
||||
|
||||
// Missing arguments tests
|
||||
'Missing one positional arg' => [
|
||||
'Format: %1$s, %2$d, %3$s',
|
||||
['Hello', 42],
|
||||
'Format: Hello, 42, %3$s'
|
||||
],
|
||||
'Missing multiple positional args' => [
|
||||
'Format: %1$s, %2$s, %3$s, %4$s',
|
||||
['Hello'],
|
||||
'Format: Hello, %2$s, %3$s, %4$s'
|
||||
],
|
||||
'Missing first positional arg' => [
|
||||
'Format: %1$s, %2$s, %3$s',
|
||||
[],
|
||||
'Format: %1$s, %2$s, %3$s'
|
||||
],
|
||||
|
||||
// Edge cases
|
||||
'Positional with larger numbers' => [
|
||||
'Format: %2$s, %1$d, %3$s, %2$s again',
|
||||
[123456, 'Hello', 'World'],
|
||||
'Format: Hello, 123456, World, Hello again'
|
||||
],
|
||||
'Positional specifiers with non-sequential indexes' => [
|
||||
'Format: %3$s then %1$s and %5$d',
|
||||
['first', 'second', 'third', 'fourth', 42],
|
||||
'Format: third then first and 42'
|
||||
],
|
||||
|
||||
// More complex format specifiers
|
||||
'Positional with format modifiers' => [
|
||||
'Format: %1$\'*10s, %2$04d',
|
||||
['Hello', 42],
|
||||
'Format: *****Hello, 0042'
|
||||
],
|
||||
'Positional with various types' => [
|
||||
'Format: String: %1$s, Integer: %2$d, Char: %3$c',
|
||||
['Hello', 42, 65],
|
||||
'Format: String: Hello, Integer: 42, Char: A'
|
||||
],
|
||||
|
||||
// Testing with non-Latin characters
|
||||
'Positional with UTF-8 characters' => [
|
||||
'Format: %1$s %2$s %3$s',
|
||||
['こんにちは', 'Здравствуйте', '你好'],
|
||||
'Format: こんにちは Здравствуйте 你好'
|
||||
],
|
||||
|
||||
// Mixed formats
|
||||
'Mixed positional with complex specifiers' => [
|
||||
'Format: %1$-10s | %2$+d',
|
||||
['Hello', 42],
|
||||
'Format: Hello | +42'
|
||||
],
|
||||
'Reused positional indexes with some missing' => [
|
||||
'Format: %1$s %2$d %1$s %3$s %2$d',
|
||||
['Hello', 42],
|
||||
'Format: Hello 42 Hello %3$s 42'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,23 +28,23 @@ class RestServicesTest extends ItopDataTestCase
|
||||
static::assertJsonStringEqualsJsonString($sExpectedJsonDataSanitized, $sOutputJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
public function providerTestSanitizeJsonInput(): array
|
||||
{
|
||||
return [
|
||||
'core/check_credentials' => [
|
||||
'{"operation": "core/check_credentials", "user": "admin", "password": "admin"}',
|
||||
'{
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
public function providerTestSanitizeJsonInput(): array
|
||||
{
|
||||
return [
|
||||
'core/check_credentials' => [
|
||||
'{"operation": "core/check_credentials", "user": "admin", "password": "admin"}',
|
||||
'{
|
||||
"operation": "core/check_credentials",
|
||||
"user": "admin",
|
||||
"password": "*****"
|
||||
}'
|
||||
],
|
||||
'core/update' => [
|
||||
'{"operation": "core/update", "comment": "Update user", "class": "UserLocal", "key": {"id":1}, "output_fields": "first_name, password", "fields": {"password" : "123456"}}',
|
||||
'{
|
||||
],
|
||||
'core/update' => [
|
||||
'{"operation": "core/update", "comment": "Update user", "class": "UserLocal", "key": {"id":1}, "output_fields": "first_name, password", "fields": {"password" : "123456"}}',
|
||||
'{
|
||||
"operation": "core/update",
|
||||
"comment": "Update user",
|
||||
"class": "UserLocal",
|
||||
@@ -56,10 +56,10 @@ class RestServicesTest extends ItopDataTestCase
|
||||
"password": "*****"
|
||||
}
|
||||
}'
|
||||
],
|
||||
'core/create' => [
|
||||
'{"operation": "core/create", "comment": "Create user", "class": "UserLocal", "fields": {"first_name": "John", "last_name": "Doe", "email": "jd@example/com", "password" : "123456"}}',
|
||||
'{
|
||||
],
|
||||
'core/create' => [
|
||||
'{"operation": "core/create", "comment": "Create user", "class": "UserLocal", "fields": {"first_name": "John", "last_name": "Doe", "email": "jd@example/com", "password" : "123456"}}',
|
||||
'{
|
||||
"operation": "core/create",
|
||||
"comment": "Create user",
|
||||
"class": "UserLocal",
|
||||
@@ -70,9 +70,9 @@ class RestServicesTest extends ItopDataTestCase
|
||||
"password": "*****"
|
||||
}
|
||||
}'
|
||||
],
|
||||
];
|
||||
}
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $sOperation
|
||||
@@ -93,33 +93,33 @@ class RestServicesTest extends ItopDataTestCase
|
||||
static::assertJsonStringEqualsJsonString($sExpectedJsonDataSanitized, json_encode($oRestResultWithObject));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
public function providerTestSanitizeJsonOutput(): array
|
||||
{
|
||||
return [
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
public function providerTestSanitizeJsonOutput(): array
|
||||
{
|
||||
return [
|
||||
|
||||
'core/update' => [
|
||||
'core/update',
|
||||
['comment' => 'Update user', 'class' => 'UserLocal', 'key' => ['login' => 'my_example'], 'output_fields' => 'password', 'fields' => ['password' => 'opkB!req57']],
|
||||
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
|
||||
],
|
||||
'core/create' => [
|
||||
'core/create',
|
||||
['comment' => 'Create user', 'class' => 'UserLocal', 'fields' => ['password' => 'Azertyuiiop*12', 'login' => 'toto', 'profile_list' => [1]]],
|
||||
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
|
||||
],
|
||||
'core/get' => [
|
||||
'core/get',
|
||||
['comment' => 'Get user', 'class' => 'UserLocal', 'key' => ['login' => 'my_example'], 'output_fields' => 'first_name, password'],
|
||||
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
|
||||
],
|
||||
'core/check_credentials' => [
|
||||
'core/check_credentials',
|
||||
['user' => 'admin', 'password' => 'admin'],
|
||||
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
|
||||
],
|
||||
];
|
||||
}
|
||||
'core/update' => [
|
||||
'core/update',
|
||||
['comment' => 'Update user', 'class' => 'UserLocal', 'key' => ['login' => 'my_example'], 'output_fields' => 'password', 'fields' => ['password' => 'opkB!req57']],
|
||||
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
|
||||
],
|
||||
'core/create' => [
|
||||
'core/create',
|
||||
['comment' => 'Create user', 'class' => 'UserLocal', 'fields' => ['password' => 'Azertyuiiop*12', 'login' => 'toto', 'profile_list' => [1]]],
|
||||
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
|
||||
],
|
||||
'core/get' => [
|
||||
'core/get',
|
||||
['comment' => 'Get user', 'class' => 'UserLocal', 'key' => ['login' => 'my_example'], 'output_fields' => 'first_name, password'],
|
||||
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
|
||||
],
|
||||
'core/check_credentials' => [
|
||||
'core/check_credentials',
|
||||
['user' => 'admin', 'password' => 'admin'],
|
||||
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2013-2024 Combodo SAS
|
||||
* This file is part of iTop.
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Synchro;
|
||||
|
||||
use CMDBObject;
|
||||
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
|
||||
use DBObject;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use DBSearch;
|
||||
use EventWithTitleAsReconciliationKey;
|
||||
use Exception;
|
||||
use MetaModel;
|
||||
use SynchroDataSource;
|
||||
use UserLocal;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Class DataSynchroTest
|
||||
*
|
||||
* @package Combodo\iTop\Test\UnitTest\Synchro
|
||||
* @group dataSynchro
|
||||
* @group defaultProfiles
|
||||
*/
|
||||
class DBObjectDataSynchroTest extends ItopCustomDatamodelTestCase
|
||||
{
|
||||
protected const AUTH_USER = 'DBObjectDataSynchroTest';
|
||||
protected const AUTH_PWD = 'sdf234(-fgh;,dfgDFG';
|
||||
const USE_TRANSACTION = false;
|
||||
|
||||
public function GetDatamodelDeltaAbsPath(): string
|
||||
{
|
||||
return __DIR__.'/add-dbobject-with-reconciliation-key.xml';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$oSearch = DBSearch::FromOQL('SELECT User WHERE login = "'.static::AUTH_USER.'"');
|
||||
$oSet = new DBObjectSet($oSearch);
|
||||
if ($oSet->Count() == 0)
|
||||
{
|
||||
$iProfileId = self::$aURP_Profiles['REST Services User'];
|
||||
$oProfileSearch = DBSearch::FromOQL("SELECT URP_Profiles WHERE id = $iProfileId");
|
||||
$oProfileSearch->AllowAllData();
|
||||
$oProfileSet = new DBObjectSet($oProfileSearch);
|
||||
$oAdminProfile = $oProfileSet->fetch();
|
||||
|
||||
$oUser = MetaModel::NewObject('UserLocal', array(
|
||||
'login' => static::AUTH_USER,
|
||||
'password' => static::AUTH_PWD,
|
||||
'expiration' => UserLocal::EXPIRE_NEVER,
|
||||
));
|
||||
$oProfiles = $oUser->Get('profile_list');
|
||||
$oProfiles->AddItem(MetaModel::NewObject('URP_UserProfile', array(
|
||||
'profileid' => $oAdminProfile->GetKey()
|
||||
)));
|
||||
$oUser->Set('profile_list', $oProfiles);
|
||||
$oUser->DBInsertNoReload();
|
||||
}
|
||||
}
|
||||
|
||||
protected function ExecSynchroImport($aParams, $bSynchroByHttp)
|
||||
{
|
||||
if (!$bSynchroByHttp) {
|
||||
return utils::ExecITopScript('synchro/synchro_import.php', $aParams, static::AUTH_USER, static::AUTH_PWD);
|
||||
}
|
||||
|
||||
$aParams['auth_user'] = static::AUTH_USER;
|
||||
$aParams['auth_pwd'] = static::AUTH_PWD;
|
||||
|
||||
//$aParams['output'] = 'details';
|
||||
$aParams['csvdata'] = file_get_contents($aParams['csvfile']);
|
||||
|
||||
|
||||
$sUrl = \MetaModel::GetConfig()->Get('app_root_url').'/synchro/synchro_import.php?login_mode=form';
|
||||
$sResult = utils::DoPostRequest($sUrl, $aParams, null, $aResponseHeaders, [
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => 0,
|
||||
]);
|
||||
// Read the status code from the last line
|
||||
$aLines = explode("\n", trim(strip_tags($sResult)));
|
||||
//$sLastLine = array_pop($aLines);
|
||||
|
||||
return array(0, $aLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a series of data synchronization through the REST API
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function RunDataSynchroTest($aUserLoginUsecase)
|
||||
{
|
||||
$sDescription = $aUserLoginUsecase['desc'];
|
||||
$sTargetClass = $aUserLoginUsecase['target_class'];
|
||||
$aSourceProperties = $aUserLoginUsecase['source_properties'];
|
||||
$aSourceData = $aUserLoginUsecase['source_data'];
|
||||
$aTargetData = $aUserLoginUsecase['target_data'];
|
||||
$aAttributes =$aUserLoginUsecase['attributes'];
|
||||
$bSynchroByHttp = $aUserLoginUsecase['bSynchroByHttp'];
|
||||
|
||||
$sClass = $sTargetClass;
|
||||
|
||||
$aTargetAttributes = array_shift($aTargetData);
|
||||
$aSourceAttributes = array_shift($aSourceData);
|
||||
|
||||
if (count($aSourceData) + 1 != count($aTargetData))
|
||||
{
|
||||
throw new Exception("Target data must contain exactly ".(count($aSourceData) + 1)." items, found ".count($aTargetData));
|
||||
}
|
||||
|
||||
// Create the data source
|
||||
//
|
||||
$oDataSource = new SynchroDataSource();
|
||||
$oDataSource->Set('name', 'Test data sync '.time());
|
||||
$oDataSource->Set('description', 'unit test - created automatically');
|
||||
$oDataSource->Set('status', 'production');
|
||||
$oDataSource->Set('user_id', 0);
|
||||
$oDataSource->Set('scope_class', $sClass);
|
||||
foreach ($aSourceProperties as $sProperty => $value)
|
||||
{
|
||||
$oDataSource->Set($sProperty, $value);
|
||||
}
|
||||
$iDataSourceId = $oDataSource->DBInsert();
|
||||
|
||||
$oAttributeSet = $oDataSource->Get('attribute_list');
|
||||
while ($oAttribute = $oAttributeSet->Fetch())
|
||||
{
|
||||
if (array_key_exists($oAttribute->Get('attcode'), $aAttributes))
|
||||
{
|
||||
$aAttribInfo = $aAttributes[$oAttribute->Get('attcode')];
|
||||
if (array_key_exists('reconciliation_attcode', $aAttribInfo))
|
||||
{
|
||||
$oAttribute->Set('reconciliation_attcode', $aAttribInfo['reconciliation_attcode']);
|
||||
}
|
||||
$oAttribute->Set('update', $aAttribInfo['do_update']);
|
||||
$oAttribute->Set('reconcile', $aAttribInfo['do_reconcile']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$oAttribute->Set('update', false);
|
||||
$oAttribute->Set('reconcile', false);
|
||||
}
|
||||
$oAttribute->DBUpdate();
|
||||
}
|
||||
|
||||
// Prepare list of prefixes -> make sure objects are unique with regard to the reconciliation scheme
|
||||
$aPrefixes = array(); // attcode => prefix
|
||||
foreach($aSourceAttributes as $iDummy => $sAttCode)
|
||||
{
|
||||
$aPrefixes[$sAttCode] = ''; // init with something
|
||||
}
|
||||
foreach($aAttributes as $sAttCode => $aAttribInfo)
|
||||
{
|
||||
if (isset($aAttribInfo['automatic_prefix']) && $aAttribInfo['automatic_prefix'])
|
||||
{
|
||||
$aPrefixes[$sAttCode] = 'TEST_'.$iDataSourceId.'_';
|
||||
}
|
||||
}
|
||||
|
||||
// List existing objects (to be ignored in the analysis)
|
||||
//
|
||||
$oAllObjects = new DBObjectSet(new DBObjectSearch($sClass));
|
||||
$aExisting = $oAllObjects->ToArray(true);
|
||||
$sExistingIds = implode(', ', array_keys($aExisting));
|
||||
|
||||
// Create the initial object list
|
||||
//
|
||||
$aInitialTarget = $aTargetData[0];
|
||||
foreach($aInitialTarget as $aObjFields)
|
||||
{
|
||||
$oNewTarget = MetaModel::NewObject($sClass);
|
||||
foreach($aTargetAttributes as $iAtt => $sAttCode)
|
||||
{
|
||||
$oNewTarget->Set($sAttCode, $aPrefixes[$sAttCode].$aObjFields[$iAtt]);
|
||||
}
|
||||
$oNewTarget->DBInsertNoReload();
|
||||
}
|
||||
|
||||
//add sleep to make sure expected objects will be found
|
||||
usleep(10000);
|
||||
foreach($aTargetData as $iRow => $aExpectedObjects)
|
||||
{
|
||||
// Check the status (while ignoring existing objects)
|
||||
//
|
||||
if (empty($sExistingIds))
|
||||
{
|
||||
$oObjects = new DBObjectSet(DBObjectSearch::FromOQL("SELECT $sClass"));
|
||||
}
|
||||
else
|
||||
{
|
||||
$oObjects = new DBObjectSet(DBObjectSearch::FromOQL("SELECT $sClass WHERE id NOT IN($sExistingIds)"));
|
||||
}
|
||||
$aFound = $oObjects->ToArray();
|
||||
$aErrors_Unexpected = array();
|
||||
foreach($aFound as $iObj => $oObj)
|
||||
{
|
||||
// Is this object in the expected objects list
|
||||
$bFoundMatch = false;
|
||||
foreach($aExpectedObjects as $iExp => $aValues)
|
||||
{
|
||||
$bDoesMatch = true;
|
||||
foreach($aTargetAttributes as $iCol => $sAttCode)
|
||||
{
|
||||
if ($oObj->Get($sAttCode) != $aPrefixes[$sAttCode].$aValues[$iCol])
|
||||
{
|
||||
$bDoesMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($bDoesMatch)
|
||||
{
|
||||
$bFoundMatch = true;
|
||||
unset($aExpectedObjects[$iExp]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$bFoundMatch)
|
||||
{
|
||||
$aObjDesc = array();
|
||||
foreach($aTargetAttributes as $iCol => $sAttCode)
|
||||
{
|
||||
$aObjDesc[$sAttCode] = $oObj->Get($sAttCode);
|
||||
}
|
||||
$aErrors_Unexpected[get_class($oObj).'::'.$oObj->GetKey()] = $aObjDesc;
|
||||
}
|
||||
}
|
||||
|
||||
// Display the current status
|
||||
//
|
||||
$aErrors = array();
|
||||
if (count($aErrors_Unexpected) > 0) {
|
||||
$aErrors[] = "Unexpected objects found in iTop DB after step $iRow (starting at 0):\n".print_r($aErrors_Unexpected, true);
|
||||
}
|
||||
if (count($aExpectedObjects) > 0) {
|
||||
$aErrors[] = "Expected objects NOT found in iTop DB after step $iRow (starting at 0)\n".print_r($aExpectedObjects, true);
|
||||
}
|
||||
if (count($aErrors) > 0) {
|
||||
$sAdditionalInfo = (isset($sResultsViewable)) ? $sResultsViewable : "";
|
||||
static::fail(implode("\n", $aErrors) . "\n $sAdditionalInfo");
|
||||
} else {
|
||||
static::assertTrue(true);
|
||||
}
|
||||
|
||||
// If not on the final row, run a data exchange sequence
|
||||
//
|
||||
if (array_key_exists($iRow, $aSourceData))
|
||||
{
|
||||
$aToBeLoaded = $aSourceData[$iRow];
|
||||
|
||||
// First line
|
||||
$sCsvData = implode(';', $aSourceAttributes)."\n";
|
||||
|
||||
$sTextQualifier = '"';
|
||||
|
||||
foreach($aToBeLoaded as $aDataRow)
|
||||
{
|
||||
$aFinalData = array();
|
||||
foreach($aDataRow as $iCol => $value)
|
||||
{
|
||||
$sAttCode = $aSourceAttributes[$iCol];
|
||||
$sRawValue = $aPrefixes[$sAttCode].$value;
|
||||
|
||||
$sFrom = array("\r\n", $sTextQualifier);
|
||||
$sTo = array("\n", $sTextQualifier.$sTextQualifier);
|
||||
$sCSVValue = $sTextQualifier.str_replace($sFrom, $sTo, (string)$sRawValue).$sTextQualifier;
|
||||
|
||||
$aFinalData[] = $sCSVValue;
|
||||
}
|
||||
$sCsvData .= implode(';', $aFinalData)."\n";
|
||||
}
|
||||
$sCSVTmpFile = tempnam(sys_get_temp_dir(), "CSV");
|
||||
file_put_contents($sCSVTmpFile, $sCsvData);
|
||||
|
||||
$aParams = array(
|
||||
'csvfile' => $sCSVTmpFile,
|
||||
'data_source_id' => $iDataSourceId,
|
||||
'separator' => ';',
|
||||
'simulate' => 0,
|
||||
'output' => 'details',
|
||||
);
|
||||
list($iRetCode, $aOutputLines) = static::ExecSynchroImport($aParams, $bSynchroByHttp);
|
||||
|
||||
unlink($sCSVTmpFile);
|
||||
|
||||
// Report the load results
|
||||
//
|
||||
if (strlen($sCsvData) > 5000)
|
||||
{
|
||||
$sCsvDataViewable = 'INPUT TOO LONG TO BE DISPLAYED ('.strlen($sCsvData).")\n".substr($sCsvData, 0, 500)."\n... TO BE CONTINUED";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sCsvDataViewable = $sCsvData;
|
||||
}
|
||||
echo "Input Data:\n";
|
||||
echo $sCsvDataViewable;
|
||||
echo "\n";
|
||||
|
||||
$sResultsViewable = '| '.implode("\n| ", $aOutputLines);
|
||||
|
||||
echo "Results:\n";
|
||||
echo $sResultsViewable;
|
||||
echo "\n";
|
||||
|
||||
if ($iRetCode != 0)
|
||||
{
|
||||
static::fail("Execution of synchro_import failing with code '$iRetCode', see error.log for more details");
|
||||
}
|
||||
|
||||
if (stripos($sResultsViewable, 'exception') !== false)
|
||||
{
|
||||
self::fail('Encountered an Exception during the last import/synchro');
|
||||
}
|
||||
|
||||
$aKeys = ["creation", "update", "deletion"];
|
||||
foreach ($aKeys as $sKey){
|
||||
$this->assertStringContainsString("$sKey errors: 0", $sResultsViewable, "step $iRow : below res should contain '$sKey errors: 0': " . $sResultsViewable);
|
||||
}
|
||||
|
||||
//N°3805 : potential javascript returned like
|
||||
/*
|
||||
Please wait...
|
||||
var aListJsFiles = [];
|
||||
$(document).ready(function () {
|
||||
setTimeout(function () {
|
||||
}, 50);
|
||||
});
|
||||
*/
|
||||
$sLastExpectedLine = "#Replica disappeared, no action taken: 0";
|
||||
$aSplittedRes = explode($sLastExpectedLine, $sResultsViewable);
|
||||
$this->assertNotFalse($aSplittedRes);
|
||||
if (count($aSplittedRes)>1){
|
||||
$sPotentialIssuesWithWebApplication = $aSplittedRes[1];
|
||||
$this->assertEquals("", $sPotentialIssuesWithWebApplication, 'when failed it means data synchro result is polluted with some web application stuff like html or js');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $oDataSource;
|
||||
}
|
||||
|
||||
public function testDataSynchroByCli_DBObjectUseCase(){
|
||||
/**
|
||||
* <class id="Event" _delta="define">
|
||||
* <!-- Generated by toolkit/export-class-to-meta.php -->
|
||||
* <parent>DBObject</parent>
|
||||
* <properties>
|
||||
* <category>core/cmdb,view_in_gui</category>
|
||||
* </properties>
|
||||
* <fields>
|
||||
* <field id="message" xsi:type="AttributeText"/>
|
||||
* <field id="date" xsi:type="AttributeDateTime"/>
|
||||
* <field id="userinfo" xsi:type="AttributeString"/>
|
||||
* </fields>
|
||||
* </class>
|
||||
*/
|
||||
|
||||
$DBObjectClass = EventWithTitleAsReconciliationKey::class;
|
||||
$oEventNotification = new EventWithTitleAsReconciliationKey();
|
||||
$this->assertTrue(is_a($oEventNotification, DBObject::class));
|
||||
$this->assertFalse(is_a($oEventNotification, CMDBObject::class));
|
||||
|
||||
foreach (['A', 'C'] as $sKey) {
|
||||
$oEventA = new EventWithTitleAsReconciliationKey();
|
||||
$oEventA->Set('title', "title_$sKey");
|
||||
$oEventA->Set('message', "message_$sKey");
|
||||
$oEventA->Set('userinfo', "userinfo_$sKey");
|
||||
$oEventA->DBWrite();
|
||||
}
|
||||
|
||||
$aDbObjectSyncroUsecase = [
|
||||
'desc' => 'Load EventNotification (DBObject)',
|
||||
'target_class' => $DBObjectClass,
|
||||
'source_properties' => [
|
||||
'full_load_periodicity' => 3600, // should be ignored in this case
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'delete',
|
||||
'delete_policy_update' => '',
|
||||
'delete_policy_retention' => 0,
|
||||
],
|
||||
'source_data' => [
|
||||
['primary_key', 'title', 'message', 'date','userinfo'],
|
||||
[
|
||||
['A', 'title_A', 'message_A', 'userinfo_AAA'],
|
||||
['B', 'title_B', 'message_B', 'userinfo_B'],
|
||||
],
|
||||
],
|
||||
'target_data' => [
|
||||
['title'], //columns
|
||||
[
|
||||
// Initial state
|
||||
],
|
||||
[
|
||||
['title_A'], //expected values
|
||||
['title_B'], //expected values
|
||||
],
|
||||
],
|
||||
'attributes' => [
|
||||
'title' => [
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id (for unit testing)
|
||||
],
|
||||
'message' => [
|
||||
'do_reconcile' => false,
|
||||
'do_update' => false,
|
||||
],
|
||||
'userinfo' => [
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
],
|
||||
],
|
||||
'bSynchroByHttp' => false
|
||||
];
|
||||
|
||||
$this->RunDataSynchroTest($aDbObjectSyncroUsecase);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,18 @@
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Synchro;
|
||||
|
||||
use CMDBObject;
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use DBObject;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use DBSearch;
|
||||
use Event;
|
||||
use EventNotification;
|
||||
use Exception;
|
||||
use MetaModel;
|
||||
use ReflectionClass;
|
||||
use SynchroDataSource;
|
||||
use UserLocal;
|
||||
use utils;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0">
|
||||
<classes>
|
||||
<class id="EventWithTitleAsReconciliationKey" _delta="define">
|
||||
<parent>Event</parent>
|
||||
<properties>
|
||||
<category>grant_by_profile,bizmodel,searchable</category>
|
||||
<abstract>false</abstract>
|
||||
<key_type>autoincrement</key_type>
|
||||
<db_table>remoteapplicationconnection2</db_table>
|
||||
<db_key_field>id</db_key_field>
|
||||
<db_final_class_field/>
|
||||
<naming>
|
||||
<attributes>
|
||||
<attribute id="title"/>
|
||||
</attributes>
|
||||
</naming>
|
||||
<display_template/>
|
||||
<reconciliation>
|
||||
<attributes>
|
||||
<attribute id="title"/>
|
||||
</attributes>
|
||||
</reconciliation>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="title" xsi:type="AttributeString">
|
||||
<sql>title</sql>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
</field>
|
||||
</fields>
|
||||
<methods/>
|
||||
<presentation/>
|
||||
</class>
|
||||
</classes>
|
||||
</itop_design>
|
||||
@@ -227,7 +227,7 @@ try
|
||||
/** @var iRestServiceProvider $oRS */
|
||||
$oRS = $aOpToRestService[$sOperation]['service_provider'];
|
||||
$sProvider = get_class($oRS);
|
||||
|
||||
|
||||
if ($oRS instanceof iRestInputSanitizer) {
|
||||
$sSanitizedJsonInput = $oRS->SanitizeJsonInput($sJsonString);
|
||||
}
|
||||
@@ -299,7 +299,20 @@ if (MetaModel::GetConfig()->Get('log_rest_service'))
|
||||
$oLog->SetTrim('message', $sMessage);
|
||||
$oLog->Set('code', $oResult->code);
|
||||
$oResult->SanitizeContent();
|
||||
$oLog->SetTrim('json_output', json_encode($oResult, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
$iUnescapeSlashAndUnicode = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
|
||||
$sJsonOuputWithPrettyPrinting = json_encode($oResult, $iUnescapeSlashAndUnicode | JSON_PRETTY_PRINT);
|
||||
$sJsonOutputWithoutPrettyPrinting = json_encode($oResult, $iUnescapeSlashAndUnicode);
|
||||
!StringFitsInLogField( $sJsonOuputWithPrettyPrinting) ?
|
||||
$oLog->SetTrim('json_output', $sJsonOutputWithoutPrettyPrinting) : // too long, we don't make it pretty
|
||||
$oLog->SetTrim('json_output', $sJsonOuputWithPrettyPrinting);
|
||||
|
||||
$oLog->DBInsertNoReload();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 3.3.0
|
||||
*/
|
||||
function StringFitsInLogField(string $sLog): bool
|
||||
{
|
||||
return mb_strlen($sLog) <= 16383; // hardcoded value, see N°8260
|
||||
}
|
||||
Reference in New Issue
Block a user