Compare commits

...

36 Commits

Author SHA1 Message Date
Anne-Cath
891a7bcbdd N°7326 - JS error in editing object when a tab with list is deleted 2025-03-17 11:17:25 +01:00
Anne-Cath
60e54e6160 N°7326 - JS error in editing object when a tab with list is deleted 2025-03-17 11:17:20 +01:00
Tommaso Rossi
9a895a7fbd 🐛 N°8115 - Unattended Install: unable to connect to MySQL with TLS (#694)
🐛 Support TLS connections to MySQL in unattended-install process
2025-03-13 15:59:04 +01:00
jf-cbd
f5011bb200 N°8260 - Change format of REST logs when they are close to the SQL field size limit 2025-03-13 15:39:00 +01:00
jf-cbd
29c75f626b N°8259 - Problem with GetMaxSize on AttributeText 2025-03-13 15:29:00 +01:00
jf-cbd
0562563cbb Dump autoloader 2025-03-12 11:51:59 +01:00
jf-cbd
40068bd913 Merge remote-tracking branch 'origin/support/2.7' into support/3.2
# Conflicts:
#	lib/composer/autoload_classmap.php
#	lib/composer/autoload_static.php
2025-03-12 11:47:33 +01:00
jf-cbd
874a5fd2ce Dump autoloader 2025-03-12 11:36:06 +01:00
jf-cbd
1ec139782e Merge remote-tracking branch 'origin/support/3.1' into support/3.2 2025-03-06 16:24:40 +01:00
jf-cbd
056dce4d78 Merge remote-tracking branch 'origin/support/2.7' into support/3.1
# Conflicts:
#	webservices/rest.php
2025-03-06 16:12:15 +01:00
jf-cbd
063bb9680e N°8231 - Better variable fallback 2025-03-06 16:09:51 +01:00
jf-cbd
9b1395db03 Workaround for N°4459 doesn't work on 3.2 2025-03-06 15:23:38 +01:00
jf-cbd
8fd9eb6a84 Merge remote-tracking branch 'origin/support/3.1' into support/3.2 2025-03-06 14:58:24 +01:00
jf-cbd
1142bf327c Fix tests 2025-03-06 14:54:18 +01:00
jf-cbd
278496eaf6 Update tests for 3.1 2025-03-06 14:40:39 +01:00
jf-cbd
77ba0b398f Fix merge conflict 2025-03-06 12:11:20 +01:00
jf-cbd
04ca7bf603 Merge remote-tracking branch 'origin/support/2.7' into support/3.1
# Conflicts:
#	core/restservices.class.inc.php
#	datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
#	tests/php-unit-tests/unitary-tests/core/Delta/delta_test_sanitize_output.xml
#	tests/php-unit-tests/unitary-tests/core/RestServicesSanitizeOutputTest.php
#	tests/php-unit-tests/unitary-tests/core/RestServicesTest.php
#	webservices/rest.php
2025-03-06 12:10:00 +01:00
jf-cbd
8f8ac46f55 N°8215 - When PHP warning are enabled, Global Request doesn't work 2025-03-06 11:59:08 +01:00
denis.flaven@combodo.com
07b904ee1b N°8231 - making rest api logs more readable 2025-03-06 11:59:08 +01:00
Eric Espie
5aee7f4722 N°8242 - When editing a dashboard the parameters do not refresh as expected 2025-03-05 11:02:19 +01:00
Eric Espie
292701b71c N°8242 - When editing a dashboard the parameters do not refresh as expected 2025-03-04 16:51:58 +01:00
jf-cbd
533b57ab99 Merge branch 'support/3.1' into support/3.2 2025-03-03 17:07:24 +01:00
jf-cbd
be8d348b25 N°8231 - Update tests 2025-03-03 16:49:27 +01:00
jf-cbd
6c7a98fe3d Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2025-03-03 16:38:32 +01:00
denis.flaven@combodo.com
ec2203229b N°8231 - making rest api log more readable 2025-03-03 16:23:25 +01:00
jf-cbd
2f699d355a Merge remote-tracking branch 'origin/support/3.1' into support/3.2 2025-03-03 14:37:57 +01:00
jf-cbd
da4457f5b4 Merge remote-tracking branch 'origin/support/2.7' into support/3.1 2025-03-03 14:36:50 +01:00
denis.flaven@combodo.com
97848cea4f N°8231 - making rest api log more readable 2025-03-03 11:38:52 +01:00
jf-cbd
2ccd883c9a 👥 Update contributor list 2025-02-25 11:38:54 +01:00
jf-cbd
eabd68ff77 👥 Update contributor list 2025-02-25 11:38:14 +01:00
jf-cbd
2b493787b1 Update itop-version-history.md with 2.7.12, 3.1.3 and 3.2.1 2025-02-25 09:19:47 +01:00
jf-cbd
d74c850621 N°7870 - Tests: access the symfony service container to instantiate services 2025-02-21 15:28:38 +01:00
jf-cbd
4e575e17a3 Merge remote-tracking branch 'origin/support/3.1' into support/3.2 2025-02-21 13:23:57 +01:00
jf-cbd
94d6eca0c1 N°8215 - When PHP warning are enabled, Global Request doesn't work 2025-02-21 13:20:27 +01:00
jf-cbd
355da8ec0a Merge remote-tracking branch 'origin/support/2.7' into support/3.1
# Conflicts:
#	datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
2025-02-21 13:18:19 +01:00
jf-cbd
5f006c45db N°8215 - When PHP warning are enabled, Global Request doesn't work 2025-02-21 13:16:07 +01:00
22 changed files with 1306 additions and 589 deletions

View File

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

View File

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

View File

@@ -1711,6 +1711,11 @@ interface iRestServiceProvider
public function ExecOperation($sVersion, $sVerb, $aParams);
}
interface iRestInputSanitizer
{
public function SanitizeJsonInput(string $sJsonInput): string;
}
/**
* Minimal REST response structure. Derive this structure to add response data and error codes.
*
@@ -1802,6 +1807,14 @@ class RestResult
* @api
*/
public $message;
/**
* Sanitize the content of this result to hide sensitive information
*/
public function SanitizeContent()
{
// The default implementation does nothing
}
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -90,6 +90,27 @@ class UILinksWidgetDirect
return ConsoleBlockRenderer::RenderBlockTemplateInPage($oPage, $oBlock);
}
/**
* @param WebPage $oPage
* @param $oValue
* @param $aArgs
* @param $sFormPrefix
* @param $oCurrentObj
* @return BlockIndirectLinkSetEditTable
* @throws ArchivedObjectException
* @throws ConfigException
* @throws CoreException
* @throws CoreUnexpectedValue
* @since 3.2
*/
public function GetBlock(WebPage $oPage, $oValue, $aArgs, $sFormPrefix, $oCurrentObj)
{
$oBlock = new BlockDirectLinkSetEditTable($this, $this->sInputid);
$oBlock->InitTable($oPage, $oValue, $sFormPrefix, $oCurrentObj);
return $oBlock;
}
/**
* @param WebPage $oPage
* @param string $sProposedRealClass

View File

@@ -148,6 +148,27 @@ class UILinksWidget
return ConsoleBlockRenderer::RenderBlockTemplateInPage($oPage, $oBlock);
}
/**
* @param WebPage $oPage
* @param $oValue
* @param $aArgs
* @param $sFormPrefix
* @param $oCurrentObj
* @return BlockIndirectLinkSetEditTable
* @throws ArchivedObjectException
* @throws ConfigException
* @throws CoreException
* @throws CoreUnexpectedValue
* @since 3.2
*/
public function GetBlock(WebPage $oPage, $oValue, $aArgs, $sFormPrefix, $oCurrentObj): BlockIndirectLinkSetEditTable
{
$oBlock = new BlockIndirectLinkSetEditTable($this);
$oBlock->InitTable($oPage, $oValue, $aArgs, $sFormPrefix, $oCurrentObj, $this->m_aTableConfig);
return $oBlock;
}
/**
* @param WebPage $oPage
* @param DBObject $oCurrentObj

View File

@@ -419,11 +419,26 @@ class utils
* @since 2.7.7, 3.0.2, 3.1.0 N°4899 - new 'url' filter
* @since 2.7.10 N°6606 use the utils::ENUM_SANITIZATION_* const
* @since 2.7.10 N°6606 new case for ENUM_SANITIZATION_FILTER_PHP_CLASS
* @since 3.2.1-1 N°8242 Allow value to be an array for every filter
*
* @link https://www.php.net/manual/en/filter.filters.sanitize.php PHP sanitization filters
*/
protected static function Sanitize_Internal($value, $sSanitizationFilter)
{
if (is_array($value))
{
$retValue = array();
foreach ($value as $key => $val)
{
$retValue[$key] = self::Sanitize_Internal($val, $sSanitizationFilter); // recursively check arrays
if ($retValue[$key] === false)
{
return false;
}
}
return $retValue;
}
switch ($sSanitizationFilter)
{
case static::ENUM_SANITIZATION_FILTER_INTEGER:
@@ -454,52 +469,36 @@ class utils
case static::ENUM_SANITIZATION_FILTER_PARAMETER:
case static::ENUM_SANITIZATION_FILTER_FIELD_NAME:
case static::ENUM_SANITIZATION_FILTER_TRANSACTION_ID:
if (is_array($value))
switch ($sSanitizationFilter)
{
$retValue = array();
foreach ($value as $key => $val)
{
$retValue[$key] = self::Sanitize_Internal($val, $sSanitizationFilter); // recursively check arrays
if ($retValue[$key] === false)
{
$retValue = false;
break;
}
}
}
else
{
switch ($sSanitizationFilter)
{
case static::ENUM_SANITIZATION_FILTER_TRANSACTION_ID:
// Same as parameter type but keep the dot character
// transaction_id, the dot is mostly for Windows servers when using file storage as the tokens are named *.tmp
// - See N°1835
// - Note: It must be included at the regexp beginning otherwise you'll get an invalid character error
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[\. A-Za-z0-9_=-]*$/')));
break;
case static::ENUM_SANITIZATION_FILTER_TRANSACTION_ID:
// Same as parameter type but keep the dot character
// transaction_id, the dot is mostly for Windows servers when using file storage as the tokens are named *.tmp
// - See N°1835
// - Note: It must be included at the regexp beginning otherwise you'll get an invalid character error
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[\. A-Za-z0-9_=-]*$/')));
break;
case static::ENUM_SANITIZATION_FILTER_ROUTE:
case static::ENUM_SANITIZATION_FILTER_OPERATION:
// - Routes should be of the "controller_namespace_code.controller_method_name" form
// - Operations should be allowed to be namespaced as well even though then don't have dedicated controller yet
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[\.A-Za-z0-9_-]*$/')));
break;
case static::ENUM_SANITIZATION_FILTER_ROUTE:
case static::ENUM_SANITIZATION_FILTER_OPERATION:
// - Routes should be of the "controller_namespace_code.controller_method_name" form
// - Operations should be allowed to be namespaced as well even though then don't have dedicated controller yet
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[\.A-Za-z0-9_-]*$/')));
break;
case static::ENUM_SANITIZATION_FILTER_PARAMETER:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[ A-Za-z0-9_=-]*$/'))); // the '=', '%3D, '%2B', '%2F'
// Characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC)
break;
case static::ENUM_SANITIZATION_FILTER_PARAMETER:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[ A-Za-z0-9_=-]*$/'))); // the '=', '%3D, '%2B', '%2F'
// Characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC)
break;
case static::ENUM_SANITIZATION_FILTER_FIELD_NAME:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name
break;
case static::ENUM_SANITIZATION_FILTER_FIELD_NAME:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name
break;
case static::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[ A-Za-z0-9_=%:+-]*$/')));
break;
case static::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[ A-Za-z0-9_=%:+-]*$/')));
break;
}
}
break;

View File

@@ -142,7 +142,7 @@ abstract class AttributeDefinition
protected $aCSSClasses;
public function GetType()
public function GetType()
{
return Dict::S('Core:'.get_class($this));
}
@@ -4193,7 +4193,7 @@ class AttributeFinalClass extends AttributeString
*/
class AttributePassword extends AttributeString implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
/**
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
@@ -4270,7 +4270,7 @@ class AttributePassword extends AttributeString implements iAttributeNoGroupBy
*/
class AttributeEncryptedString extends AttributeString implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
protected function GetSQLCol($bFullSpec = false)
{
@@ -4440,7 +4440,7 @@ class AttributeText extends AttributeString
{
// Is there a way to know the current limitation for mysql?
// See mysql_field_len()
return 65535;
return 16383; // number of characters (that can be 1-4 bytes long), not of bytes
}
public static function RenderWikiHtml($sText, $bWikiOnly = false)
@@ -10004,7 +10004,7 @@ class AttributeSubItem extends AttributeDefinition
*/
class AttributeOneWayPassword extends AttributeDefinition implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
/**
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)

View File

@@ -760,10 +760,10 @@ abstract class DBObject implements iDisplay
*/
public function SetTrim($sAttCode, $sValue)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$iMaxSize = $oAttDef->GetMaxSize();
$sLength = mb_strlen($sValue);
if ($iMaxSize && ($sLength > $iMaxSize)) {
if (!$this->StringFitsInField($sAttCode, $sValue)) {
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$iMaxSize = $oAttDef->GetMaxSize();
$sLength = mb_strlen($sValue);
$sMessage = " -truncated ($sLength chars)";
$sValue = mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
}
@@ -818,6 +818,24 @@ 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

@@ -44,6 +44,8 @@ class ObjectResult
* @var string
* @api
*/
use SanitizeTrait;
public $message;
/**
* @var mixed|null
@@ -156,6 +158,19 @@ class ObjectResult
{
$this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode, $bExtendedOutput);
}
public function SanitizeContent()
{
foreach($this->fields as $sFieldAttCode => $fieldValue)
{
try {
$oAttDef = MetaModel::GetAttributeDef($this->class, $sFieldAttCode);
} catch (Exception $e) { // for special cases like ID
continue;
}
$this->SanitizeFieldIfSensitive($this->fields, $sFieldAttCode, $fieldValue, $oAttDef);
}
}
}
@@ -221,6 +236,16 @@ class RestResultWithObjects extends RestResult
$sObjKey = get_class($oObject).'::'.$oObject->GetKey();
$this->objects[$sObjKey] = $oObjRes;
}
public function SanitizeContent()
{
parent::SanitizeContent();
foreach($this->objects as $sObjKey => $oObjRes)
{
$oObjRes->SanitizeContent();
}
}
}
/**
@@ -308,9 +333,10 @@ class RestDelete
*
* @package Core
*/
class CoreServices implements iRestServiceProvider
class CoreServices implements iRestServiceProvider, iRestInputSanitizer
{
/**
use SanitizeTrait;
/**
* Enumerate services delivered by this class
*
* @param string $sVersion The version (e.g. 1.0) supported by the services
@@ -528,18 +554,18 @@ class CoreServices implements iRestServiceProvider
}
else
{
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*')
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*')
{
$aFields = $aShowFields[$sClass];
//Id is not a valid attribute to optimize
if (in_array('id', $aFields))
if (in_array('id', $aFields))
{
unset($aFields[array_search('id', $aFields)]);
}
$aAttToLoad = array($oObjectSet->GetClassAlias() => $aFields);
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
}
while ($oObject = $oObjectSet->Fetch())
{
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
@@ -737,6 +763,33 @@ class CoreServices implements iRestServiceProvider
return $oResult;
}
public function SanitizeJsonInput(string $sJsonInput): string
{
$sSanitizedJsonInput = $sJsonInput;
$aJsonData = json_decode($sSanitizedJsonInput, true);
$sOperation = $aJsonData['operation'];
switch ($sOperation) {
case 'core/check_credentials':
if (isset($aJsonData['password'])) {
$aJsonData['password'] = '*****';
}
break;
case 'core/update':
case 'core/create':
default :
$sClass = $aJsonData['class'];
if (isset($aJsonData['fields'])) {
foreach ($aJsonData['fields'] as $sFieldAttCode => $fieldValue) {
$oAttDef = MetaModel::GetAttributeDef($sClass, $sFieldAttCode);
$this->SanitizeFieldIfSensitive($aJsonData['fields'], $sFieldAttCode, $fieldValue, $oAttDef);
}
}
break;
}
return json_encode($aJsonData, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
/**
* Helper for object deletion
*/
@@ -875,3 +928,50 @@ class CoreServices implements iRestServiceProvider
return $iLimit * max(0, $iPage - 1);
}
}
trait SanitizeTrait
{
/**
* Sanitize a field if it is sensitive.
*
* @param array $fields The fields array
* @param string $sFieldAttCode The attribute code
* @param mixed $oAttDef The attribute definition
* @throws Exception
*/
private function SanitizeFieldIfSensitive(array &$fields, string $sFieldAttCode, $fieldValue, $oAttDef): void
{
// for simple attribute
if ($oAttDef instanceof iAttributeNoGroupBy) // iAttributeNoGroupBy is equivalent to sensitive attribute
{
$fields[$sFieldAttCode] = '*****';
return;
}
// for 1-n / n-n relation
if ($oAttDef instanceof AttributeLinkedSet) {
foreach ($fieldValue as $i => $aLnkValues) {
foreach ($aLnkValues as $sLnkAttCode => $sLnkValue) {
$oLnkAttDef = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sLnkAttCode);
if ($oLnkAttDef instanceof iAttributeNoGroupBy) { // 1-n relation
$fields[$sFieldAttCode][$i][$sLnkAttCode] = '*****';
}
elseif ($oAttDef instanceof AttributeLinkedSetIndirect && $oLnkAttDef instanceof AttributeExternalField) { // for n-n relation
$oExtKeyAttDef = MetaModel::GetAttributeDef($oLnkAttDef->GetTargetClass(), $oLnkAttDef->GetExtAttCode());
if ($oExtKeyAttDef instanceof iAttributeNoGroupBy) {
$fields[$sFieldAttCode][$i][$sLnkAttCode] = '*****';
}
}
}
}
return;
}
// for external attribute
if ($oAttDef instanceof AttributeExternalField) {
$oExtKeyAttDef = MetaModel::GetAttributeDef($oAttDef->GetTargetClass(), $oAttDef->GetExtAttCode());
if ($oExtKeyAttDef instanceof iAttributeNoGroupBy) {
$fields[$sFieldAttCode] = '*****';
}
}
}
}

View File

@@ -1399,55 +1399,56 @@ class ObjectFormManager extends FormManager
$this->aFieldsAtts = array();
$this->aExtraData = array();
$aFieldsDMOnlyAttCodes = array();
switch ($this->aFormProperties['type']) {
case 'custom_list':
case 'static':
foreach ($this->aFormProperties['fields'] as $sAttCode => $aOptions) {
// When in a transition and no flags are specified for the field, we will retrieve its flags from DM later
if ($this->IsTransitionForm() && empty($aOptions)) {
$aFieldsDMOnlyAttCodes[] = $sAttCode;
continue;
}
if (array_key_exists('type', $this->aFormProperties)) {
switch ($this->aFormProperties['type']) {
case 'custom_list':
case 'static':
foreach ($this->aFormProperties['fields'] as $sAttCode => $aOptions) {
// When in a transition and no flags are specified for the field, we will retrieve its flags from DM later
if ($this->IsTransitionForm() && empty($aOptions)) {
$aFieldsDMOnlyAttCodes[] = $sAttCode;
continue;
}
// Otherwise we proceed as usual
$iFieldFlags = OPT_ATT_NORMAL;
// Checking if field should be slave
if (isset($aOptions['slave']) && ($aOptions['slave'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_SLAVE;
// Otherwise we proceed as usual
$iFieldFlags = OPT_ATT_NORMAL;
// Checking if field should be slave
if (isset($aOptions['slave']) && ($aOptions['slave'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_SLAVE;
}
// Checking if field should be must_change
if (isset($aOptions['must_change']) && ($aOptions['must_change'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MUSTCHANGE;
}
// Checking if field should be must prompt
if (isset($aOptions['must_prompt']) && ($aOptions['must_prompt'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MUSTPROMPT;
}
// Checking if field should be hidden
if (isset($aOptions['hidden']) && ($aOptions['hidden'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_HIDDEN;
}
// Checking if field should be readonly
if (isset($aOptions['read_only']) && ($aOptions['read_only'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_READONLY;
}
// Checking if field should be mandatory
if (isset($aOptions['mandatory']) && ($aOptions['mandatory'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MANDATORY;
}
// Finally, adding the attribute and its flags
$this->aFieldsAtts[$sAttCode] = $iFieldFlags;
}
// Checking if field should be must_change
if (isset($aOptions['must_change']) && ($aOptions['must_change'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MUSTCHANGE;
}
// Checking if field should be must prompt
if (isset($aOptions['must_prompt']) && ($aOptions['must_prompt'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MUSTPROMPT;
}
// Checking if field should be hidden
if (isset($aOptions['hidden']) && ($aOptions['hidden'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_HIDDEN;
}
// Checking if field should be readonly
if (isset($aOptions['read_only']) && ($aOptions['read_only'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_READONLY;
}
// Checking if field should be mandatory
if (isset($aOptions['mandatory']) && ($aOptions['mandatory'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MANDATORY;
}
// Finally, adding the attribute and its flags
$this->aFieldsAtts[$sAttCode] = $iFieldFlags;
}
break;
break;
case 'zlist':
foreach (MetaModel::FlattenZList(MetaModel::GetZListItems($sObjectClass, $this->aFormProperties['fields'])) as $sAttCode) {
$this->aFieldsAtts[$sAttCode] = OPT_ATT_NORMAL;
}
break;
case 'zlist':
foreach (MetaModel::FlattenZList(MetaModel::GetZListItems($sObjectClass, $this->aFormProperties['fields'])) as $sAttCode) {
$this->aFieldsAtts[$sAttCode] = OPT_ATT_NORMAL;
}
break;
}
}
if ($this->aFormProperties['layout'] !== null) {
if (isset($this->aFormProperties['layout'])) {
$oXPath = new DOMXPath($this->oHtmlDocument);
/** @var \DOMElement $oFieldNode */
foreach ($oXPath->query('//div[contains(@class, "form_field")][@data-field-id]') as $oFieldNode) {
@@ -1510,7 +1511,7 @@ class ObjectFormManager extends FormManager
// Also, retrieving mandatory attributes from metamodel to be able to complete the form with them if necessary
//
// Note: When in a transition, we don't do this for fields that should be set from DM
if ($this->aFormProperties['type'] !== 'static') {
if (array_key_exists('type', $this->aFormProperties) && $this->aFormProperties['type'] !== 'static') {
if ($this->IsTransitionForm()) {
$aDatamodelAttCodes = $this->oObject->GetTransitionAttributes($this->aFormProperties['stimulus_code']);
}
@@ -1616,7 +1617,7 @@ class ObjectFormManager extends FormManager
}
$this->oHtmlDocument = new DOMDocument();
if ($this->aFormProperties['layout'] !== null) {
if (isset($this->aFormProperties['layout'])) {
// Checking if we need to render the template from twig to html in order to parse the fields
if ($this->aFormProperties['layout']['type'] === 'twig') {
if ($this->oFormHandlerHelper !== null) {

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ namespace Combodo\iTop\Test\UnitTest;
use CMDBSource;
use DeprecatedCallsLog;
use MySQLTransactionNotClosedException;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use ReflectionMethod;
use SetupUtils;
use Symfony\Component\HttpKernel\KernelInterface;
@@ -23,7 +23,7 @@ use const DEBUG_BACKTRACE_IGNORE_ARGS;
*
* @since 3.0.4 3.1.1 3.2.0 N°6658 move some setUp/tearDown code to the corresponding methods *BeforeClass to speed up tests process time.
*/
abstract class ItopTestCase extends TestCase
abstract class ItopTestCase extends KernelTestCase
{
public const TEST_LOG_DIR = 'test';

View File

@@ -834,6 +834,7 @@ 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<6F><6F>ls.co<63>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],

View File

@@ -2,11 +2,19 @@
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 {
@@ -343,4 +351,23 @@ 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,6 +22,7 @@ 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;
@@ -1415,4 +1416,22 @@ 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,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.7">
<classes>
<class id="TestServer" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>test_server</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="contact_list" xsi:type="AttributeLinkedSetIndirect">
<linked_class>lnkContactTestToServer</linked_class>
<ext_key_to_me>test_server_id</ext_key_to_me>
<ext_key_to_remote>contact_test_id</ext_key_to_remote>
<is_null_allowed>true</is_null_allowed>
</field>
<field id="password_list" xsi:type="AttributeLinkedSet">
<linked_class>PasswordTest</linked_class>
<ext_key_to_me>server_test_id</ext_key_to_me>
<is_null_allowed>true</is_null_allowed>
</field>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
</class>
<class id="ContactTest" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>contact_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="password" xsi:type="AttributeEncryptedString">
<sql>password</sql>
</field>
<field id="server_test_list" xsi:type="AttributeLinkedSetIndirect">
<linked_class>lnkContactTestToServer</linked_class>
<ext_key_to_me>contact_test_id</ext_key_to_me>
<ext_key_to_remote>test_server_id</ext_key_to_remote>
<is_null_allowed>true</is_null_allowed>
</field>
</fields>
</class>
<class id="lnkContactTestToServer" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>lnk_contact_server_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="contact_test_password" xsi:type="AttributeExternalField" _delta="define">
<extkey_attcode>contact_test_id</extkey_attcode>
<target_attcode>password</target_attcode>
</field>
<field id="test_server_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>TestServer</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<sql>test_server</sql>
<is_null_allowed>false</is_null_allowed>
</field>
<field id="contact_test_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>ContactTest</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<sql>contact_test</sql>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
</class>
<class id="PasswordTest" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>password_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="server_test_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>TestServer</target_class>
<sql>server_test_id</sql>
<on_target_delete>DEL_MANUAL</on_target_delete>
</field>
<field id="password" xsi:type="AttributeEncryptedString" _delta="define">
<sql>password</sql>
</field>
</fields>
</class>
</classes>
</itop_design>

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Core;
use ArchivedObjectException;
use AttributeEncryptedString;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use CoreException;
use CoreUnexpectedValue;
use Exception;
use MetaModel;
use ormLinkSet;
use PasswordTest;
use RestResultWithObjects;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class RestServicesSanitizeOutputTest extends ItopCustomDatamodelTestCase
{
private const SIMPLE_PASSWORD = '123456';
/**
* @throws Exception
*/
protected function setUp(): void
{
parent::setUp();
}
/**
* @return void
* @throws CoreException
*/
public function testSanitizeAttributeOnRequestedObject()
{
$oContactTest = MetaModel::NewObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD
]
);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oContactTest, ['ContactTest' => ['password']]);
$oRestResultWithObject->SanitizeContent();
static::assertJsonStringEqualsJsonString(
'{"objects":{"ContactTest::-1":{"code":0,"message":"ok","class":"ContactTest","key":-1,"fields":{"password":"*****"}}},"code":0,"message":null}',
json_encode($oRestResultWithObject));
}
/**
* @return void
* @throws Exception
*/
public function testSanitizeAttributeExternalFieldOnLink()
{
$oContactTest = $this->createObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD
]
);
$oTestServer = $this->createObject('TestServer', [
'name' => 'test_server',
]);
// create lnkContactTestToServer
$oLnkContactTestToServer = $this->createObject('lnkContactTestToServer', [
'contact_test_id' => $oContactTest->GetKey(),
'test_server_id' => $oTestServer->GetKey()
]);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oLnkContactTestToServer,
['lnkContactTestToServer' => ['contact_test_password']]);
$oRestResultWithObject->SanitizeContent();
static::assertStringContainsString(
'*****',
json_encode($oRestResultWithObject));
static::assertStringNotContainsString(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @throws Exception
*/
public function testSanitizeAttributeOnObjectRelatedThroughNNRelation()
{
$oContactTest = $this->createObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD
]);
$oTestServer = $this->createObject('TestServer', [
'name' => 'test_server',
]);
// create lnkContactTestToServer
$this->createObject('lnkContactTestToServer', [
'contact_test_id' => $oContactTest->GetKey(),
'test_server_id' => $oTestServer->GetKey()
]);
$oTestServer->Reload();
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oTestServer,
['TestServer' => ['contact_list']]);
$oRestResultWithObject->SanitizeContent();
static::assertStringContainsString(
'*****',
json_encode($oRestResultWithObject));
static::assertStringNotContainsString(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws ArchivedObjectException
* @throws Exception
*/
public function testSanitizeOnObjectRelatedThrough1NRelation()
{
$oTestServer = $this->createObject('TestServer', [
'name' => 'my_server',
]);
$oPassword = new PasswordTest();
$oPassword->Set('password', self::SIMPLE_PASSWORD);
$oPassword->Set('server_test_id', $oTestServer->GetKey());
/** @var ormLinkSet $oContactList */
$oContactList = $oTestServer->Get('password_list');
$oContactList->AddItem($oPassword);
$oTestServer->Set('password_list', $oContactList);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oTestServer, ['TestServer' => ['id', 'password_list']]);
$oRestResultWithObject->SanitizeContent();
static::assertStringContainsString(
'*****',
json_encode($oRestResultWithObject));
static::assertStringNotContainsString(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @return string Abs path to the XML delta to use for the tests of that class
*/
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__.'/Delta/delta_test_sanitize_output.xml';
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreException;
use CoreServices;
use CoreUnexpectedValue;
use RestResultWithObjects;
use UserLocal;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class RestServicesTest extends ItopDataTestCase
{
/**
* @return void
* @dataProvider providerTestSanitizeJsonInput
*/
public function testSanitizeJsonInput($sJsonData, $sExpectedJsonDataSanitized)
{
$oRS = new CoreServices();
$sOutputJson = $oRS->SanitizeJsonInput($sJsonData);
static::assertJsonStringEqualsJsonString($sExpectedJsonDataSanitized, $sOutputJson);
}
/**
* @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"}}',
'{
"operation": "core/update",
"comment": "Update user",
"class": "UserLocal",
"key": {
"id": 1
},
"output_fields": "first_name, password",
"fields": {
"password": "*****"
}
}'
],
'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",
"fields": {
"first_name": "John",
"last_name": "Doe",
"email": "jd@example/com",
"password": "*****"
}
}'
],
];
}
/**
* @param $sOperation
* @param $aJsonData
* @param $sExpectedJsonDataSanitized
* @return void
* @throws CoreException
* @throws CoreUnexpectedValue
* @dataProvider providerTestSanitizeJsonOutput
*/
public function testSanitizeJsonOutput($sOperation, $aJsonData, $sExpectedJsonDataSanitized)
{
$oUser = new UserLocal();
$oUser->Set('password', '123456');
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oUser, ['UserLocal' => ['login', 'password']]);
$oRestResultWithObject->SanitizeContent();
static::assertJsonStringEqualsJsonString($sExpectedJsonDataSanitized, json_encode($oRestResultWithObject));
}
/**
* @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}'
],
];
}
}

View File

@@ -227,7 +227,11 @@ try
/** @var iRestServiceProvider $oRS */
$oRS = $aOpToRestService[$sOperation]['service_provider'];
$sProvider = get_class($oRS);
if ($oRS instanceof iRestInputSanitizer) {
$sSanitizedJsonInput = $oRS->SanitizeJsonInput($sJsonString);
}
CMDBObject::SetTrackOrigin('webservice-rest');
$oResult = $oRS->ExecOperation($sVersion, $sOperation, $aJsonData);
}
@@ -252,6 +256,7 @@ catch(Exception $e)
//
$sResponse = json_encode($oResult);
if ($sResponse === false)
{
$oJsonIssue = new RestResult();
@@ -283,7 +288,7 @@ if (MetaModel::GetConfig()->Get('log_rest_service'))
$oLog->SetTrim('userinfo', UserRights::GetUser());
$oLog->Set('version', $sVersion);
$oLog->Set('operation', $sOperation);
$oLog->SetTrim('json_input', $sJsonString);
$oLog->SetTrim('json_input', $sSanitizedJsonInput ?? $sJsonString);
$oLog->Set('provider', $sProvider);
$sMessage = $oResult->message;
@@ -293,7 +298,13 @@ if (MetaModel::GetConfig()->Get('log_rest_service'))
}
$oLog->SetTrim('message', $sMessage);
$oLog->Set('code', $oResult->code);
$oLog->SetTrim('json_output', $sResponse);
$oResult->SanitizeContent();
$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) ?
$oLog->SetTrim('json_output', $sJsonOutputWithoutPrettyPrinting) : // too long, we don't make it pretty
$oLog->SetTrim('json_output', $sJsonOuputWithPrettyPrinting);
$oLog->DBInsertNoReload();
}