Compare commits

...

30 Commits

Author SHA1 Message Date
odain
e9699846f2 N°8413 - Make data synchro work on DBObject
N°8413 - Make data synchro work on DBObject
2025-06-04 11:51:34 +02:00
bdalsass
3ff3dcba54 N°8308 - Broken URL in sending mail 2025-06-02 17:15:51 +02:00
jf-cbd
657fc912bf N°8198 - ModuleInstallation now extends DBObject + better exception message 2025-06-02 16:09:29 +02:00
Anne-Catherine
5ae5221f6f N°6925 - Enable on any search result edition of OQL (#711) 2025-05-28 10:59:50 +02:00
jf-cbd
b15ca2fbc9 N°8260 - Change format of REST logs when they are close to the SQL field size limit 2025-05-27 10:41:05 +02:00
Stephen Abello
cb382eab4e Fix CI following dict behavior change 2025-05-26 16:23:38 +02:00
Stephen Abello
9723cde24c N°7960 - Add a wrapper to vsprintf to handle argument number disparities (#717) 2025-05-26 15:41:25 +02:00
Stephen Abello
7ae49e2cf4 N°8076 - Fix cancel and close buttons in a modal blocking all buttons for the underlying object 2025-05-26 11:25:08 +02:00
v-dumas
cb13a7a5b4 N°2583 - Audit, User, Query classes can be Import with bulk_modify right 2025-05-23 17:37:47 +02:00
bdalsass
9618e47045 N°8201 - [CVE_Request]_Cross-Site-Script Reflected(XSS Reflected at the name="attr_installed" (Low or Medium) 2025-05-23 10:16:22 +02:00
bdalsass
d84506ea9e Merge remote-tracking branch 'origin/support/2.7' into support/3.2
# Conflicts:
#	pages/UI.php
2025-05-23 10:14:50 +02:00
bdalsass
13239c2751 N°8201 - [CVE_Request]_Cross-Site-Script Reflected(XSS Reflected at the name="attr_installed" (Low or Medium) 2025-05-23 10:06:01 +02:00
bdalsass
8b30e36dd1 N°8168 - Stored XSS in portals lnk 2025-05-23 08:52:18 +02:00
bdalsass
80b290ab88 Merge remote-tracking branch 'origin/support/2.7' into support/3.2
# Conflicts:
#	sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php
2025-05-23 08:49:52 +02:00
bdalsass
81b20ee583 N°8168 - Stored XSS in portals lnk 2025-05-23 08:42:56 +02:00
Stephen Abello
a5545b0084 N°8205 - Security hardening 2025-05-20 09:48:00 +02:00
Stephen Abello
d72e861dfe N°8315 - Security hardening 2025-05-19 14:51:12 +02:00
Stephen Abello
92385273ff N°8315 - Security hardening 2025-05-19 14:45:24 +02:00
bdalsass
61c25f85e7 N°8313 - edit dashboard (fix broken test 3.2) 2025-05-19 13:12:02 +02:00
bdalsass
b1cf2ec137 N°8313 - edit dashboard (fix broken test 3.2) 2025-05-16 14:35:39 +02:00
bdalsass
7549ded51d Merge remote-tracking branch 'origin/support/2.7' into support/3.2
# Conflicts:
#	application/dashboard.class.inc.php
#	setup/setuputils.class.inc.php
#	tests/php-unit-tests/unitary-tests/application/utilsTest.php
2025-05-16 14:19:00 +02:00
bdalsass
38683c20b1 N°8313 - edit dashboard (fix broken test) 2025-05-16 14:05:55 +02:00
bdalsass
81791dd253 N°8313 - edit dashboard 2025-05-16 14:05:55 +02:00
bdalsass
e77e0eec9f N°8355 - render_dashboard 2025-05-16 14:05:55 +02:00
jf-cbd
f5ddbbbe0e Rollback typing parameter 2025-05-13 16:59:43 +02:00
jf-cbd
6811a82e1a Merge remote-tracking branch 'origin/support/2.7' into support/3.2
# Conflicts:
#	datamodels/2.x/itop-backup/status.php
#	setup/setuputils.class.inc.php
2025-05-13 16:40:11 +02:00
jf-cbd
960133c0df N°8379 - fix backup issue 2025-05-13 16:27:59 +02:00
jf-cbd
544c4ae888 N°8379 - fix backup issue 2025-05-13 16:05:39 +02:00
jbostoen
9ee18c2f36 Add helper method to create an ObjectResult from a DBObject. (#706)
Author: Jeffrey Bostoen <support@jeffreybostoen.be>
2025-04-25 14:46:33 +02:00
jf-cbd
43a10e6944 Revert "N°8259 - Problem with GetMaxSize on AttributeText"
This reverts commit 29c75f626b.
2025-04-22 09:37:54 +02:00
35 changed files with 864 additions and 169 deletions

View File

@@ -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);

View File

@@ -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'));

View File

@@ -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

View File

@@ -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
*

View File

@@ -4440,7 +4440,7 @@ class AttributeText extends AttributeString
{
// Is there a way to know the current limitation for mysql?
// See mysql_field_len()
return 16383; // number of characters (that can be 1-4 bytes long), not of bytes
return 65535;
}
public static function RenderWikiHtml($sText, $bWikiOnly = false)

View File

@@ -760,10 +760,10 @@ abstract class DBObject implements iDisplay
*/
public function SetTrim($sAttCode, $sValue)
{
if (!$this->StringFitsInField($sAttCode, $sValue)) {
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$iMaxSize = $oAttDef->GetMaxSize();
$sLength = mb_strlen($sValue);
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$iMaxSize = $oAttDef->GetMaxSize();
$sLength = mb_strlen($sValue);
if ($iMaxSize && ($sLength > $iMaxSize)) {
$sMessage = " -truncated ($sLength chars)";
$sValue = mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
}
@@ -818,24 +818,6 @@ abstract class DBObject implements iDisplay
$oKPI->ComputeStatsForExtension($this, 'AfterDelete');
}
/**
* @param string $sAttCode
* @param string $sValue
*
* @return bool
* @throws \Exception
*
* @Since 3.2.2
*/
public function StringFitsInField(string $sAttCode, string $sValue): bool
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$iMaxSize = $oAttDef->GetMaxSize();
$sLength = mb_strlen($sValue);
return !($iMaxSize && ($sLength > $iMaxSize));
}
/**
* Compute (and optionally start) the StopWatches deadlines
*

View File

@@ -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);

View File

@@ -77,7 +77,7 @@ abstract class ModelReflection
return $sFormatCode.' - '.implode(', ', $aArguments);
}
return vsprintf($sLocalizedFormat, $aArguments);
return utils::VSprintf($sLocalizedFormat, $aArguments);
}
/**

View File

@@ -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;

View File

@@ -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'] = '';
}

View File

@@ -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 = '';

View File

@@ -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();

View File

@@ -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]),

View File

@@ -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;
},
},
});
}

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -25,7 +25,7 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
class ModuleInstallation extends cmdbAbstractObject
class ModuleInstallation extends DBObject
{
public static function Init()
{

View File

@@ -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();

View File

@@ -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();

View File

@@ -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

View File

@@ -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));

View File

@@ -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 {

View File

@@ -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'));
}

View File

@@ -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
}

View File

@@ -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',

View File

@@ -836,8 +836,8 @@ HTML,
'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😃😃😃'],
];
}
@@ -907,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'
]
];
}
}

View File

@@ -2,19 +2,11 @@
namespace Combodo\iTop\Test\UnitTest\Core;
use ArchivedObjectException;
use AttributeDate;
use AttributeDateTime;
use Change;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreCannotSaveObjectException;
use CoreException;
use CoreUnexpectedValue;
use CoreWarning;
use EventRestService;
use MetaModel;
use MySQLException;
use OQLException;
use UserRequest;
class AttributeDefinitionTest extends ItopDataTestCase {
@@ -351,23 +343,4 @@ PHP
return $oAttribute;
}
/**
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws OQLException
* @throws ArchivedObjectException
* @throws CoreCannotSaveObjectException
* @throws CoreWarning
* @throws MySQLException
*/
public function testTrimLogOnAttributeText()
{
// will throw MySQLException if GetMaxSize() of AttributeText is incorrect (should be number of bytes, not of characters)
$oLog = new EventRestService();
$sLongString = json_encode(array_fill(0, 5000, 'é😃 '),
JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
$oLog->SetTrim('json_input', $sLongString);
static::assertNotEquals($oLog->Get('json_input'), $sLongString);
static::assertStringContainsString('truncated', $oLog->Get('json_input'));
}
}

View File

@@ -22,7 +22,6 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Attachment;
use AttributeDateTime;
use Combodo\iTop\Service\Events\EventData;
use EventRestService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreException;
use DateTime;
@@ -1416,22 +1415,4 @@ class DBObjectTest extends ItopDataTestCase
$this->assertEquals("2024-01-15 09:45:00", $oObject->Get('end_date'), 'SetComputedDate +2 weeks on a WorkOrder DateTimeAttribute');
}
public function testStringFitsInField()
{
//🎁 character is 4 bytes long
$sTooLongText = str_repeat('🎁', 17000);
$oLog = new EventRestService();
$this->assertFalse($oLog->StringFitsInField('json_output', $sTooLongText));
$sCorrectLengthText = str_repeat('🎁', 16383);
$this->assertTrue($oLog->StringFitsInField('json_output', $sCorrectLengthText));
$sCorrectLengthString = str_repeat('🎁', 255);
$this->assertTrue($oLog->StringFitsInField('operation', $sCorrectLengthString));
$sTooLongString = str_repeat('🎁', 256);
$this->assertFalse($oLog->StringFitsInField('operation', $sTooLongString));
}
}

View File

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

View File

@@ -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;

View File

@@ -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>

View File

@@ -302,9 +302,17 @@ if (MetaModel::GetConfig()->Get('log_rest_service'))
$iUnescapeSlashAndUnicode = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
$sJsonOuputWithPrettyPrinting = json_encode($oResult, $iUnescapeSlashAndUnicode | JSON_PRETTY_PRINT);
$sJsonOutputWithoutPrettyPrinting = json_encode($oResult, $iUnescapeSlashAndUnicode);
!$oLog->StringFitsInField('json_output', $sJsonOuputWithPrettyPrinting) ?
!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
}