diff --git a/.jenkins/configuration/default-environment/unattended_install/unattended_install.php b/.jenkins/configuration/default-environment/unattended_install/unattended_install.php index a69309364..804d206db 100644 --- a/.jenkins/configuration/default-environment/unattended_install/unattended_install.php +++ b/.jenkins/configuration/default-environment/unattended_install/unattended_install.php @@ -101,7 +101,7 @@ if ($sMode == 'install') $oMysqli = new mysqli($sDBServer, $sDBUser, $sDBPwd); if ($oMysqli->connect_errno) { - die("Cannot connect to the MySQL server (".$mysqli->connect_errno . ") ".$mysqli->connect_error."\nExiting"); + die("Cannot connect to the MySQL server (".$oMysqli->connect_errno . ") ".$oMysqli->connect_error."\nExiting"); } else { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc1d65571..c8d1a454b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,57 +37,68 @@ If you want to use another license, you may [create an extension][wiki new ext]. [wiki new ext]: https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Astart#by_writing_your_own_extension -## 🔀 Branch model +## 🔀 iTop branch model -TL;DR: -> **create a fork from iTop main repository, -> create a branch based on the develop branch** +When we first start with Git, we were using the [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branch model. As + there was some confusions about branches to use for current developed release and previous maintained release, and also because we were + using just a very few of the GitFlow commands, we decided to add just a little modification to this branch model : since april 2020 + we don't have anymore a `master` branch. -We are using the [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branch model. That means we have in our repo those -main branches: +Here are the branches we use and their meaning : -- develop: ongoing development version -- release/\*: if present, that means we are working on a beta version -- master: previous stable version -- support/\*: maintenance branches for older versions +- `develop`: ongoing development version +- `release/*`: if present, that means we are working on a alpha/beta/rc version for shipping +- `support/*`: maintenance branches for older versions -For example, if no beta version is currently ongoing we could have: +For example, if no version is currently prepared for shipping we could have: -- develop containing future 2.8.0 version -- master containing 2.7.x maintenance version -- support/2.6 containing 2.6.x maintenance version -- support/2.5 containing 2.5.x maintenance version +- `develop` containing future 2.8.0 version +- `support/2.7`: 2.7.x maintenance version +- `support/2.6`: 2.6.x maintenance version +- `support/2.5`: 2.5.x maintenance version In this example, when 2.8.0-beta is shipped that will become: -- develop: future 2.9.0 version -- release/2.8: 2.8.0-beta -- master: 2.7.x maintenance version -- support/2.6 containing 2.6.x maintenance version -- support/2.5 containing 2.5.x maintenance version +- `develop`: future 2.9.0 version +- `release/2.8`: 2.8.0-beta +- `support/2.7`: 2.7.x maintenance version +- `support/2.6`: 2.6.x maintenance version +- `support/2.5`: 2.5.x maintenance version And when 2.8.0 final will be out: -- develop: future 2.9.0 version -- master: 2.8.x maintenance version -- support/2.7 : 2.7.x maintenance version -- support/2.6 containing 2.6.x maintenance version -- support/2.5 containing 2.5.x maintenance version +- `develop`: future 2.9.0 version +- `support/2.8`: 2.8.x maintenance version (will host developments for 2.8.1) +- `support/2.7`: 2.7.x maintenance version +- `support/2.6`: 2.6.x maintenance version +- `support/2.5`: 2.5.x maintenance version -Most of the time you should based your developments on the develop branch. -That may be different if you want to fix a bug, please use develop anyway and ask in your PR if rebase is possible. +Also note that we have a "micro-version" concept : each of those versions have a very small amount of modifications. They are made from + `support/*` branches as well. For example 2.6.2-1 and 2.6.2-2 were made from the `support/2.6.2` branch. ## Coding -### 🎨 PHP styleguide - -Please follow [our guidelines](https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Acoding_standards). - ### 🌐 Translations A [dedicated page](https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Atranslation) is available in the official wiki. +### Where to start ? + +1. Create a fork from our repository (see [Working with forks - GitHub Help](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/working-with-forks)) +2. Create a branch in this fork, based on the develop branch +3. Code ! + +Do create a dedicated branch for each modification you want to propose : if you don't it will be very hard to merge back your work ! + +Most of the time you should based your developments on the develop branch. +That may be different if you want to fix a bug, please use develop anyway and ask in your PR if rebase is possible. + + +### 🎨 PHP styleguide + +Please follow [our guidelines](https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Acoding_standards). + ### ✅ Tests Please create tests that covers as much as possible the code you're submitting. diff --git a/README.md b/README.md index c095f25c8..06303e70f 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ We would like to give a special thank you to the people from the community who c - Schirrmann, Pascal - Seki, Shoji - Shilov, Vladimir + - Tahri, Ahmed R. (Ousret) - Tulio, Marco - Turrubiates, Miguel diff --git a/application/datatable.class.inc.php b/application/datatable.class.inc.php index 009937e88..adbeaecae 100644 --- a/application/datatable.class.inc.php +++ b/application/datatable.class.inc.php @@ -20,6 +20,8 @@ class DataTable { protected $iListId; // Unique ID inside the web page + /** @var string */ + private $sDatatableContainerId; protected $sTableId; // identifier for saving the settings (combined with the class aliases) protected $oSet; // The set of objects to display protected $aClassAliases; // The aliases (alias => class) inside the set @@ -29,10 +31,10 @@ class DataTable protected $bShowObsoleteData; /** - * @param $iListId mixed Unique ID for this div/table in the page - * @param $oSet DBObjectSet The set of data to display - * @param $aClassAliases array The list of classes/aliases to be displayed in this set $sAlias => $sClassName - * @param $sTableId mixed A string (or null) identifying this table in order to persist its settings + * @param string $iListId Unique ID for this div/table in the page + * @param DBObjectSet $oSet The set of data to display + * @param array$aClassAliases The list of classes/aliases to be displayed in this set $sAlias => $sClassName + * @param string $sTableId A string (or null) identifying this table in order to persist its settings * * @throws \CoreException * @throws \MissingQueryArgument @@ -42,6 +44,7 @@ class DataTable public function __construct($iListId, $oSet, $aClassAliases, $sTableId = null) { $this->iListId = utils::GetSafeId($iListId); // Make a "safe" ID for jQuery + $this->sDatatableContainerId = 'datatable_'.utils::GetSafeId($iListId); $this->oSet = $oSet; $this->aClassAliases = $aClassAliases; $this->sTableId = $sTableId; @@ -165,7 +168,7 @@ class DataTable $sDataTable = $this->GetHTMLTable($oPage, $aColumns, $sSelectMode, $iPageSize, $bViewLink, $aExtraParams); $sConfigDlg = $this->GetTableConfigDlg($oPage, $aColumns, $bViewLink, $iDefaultPageSize); - $sHtml = "iListId}\" class=\"datatable\">"; + $sHtml = "
sDatatableContainerId}\" class=\"datatable\">"; $sHtml .= "
"; $sHtml .= ""; $sHtml .= ""; @@ -201,7 +204,7 @@ class DataTable $aOptions['oDefaultSettings'] = $this->GetAsHash($this->oDefaultSettings); } $sJSOptions = json_encode($aOptions); - $oPage->add_ready_script("$('#datatable_{$this->iListId}').datatable($sJSOptions);"); + $oPage->add_ready_script("$('#{$this->sDatatableContainerId}').datatable($sJSOptions);"); return $sHtml; } @@ -418,15 +421,15 @@ EOF; $sHtml .= "iListId}\" type=\"radio\" name=\"scope\" $sGenericChecked value=\"defaults\">

'; $sHtml .= ""; $sHtml .= '
$sObjectsCount$sToolkitMenu $sActionsMenu
'; - $sHtml .= ''; + $sHtml .= ''; $sHtml .= ''; - $sHtml .= ''; + $sHtml .= ''; $sHtml .= '
'; $sHtml .= ""; $sHtml .= ""; $sDlgTitle = addslashes(Dict::S('UI:ListConfigurationTitle')); - $oPage->add_ready_script("$('#datatable_dlg_{$this->iListId}').dialog({autoOpen: false, title: '$sDlgTitle', width: 500, close: function() { $('#datatable_{$this->iListId}').datatable('onDlgCancel'); } });"); + $oPage->add_ready_script("$('#datatable_dlg_{$this->iListId}').dialog({autoOpen: false, title: '$sDlgTitle', width: 500, close: function() { $('#{$this->sDatatableContainerId}').datatable('onDlgCancel'); } });"); return $sHtml; } @@ -745,12 +748,25 @@ EOF; } $sOQL = addslashes($this->oSet->GetFilter()->serialize()); $oPage->add_ready_script( -<<iListId} table.listResults'); +<<sDatatableContainerId} table.listResults'); oTable.tableHover(); -oTable.tablesorter( { $sHeaders widgets: ['myZebra', 'truncatedList']} ).tablesorterPager({container: $('#pager{$this->iListId}'), totalRows:$iCount, size: $iPageSize, filter: '$sOQL', extra_params: '$sExtraParams', select_mode: '$sSelectModeJS', displayKey: $sDisplayKey, table_id: '{$this->iListId}', columns: $sJSColumns, class_aliases: $sJSClassAliases $sCssCount}); -EOF - ); +oTable + .tablesorter({ $sHeaders widgets: ['myZebra', 'truncatedList']}) + .tablesorterPager({ + container: $('#pager{$this->iListId}'), + totalRows:$iCount, + size: $iPageSize, + filter: '$sOQL', + extra_params: '$sExtraParams', + select_mode: '$sSelectModeJS', + displayKey: $sDisplayKey, + table_id: '{$this->sDatatableContainerId}', + columns: $sJSColumns, + class_aliases: $sJSClassAliases $sCssCount + }); +JS + ); if ($sFakeSortList != '') { $oPage->add_ready_script("oTable.trigger(\"fakesorton\", [$sFakeSortList]);"); diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php index 5ca0e3a81..6426ddf51 100644 --- a/application/ui.extkeywidget.class.inc.php +++ b/application/ui.extkeywidget.class.inc.php @@ -636,14 +636,22 @@ HTML $oSet->SetShowObsoleteData(utils::ShowObsoleteData()); $sHKAttCode = MetaModel::IsHierarchicalClass($this->sTargetClass); - $this->DumpTree($oPage, $oSet, $sHKAttCode, $currValue); + $bHasChildLeafs = $this->DumpTree($oPage, $oSet, $sHKAttCode, $currValue); $oPage->add('
'); $oPage->add(''); + + if ($bHasChildLeafs) + { + $oPage->add('
'.Dict::S("UI:Treeview:CollapseAll").' | '.Dict::S("UI:Treeview:ExpandAll").'
'); + } + $oPage->add("iId}\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"$('#dlg_tree_{$this->iId}').dialog('close');\">  "); $oPage->add("iId}\" value=\"".Dict::S('UI:Button:Ok')."\" onClick=\"oACWidget_{$this->iId}.DoHKOk();\">"); $oPage->add(''); + + $oPage->add_ready_script("\$('#tree_$this->iId ul').treeview({ control: '#treecontrolid', persist: 'false'});\n"); $oPage->add_ready_script("\$('#tree_$this->iId ul').treeview();\n"); $oPage->add_ready_script("\$('#dlg_tree_$this->iId').dialog({ width: 'auto', height: 'auto', autoOpen: true, modal: true, title: '$sDialogTitle', resizeStop: oACWidget_{$this->iId}.OnHKResize, close: oACWidget_{$this->iId}.OnHKClose });\n"); } @@ -673,6 +681,18 @@ HTML } } + /** + * @param WebPage $oP + * @param \DBObjectSet $oSet + * @param string $sParentAttCode + * @param string $currValue + * + * @return bool true if there are at least one child leaf, false if only roots nodes are present + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + */ function DumpTree($oP, $oSet, $sParentAttCode, $currValue) { $aTree = array(); @@ -701,6 +721,9 @@ HTML { $this->DumpNodes($oP, $iRootId, $aTree, $aNodes, $currValue); } + + $bHasOnlyRootNodes = (count($aTree) === 1); + return !$bHasOnlyRootNodes; } function DumpNodes($oP, $iRootId, $aTree, $aNodes, $currValue) @@ -728,7 +751,7 @@ HTML $sSelect = ' '; } } - $oP->add('
  • '.$sSelect.''); + $oP->add('
  • '.$sSelect.''); $this->DumpNodes($oP, $id, $aTree, $aNodes, $currValue); $oP->add("
  • \n"); } diff --git a/core/dbobject.class.php b/core/dbobject.class.php index fdd1125e9..3e5a5904d 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -2914,9 +2914,9 @@ abstract class DBObject implements iDisplay } } - /* - * Persist an object to the DB, for the first time - * + /** + * Persist an object to the DB, for the first time + * * @api * @see DBWrite * diff --git a/core/email.class.inc.php b/core/email.class.inc.php index 7d8659bfe..732ca37e8 100644 --- a/core/email.class.inc.php +++ b/core/email.class.inc.php @@ -304,8 +304,12 @@ class EMail $oHeaders = $this->m_oMessage->getHeaders(); switch(strtolower($sKey)) { + case 'return-path': + $this->m_oMessage->setReturnPath($sValue); + break; + default: - $oHeaders->addTextHeader($sKey, $sValue); + $oHeaders->addTextHeader($sKey, $sValue); } } } diff --git a/css/light-grey.scss b/css/light-grey.scss index 1f0676998..ebfd7e7fd 100644 --- a/css/light-grey.scss +++ b/css/light-grey.scss @@ -3891,3 +3891,16 @@ input:checked + .slider:before { } } } + + + +.ui-dialog .ui-dialog-content .treecontrol { + padding-bottom:0.3em; + padding-left: 0.2em; + margin-top: -0.3em; + padding-top: 0; + +} +.ui-dialog .ui-dialog-content .treecontrol a { + font-size: small; +} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/composer.json b/datamodels/2.x/itop-portal-base/portal/composer.json index 92a2d0e13..0042dcb7f 100644 --- a/datamodels/2.x/itop-portal-base/portal/composer.json +++ b/datamodels/2.x/itop-portal-base/portal/composer.json @@ -1,5 +1,8 @@ { "license": "AGPLv3", + "config": { + "classmap-authoritative": true + }, "autoload": { "psr-4": { "Combodo\\iTop\\Portal\\": "src/" diff --git a/datamodels/2.x/version.xml b/datamodels/2.x/version.xml index bb7a3e241..57807b22e 100755 --- a/datamodels/2.x/version.xml +++ b/datamodels/2.x/version.xml @@ -1,4 +1,4 @@ - 2.7.0 + 2.8.0-dev diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index 59b89d1f5..81e4d5f21 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -423,6 +423,8 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Button:More' => 'More', 'UI:Button:Less' => 'Less', 'UI:Button:Wait' => 'Please wait while updating fields', + 'UI:Treeview:CollapseAll' => 'Collapse All', + 'UI:Treeview:ExpandAll' => 'Expand All', 'UI:SearchToggle' => 'Search', 'UI:ClickToCreateNew' => 'Create a new %1$s', diff --git a/dictionaries/nl.dictionary.itop.ui.php b/dictionaries/nl.dictionary.itop.ui.php index 171248845..d954830ad 100644 --- a/dictionaries/nl.dictionary.itop.ui.php +++ b/dictionaries/nl.dictionary.itop.ui.php @@ -845,7 +845,7 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array( 'UI:ModificationTitle_Class_Object' => 'Aanpassen van %1$s: %2$s', 'UI:ClonePageTitle_Object_Class' => 'ITOP_APPLICATION_SHORT - Kloon %1$s - %2$s aanpassing', 'UI:CloneTitle_Class_Object' => 'Klonen van %1$s: %2$s', - 'UI:CreationPageTitle_Class' => 'ITOP_APPLICATION_SHORT - Nieuwe %1$s aangemaakt', + 'UI:CreationPageTitle_Class' => 'ITOP_APPLICATION_SHORT - %1$s aanmaken', 'UI:CreationTitle_Class' => '%1$s aanmaken', 'UI:SelectTheTypeOf_Class_ToCreate' => 'Selecteer het type %1$s dat moet worden aangemaakt:', 'UI:Class_Object_NotUpdated' => 'Geen verandering waargenomen, %1$s (%2$s) is niet aangepast.', diff --git a/setup/modelfactory.class.inc.php b/setup/modelfactory.class.inc.php index 84fc059a4..14880065d 100644 --- a/setup/modelfactory.class.inc.php +++ b/setup/modelfactory.class.inc.php @@ -261,7 +261,7 @@ class MFModule while (($sFile = readdir($hDir)) !== false) { $aMatches = array(); - if (preg_match("/^[^\\.]+.dict.".$this->sName.".php$/i", $sFile, + if (preg_match("/^[^\\.]+.dict.".$this->sName.'.php$/i', $sFile, $aMatches)) // Dictionary files are named like .dict..php { $aDictionaries[] = $this->sRootDir.'/'.$sFile; @@ -1855,7 +1855,7 @@ class MFElement extends Combodo\iTop\DesignElement * Extracts some nodes from the DOM (active nodes only !!!) * * @param string $sXPath A XPath expression - * @param $sId + * @param string $sId * * @return DOMNodeList */ @@ -1867,7 +1867,7 @@ class MFElement extends Combodo\iTop\DesignElement /** * Returns the node directly under the given node * - * @param $sTagName + * @param string $sTagName * @param bool $bMustExist * * @return MFElement @@ -1903,7 +1903,7 @@ class MFElement extends Combodo\iTop\DesignElement * * @param string $sElementName * - * @return array|null|string + * @return array|string if no subnode is found, return current node text, else return results as array * @throws \DOMFormatException */ public function GetNodeAsArrayOfItems($sElementName = 'items') @@ -2068,6 +2068,8 @@ class MFElement extends Combodo\iTop\DesignElement /** * Check if the current node is under a node 'added' or 'altered' * Usage: In such a case, the change must not be tracked + * + * @return boolean true if `_alteration` flag is set on any parent of the current node */ public function IsInDefinition() { @@ -2104,14 +2106,14 @@ class MFElement extends Combodo\iTop\DesignElement return false; } - static $aTraceAttributes = null; + protected static $aTraceAttributes = null; /** * Enable/disable the trace on changed nodes * * @param array aAttributes Array of attributes (key => value) to be added onto any changed node */ - static public function SetTrace($aAttributes = null) + public static function SetTrace($aAttributes = null) { self::$aTraceAttributes = $aAttributes; } @@ -2582,9 +2584,9 @@ class MFDocument extends \Combodo\iTop\DesignDocument } /** - * @param $sXPath - * @param $sId - * @param null $oContextNode + * @param string $sXPath + * @param string $sId + * @param \DOMNode $oContextNode * * @return \DOMNodeList */ diff --git a/test/core/BulkChangeTest.inc.php b/test/core/BulkChangeTest.inc.php new file mode 100644 index 000000000..b02e1be52 --- /dev/null +++ b/test/core/BulkChangeTest.inc.php @@ -0,0 +1,72 @@ +createObject('Person', array( + 'first_name' => 'isaac', + 'name' => 'asimov', + 'email' => 'isaac.asimov@fundation.org', + 'org_id' => $this->getTestOrgId(), + )); + + $aData = array( + array($oPerson->Get("first_name"), + $oPerson->Get("name"), + $oPerson->Get("email"), + "EN US", + "iasimov", + "harryseldon", + "profileid->name:Administrator" + ) + ); + $aAttributes = array("language" => 3, "login" => 4, "password" => 5, "profile_list" => 6); + $aExtKeys = array("contactid" => + array("first_name" => 0, "name" => 1, "email" => 2)); + $oBulk = new \BulkChange( + "UserLocal", + $aData, + $aAttributes, + $aExtKeys, + array("login"), + null, + null, + "Y-m-d H:i:s", // date format + true // localize + ); + + $oChange = \CMDBObject::GetCurrentChange(); + $aRes = $oBulk->Process($oChange); + static::assertNotNull($aRes); + + foreach ($aRes as $aRow) + { + if (array_key_exists('__STATUS__', $aRow)) + { + $sStatus = $aRow['__STATUS__']; + $this->assertFalse(strstr($sStatus->GetDescription(), "CoreCannotSaveObjectException"), "CSVimport/Datasynchro: Password validation failed with: " . $sStatus->GetDescription()); + } + } + } + +} \ No newline at end of file diff --git a/test/core/DBSearchTest.php b/test/core/DBSearchTest.php index f73ee4a75..9389bc9ca 100644 --- a/test/core/DBSearchTest.php +++ b/test/core/DBSearchTest.php @@ -56,6 +56,9 @@ class DBSearchTest extends ItopDataTestCase protected function setUp() { parent::setUp(); + + require_once(APPROOT.'application/itopwebpage.class.inc.php'); + require_once(APPROOT.'application/displayblock.class.inc.php'); } /** @@ -656,4 +659,33 @@ class DBSearchTest extends ItopDataTestCase static::assertEquals('', $sExceptionClass); } + /** + * @throws \CoreException + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \OQLException + */ + public function testSelectInWithVariableExpressions() + { + $aReq = array(array(1, 0, 0), array(1, 1, 3), array(1, 2, 1), array(1, 0, 1), array(1, 1, 0), array(1, 2, 1)); + $sOrgs = $this->init_db(3, 4, $aReq); + $allOrgIds = explode(",", $sOrgs); + + $TwoOrgIdsOnly = array($allOrgIds[0], $allOrgIds[1]); + $oSearch = DBSearch::FromOQL("SELECT UserRequest WHERE org_id IN (:org_ids)"); + self::assertNotNull($oSearch); + $oSet = new \CMDBObjectSet($oSearch, array(), array('org_ids'=> $TwoOrgIdsOnly)); + static::assertEquals(4, $oSet->Count()); + + $_SERVER['REQUEST_URI']='FAKE_REQUEST_URI' ; + $_SERVER['REQUEST_METHOD']='FAKE_REQUEST_METHOD'; + $oP = new \iTopWebPage("test"); + $oBlock = new \DisplayBlock($oSet->GetFilter(), 'list', false); + $sHtml = $oBlock->GetDisplay($oP, 'package_table', array ('menu'=>true, 'display_limit'=>false)); + + $iHtmlUserRequestLineCount = substr_count($sHtml, 'output(); + } } diff --git a/webservices/rest.php b/webservices/rest.php index 433f2b365..647a035f2 100644 --- a/webservices/rest.php +++ b/webservices/rest.php @@ -79,7 +79,7 @@ try utils::UseParamFile(); $oKPI->ComputeAndReport('Data model loaded'); - + $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); // Starting with iTop 2.2.0 portal users are no longer allowed to access the REST/JSON API $oKPI->ComputeAndReport('User login'); @@ -130,11 +130,26 @@ try { throw new Exception("Missing parameter 'json_data'", RestResult::MISSING_JSON); } - $aJsonData = @json_decode($sJsonString); - if ($aJsonData == null) + + if (is_string($sJsonString)) { - throw new Exception("Parameter json_data is not a valid JSON structure", RestResult::INVALID_JSON); - } + $aJsonData = @json_decode($sJsonString); + } + elseif(is_array($sJsonString)) + { + $aJsonData = (object) $sJsonString; + $sJsonString = json_encode($aJsonData); + } + else + { + $aJsonData = null; + } + + if ($aJsonData == null) + { + throw new Exception('Parameter json_data is not a valid JSON structure', RestResult::INVALID_JSON); + } + $oKPI->ComputeAndReport('Parameters validated');