diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php
index d1aa296e44..b40bf1e945 100644
--- a/application/applicationextension.inc.php
+++ b/application/applicationextension.inc.php
@@ -1929,41 +1929,76 @@ public static function GetClass($oData, $sParamName)
* @return array of class => list of attributes (see RestResultWithObjects::AddObject that uses it)
* @throws Exception
*/
- public static function GetFieldList($sClass, $oData, $sParamName)
+ public static function GetFieldList($sClass, $oData, $sParamName, $bFailIfNotFound = true)
{
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
- $aShowFields = array();
- if ($sFields == '*')
- {
- foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
- {
- $aShowFields[$sClass][] = $sAttCode;
- }
+ return match($sFields) {
+ '*' => self::GetFieldListForClass($sClass),
+ '*+' => self::GetFieldListForParentClass($sClass),
+ default => self::GetLimitedFieldListForClass($sClass, $sFields, $sParamName, $bFailIfNotFound),
+ };
+ }
+
+ public static function HasRequestedExtendedOutput(string $sFields): bool
+ {
+ return match($sFields) {
+ '*' => false,
+ '*+' => true,
+ default => substr_count($sFields, ':') > 1,
+ };
+ }
+
+ public static function HasRequestedAllOutputFields(string $sFields): bool
+ {
+ return match($sFields) {
+ '*', '*+' => true,
+ default => false,
+ };
+ }
+
+ protected static function GetFieldListForClass(string $sClass): array
+ {
+ return [$sClass => array_keys(MetaModel::ListAttributeDefs($sClass))];
+ }
+
+ protected static function GetFieldListForParentClass(string $sClass): array
+ {
+ $aFieldList = array();
+ foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
+ $aFieldList = array_merge($aFieldList, self::GetFieldListForClass($sRefClass));
}
- elseif ($sFields == '*+')
- {
- foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass)
- {
- foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef)
- {
- $aShowFields[$sRefClass][] = $sAttCode;
+ return $aFieldList;
+ }
+
+ protected static function GetLimitedFieldListForSingleClass(string $sClass, string $sFields, string $sParamName, bool $bFailIfNotFound = true): array
+ {
+ $aFieldList = [$sClass => []];
+ foreach (explode(',', $sFields) as $sAttCode) {
+ $sAttCode = trim($sAttCode);
+ if (($sAttCode == 'id') || (MetaModel::IsValidAttCode($sClass, $sAttCode))) {
+ $aFieldList[$sClass][] = $sAttCode;
+ } else {
+ if ($bFailIfNotFound) {
+ throw new Exception("$sParamName: invalid attribute code '$sAttCode' for class '$sClass'");
}
}
}
- else
- {
- foreach (explode(',', $sFields) as $sAttCode)
- {
- $sAttCode = trim($sAttCode);
- if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
- {
- throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
- }
- $aShowFields[$sClass][] = $sAttCode;
- }
+ return $aFieldList;
+ }
+
+ protected static function GetLimitedFieldListForClass(string $sClass, string $sFields, string $sParamName, bool $bFailIfNotFound = true): array
+ {
+ if (!str_contains($sFields, ':')) {
+ return self::GetLimitedFieldListForSingleClass($sClass, $sFields, $sParamName, $bFailIfNotFound);
}
- return $aShowFields;
+ $aFieldList = [];
+ $aFieldListParts = explode(';', $sFields);
+ foreach ($aFieldListParts as $sClassFields) {
+ list($sSubClass, $sSubClassFields) = explode(':', $sClassFields);
+ $aFieldList = array_merge($aFieldList, self::GetLimitedFieldListForSingleClass(trim($sSubClass), trim($sSubClassFields), $sParamName, $bFailIfNotFound));
+ }
+ return $aFieldList;
}
/**
diff --git a/core/restservices.class.inc.php b/core/restservices.class.inc.php
index b66ed5e090..ba93092744 100644
--- a/core/restservices.class.inc.php
+++ b/core/restservices.class.inc.php
@@ -185,23 +185,7 @@ class RestResultWithObjects extends RestResult
/** @var array "DBObject_class:DBObject_key" as key, {@see \ObjectResult} as value */
public $objects;
- /**
- * Report the given object
- *
- * @api
- * @param int $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
- * @param DBObject $oObject The object being reported
- * @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 ?
- *
- * @return void
- * @throws \ArchivedObjectException
- * @throws \CoreException
- * @throws \CoreUnexpectedValue
- * @throws \MySQLException
- */
- public function AddObject($iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
+ public function PrepareObject($iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
{
$sClass = get_class($oObject);
$oObjRes = new ObjectResult($sClass, $oObject->GetKey());
@@ -232,6 +216,28 @@ public function AddObject($iCode, $sMessage, $oObject, $aFieldSpec = null, $bExt
$oObjRes->AddField($oObject, $sAttCode, $bExtendedOutput);
}
+ return $oObjRes;
+ }
+
+ /**
+ * Report the given object
+ *
+ * @api
+ * @param int $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
+ * @param DBObject $oObject The object being reported
+ * @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 ?
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \MySQLException
+ */
+ public function AddObject($iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
+ {
+ $oObjRes = $this->PrepareObject($iCode, $sMessage, $oObject, $aFieldSpec, $bExtendedOutput);
$sObjKey = get_class($oObject).'::'.$oObject->GetKey();
$this->objects[$sObjKey] = $oObjRes;
}
@@ -247,6 +253,45 @@ public function SanitizeContent()
}
}
+/**
+ * @package RESTAPI
+ * @api
+ */
+class RestResultWithObjectSets extends RestResultWithObjects
+{
+ private $current_object = null;
+
+ public function MakeNewObjectSet()
+ {
+ $arr = array();
+ $this->current_object = &$arr;
+ $this->objects[] = &$arr;
+ }
+
+ /**
+ * Report the given object
+ *
+ * @api
+ * @param string $sObjectAlias Name of the subobject, usually the OQL class alias
+ * @param int $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
+ * @param DBObject $oObject The object being reported
+ * @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 ?
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \MySQLException
+ */
+ public function AppendSubObject($sObjectAlias, $iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
+ {
+ $oObjRes = $this->PrepareObject($iCode, $sMessage, $oObject, $aFieldSpec, $bExtendedOutput);
+ $this->current_object[$sObjectAlias] = $oObjRes;
+ }
+}
+
/**
* @package RESTAPI
* @api
@@ -526,14 +571,21 @@ public function ExecOperation($sVersion, $sVerb, $aParams)
break;
case 'core/get':
- $sClass = RestUtils::GetClass($aParams, 'class');
+ $sClassParam = RestUtils::GetMandatoryParam($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
- $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
- $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
+ $sShowFields = RestUtils::GetOptionalParam($aParams, 'output_fields', '*');
$iLimit = (int)RestUtils::GetOptionalParam($aParams, 'limit', 0);
$iPage = (int)RestUtils::GetOptionalParam($aParams, 'page', 1);
- $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
+ // Validate the class(es)
+ $aClass = explode(',', $sClassParam);
+ foreach ($aClass as $sClass) {
+ if (!MetaModel::IsValidClass(trim($sClass))) {
+ throw new Exception("class '$sClass' is not valid");
+ }
+ }
+
+ $oObjectSet = RestUtils::GetObjectSetFromKey($sClassParam, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
$sTargetClass = $oObjectSet->GetFilter()->GetClass();
if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ) != UR_ALLOWED_YES)
@@ -551,23 +603,68 @@ public function ExecOperation($sVersion, $sVerb, $aParams)
$oResult->code = RestResult::INVALID_PAGE;
$oResult->message = "The request page number is not valid. It must be an integer greater than 0";
}
- else
+ elseif (count($oObjectSet->GetSelectedClasses()) > 1)
{
- if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*')
- {
- $aFields = $aShowFields[$sClass];
- //Id is not a valid attribute to optimize
- 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);
+ $oResult = new RestResultWithObjectSets();
+ $aCache = [];
+ $aShowFields = [];
+ foreach ($oObjectSet->GetSelectedClasses() as $sSelectedClass) {
+ $aShowFields = array_merge( $aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
+ }
+
+ while ($oObjects = $oObjectSet->FetchAssoc()) {
+ $oResult->MakeNewObjectSet();
+
+ foreach ($oObjects as $sAlias => $oObject) {
+ if (!$oObject) {
+ continue;
+ }
+
+ if (!array_key_exists($sAlias, $aCache)) {
+ $sClass = get_class($oObject);
+ $bExtendedOutput = RestUtils::HasRequestedExtendedOutput($sShowFields);
+
+ if (!RestUtils::HasRequestedAllOutputFields($sShowFields)) {
+ $aFields = $aShowFields[$sClass];
+ //Id is not a valid attribute to optimize
+ if ($aFields && in_array('id', $aFields)) {
+ unset($aFields[array_search('id', $aFields)]);
+ }
+ $aAttToLoad = [$sAlias => $aFields];
+ $oObjectSet->OptimizeColumnLoad($aAttToLoad);
+ }
+ $aCache[$sAlias] = [
+ 'aShowFields' => $aShowFields,
+ 'bExtendedOutput' => $bExtendedOutput,
+ ];
+ } else {
+ $aShowFields = $aCache[$sAlias]['aShowFields'];
+ $bExtendedOutput = $aCache[$sAlias]['bExtendedOutput'];
+ }
+
+ $oResult->AppendSubObject($sAlias, 0, '', $oObject, $aShowFields, $bExtendedOutput);
+ }
+ }
+ $oResult->message = "Found: ".$oObjectSet->Count();
+ } else {
+ $aShowFields =[];
+ foreach ($aClass as $sSelectedClass) {
+ $sSelectedClass = trim($sSelectedClass);
+ $aShowFields = array_merge($aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
+ }
+
+ if (!RestUtils::HasRequestedAllOutputFields($sShowFields) && count($aShowFields) == 1) {
+ $aFields = $aShowFields[$sClass];
+ //Id is not a valid attribute to optimize
+ 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, RestUtils::HasRequestedExtendedOutput($sShowFields));
}
$oResult->message = "Found: ".$oObjectSet->Count();
}
diff --git a/tests/php-unit-tests/unitary-tests/webservices/RestTest.php b/tests/php-unit-tests/unitary-tests/webservices/RestTest.php
index 6b1c6848a3..f96545ab97 100644
--- a/tests/php-unit-tests/unitary-tests/webservices/RestTest.php
+++ b/tests/php-unit-tests/unitary-tests/webservices/RestTest.php
@@ -142,6 +142,158 @@ public function testCoreApiGet(){
$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
}
+
+ public function testCoreApiGet_Select2SubClasses(){
+ // Create ticket
+ $description = date('dmY H:i:s');
+ $iIdCaller = $this->CreatePerson(1)->GetKey();
+ $oUserRequest = $this->CreateSampleTicket($description, 'UserRequest', $iIdCaller);
+ $oChange = $this->CreateSampleTicket($description, 'Change', $iIdCaller);
+ $iIdUserRequest = $oUserRequest->GetKey();
+ $iIdChange = $oChange->GetKey();
+
+ $sJSONOutput = $this->CallCoreRestApi_Internally(<<
$description
", + "id": "$iIdChange", + "outage": "no" + }, + "key": "$iIdChange", + "message": "" + } + } +} +JSON; + $this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput); + } + + + public function testCoreApiGet_SelectTicketAndPerson(){ + // Create ticket + $description = date('dmY H:i:s'); + $iIdCaller = $this->CreatePerson(1)->GetKey(); + $oUserRequest = $this->CreateSampleTicket($description, 'UserRequest', $iIdCaller); + $iIdUserRequest = $oUserRequest->GetKey(); + + $sJSONOutput = $this->CallCoreRestApi_Internally(<<