diff --git a/application/applicationextension/rest/RestUtils.php b/application/applicationextension/rest/RestUtils.php
index 07779ecdea..880862a7a2 100644
--- a/application/applicationextension/rest/RestUtils.php
+++ b/application/applicationextension/rest/RestUtils.php
@@ -97,33 +97,125 @@ class RestUtils
* @throws Exception
* @api
*/
- public static function GetFieldList($sClass, $oData, $sParamName)
+ public static function GetFieldList($sClass, $oData, $sParamName, $bFailIfNotFound = true)
{
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
- $aShowFields = [];
- if ($sFields == '*') {
- foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
- $aShowFields[$sClass][] = $sAttCode;
- }
- } elseif ($sFields == '*+') {
- foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
- foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef) {
- $aShowFields[$sRefClass][] = $sAttCode;
- }
- }
- } 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 $aShowFields;
+ return match($sFields) {
+ '*' => self::GetFieldListForClass($sClass),
+ '*+' => self::GetFieldListForParentClass($sClass),
+ default => self::GetLimitedFieldListForClass($sClass, $sFields, $sParamName, $bFailIfNotFound),
+ };
}
+ /**
+ * Check if the requested field list asks for an extended output.
+ *
+ * Extended output is requested when using '*+' or class-scoped field definitions.
+ *
+ * @param string $sFields Requested field specification.
+ *
+ * @return bool
+ */
+ public static function HasRequestedExtendedOutput(string $sFields): bool
+ {
+ return match($sFields) {
+ '*' => false,
+ '*+' => true,
+ default => substr_count($sFields, ':') > 1,
+ };
+ }
+
+ /**
+ * Check if the requested field list asks for all output fields.
+ *
+ * @param string $sFields Requested field specification.
+ *
+ * @return bool
+ */
+ 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))];
+ }
+
+ /**
+ * Build a field list for all child classes of the given parent class.
+ *
+ * @param string $sClass Parent class name.
+ *
+ * @return array Array of class => list of attribute codes.
+ */
+ 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));
+ }
+ return $aFieldList;
+ }
+
+ /**
+ * Build a restricted field list for one class from a comma-separated attribute list.
+ *
+ * @param string $sClass Class name.
+ * @param string $sFields Comma-separated list of requested attribute codes.
+ * @param string $sParamName Input parameter name used in error messages.
+ * @param bool $bFailIfNotFound If true, throws when an attribute code is invalid.
+ *
+ * @return array Array containing one class => list of attribute codes.
+ * @throws Exception When an attribute code is invalid and $bFailIfNotFound is true.
+ */
+ 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'");
+ }
+ }
+ }
+ return $aFieldList;
+ }
+
+ /**
+ * Build a restricted field list for one or several classes.
+ *
+ * Accepted formats are either "att1,att2" for a single class, or
+ * "ClassA:att1,att2;ClassB:att3" for class-scoped field definitions.
+ *
+ * @param string $sClass Default class name used when no class scope is specified.
+ * @param string $sFields Requested field specification.
+ * @param string $sParamName Input parameter name used in error messages.
+ * @param bool $bFailIfNotFound If true, throws when an attribute code is invalid.
+ *
+ * @return array Array of class => list of attribute codes.
+ * @throws Exception Propagated from GetLimitedFieldListForSingleClass.
+ */
+ 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);
+ }
+
+ $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;
+ }
/**
* Read and interpret object search criteria from a Rest/Json structure
*
diff --git a/core/restservices.class.inc.php b/core/restservices.class.inc.php
index 02bf355f8d..ae08266588 100644
--- a/core/restservices.class.inc.php
+++ b/core/restservices.class.inc.php
@@ -248,6 +248,45 @@ class RestResultWithObjects extends RestResult
}
}
+/**
+ * @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 = ObjectResult::FromDBObject($oObject, $aFieldSpec, $bExtendedOutput, $iCode, $sMessage);
+ $this->current_object[$sObjectAlias] = $oObjRes;
+ }
+}
+
/**
* @package RESTAPI
* @api
@@ -500,15 +539,22 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
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));
- $sTargetClass = $oObjectSet->GetFilter()->GetClass();
+ // 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) {
$oResult->code = RestResult::UNAUTHORIZED;
@@ -519,19 +565,67 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
} elseif ($iPage < 1) {
$oResult->code = RestResult::INVALID_PAGE;
$oResult->message = "The request page number is not valid. It must be an integer greater than 0";
- } else {
- 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 = [$oObjectSet->GetClassAlias() => $aFields];
- $oObjectSet->OptimizeColumnLoad($aAttToLoad);
+ } elseif (count($oObjectSet->GetSelectedClasses()) > 1) {
+ $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 = [$oObjectSet->GetClassAlias() => $aFields];
+ $oObjectSet->OptimizeColumnLoad($aAttToLoad);
+ }
+
while ($oObject = $oObjectSet->Fetch()) {
- $oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
+ $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 956bc05d6f..a072ad7ecf 100644
--- a/tests/php-unit-tests/unitary-tests/webservices/RestTest.php
+++ b/tests/php-unit-tests/unitary-tests/webservices/RestTest.php
@@ -139,6 +139,158 @@ JSON;
$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(<<