mirror of
https://github.com/Combodo/iTop.git
synced 2026-06-21 15:26:37 +02:00
Compare commits
4 Commits
develop
...
feature/lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3244658686 | ||
|
|
806a5b92df | ||
|
|
7d0005acf3 | ||
|
|
9c782c41df |
@@ -97,125 +97,33 @@ class RestUtils
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
* @api
|
* @api
|
||||||
*/
|
*/
|
||||||
public static function GetFieldList($sClass, $oData, $sParamName, $bFailIfNotFound = true)
|
public static function GetFieldList($sClass, $oData, $sParamName)
|
||||||
{
|
{
|
||||||
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
|
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
|
||||||
return match($sFields) {
|
$aShowFields = [];
|
||||||
'*' => self::GetFieldListForClass($sClass),
|
if ($sFields == '*') {
|
||||||
'*+' => self::GetFieldListForParentClass($sClass),
|
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
|
||||||
default => self::GetLimitedFieldListForClass($sClass, $sFields, $sParamName, $bFailIfNotFound),
|
$aShowFields[$sClass][] = $sAttCode;
|
||||||
};
|
}
|
||||||
}
|
} elseif ($sFields == '*+') {
|
||||||
|
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
|
||||||
/**
|
foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef) {
|
||||||
* Check if the requested field list asks for an extended output.
|
$aShowFields[$sRefClass][] = $sAttCode;
|
||||||
*
|
|
||||||
* 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'");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
return $aFieldList;
|
foreach (explode(',', $sFields) as $sAttCode) {
|
||||||
}
|
$sAttCode = trim($sAttCode);
|
||||||
|
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode))) {
|
||||||
/**
|
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
|
||||||
* Build a restricted field list for one or several classes.
|
}
|
||||||
*
|
$aShowFields[$sClass][] = $sAttCode;
|
||||||
* 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 = [];
|
return $aShowFields;
|
||||||
$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
|
* Read and interpret object search criteria from a Rest/Json structure
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
* @license http://opensource.org/licenses/AGPL-3.0
|
* @license http://opensource.org/licenses/AGPL-3.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class CoreException extends Exception
|
use Combodo\iTop\Exception\ItopException;
|
||||||
|
|
||||||
|
class CoreException extends ItopException
|
||||||
{
|
{
|
||||||
protected $m_sIssue;
|
protected $m_sIssue;
|
||||||
protected $m_sImpact;
|
protected $m_sImpact;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
use Combodo\iTop\Application\Helper\ExceptionHandlerHelper;
|
||||||
use Combodo\iTop\Application\Helper\Session;
|
use Combodo\iTop\Application\Helper\Session;
|
||||||
use Combodo\iTop\Service\Startup\StartupService;
|
use Combodo\iTop\Service\Startup\StartupService;
|
||||||
|
|
||||||
@@ -64,6 +65,9 @@ register_shutdown_function(function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
set_exception_handler([ExceptionHandlerHelper::class, 'HandleException']);
|
||||||
|
|
||||||
$oKPI = new ExecutionKPI();
|
$oKPI = new ExecutionKPI();
|
||||||
Session::Start();
|
Session::Start();
|
||||||
$oKPI->ComputeAndReport("Session Start");
|
$oKPI->ComputeAndReport("Session Start");
|
||||||
|
|||||||
@@ -614,6 +614,12 @@ class LogChannels
|
|||||||
* @since 3.2.0
|
* @since 3.2.0
|
||||||
*/
|
*/
|
||||||
public const SECURITY = 'Security';
|
public const SECURITY = 'Security';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
* @since 3.3.0
|
||||||
|
*/
|
||||||
|
public const EXCEPTION = 'Exception';
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class LogAPI
|
abstract class LogAPI
|
||||||
@@ -708,8 +714,14 @@ abstract class LogAPI
|
|||||||
|
|
||||||
private static function PrepareErrorLog(string $sMessage, throwable $oException, array $aContext, bool $isPrevious = false): array
|
private static function PrepareErrorLog(string $sMessage, throwable $oException, array $aContext, bool $isPrevious = false): array
|
||||||
{
|
{
|
||||||
$aContext['Error Message'] = $oException->getMessage();
|
$aContext['exception'] = get_class($oException);
|
||||||
$aContext['Stack Trace'] = $oException->getTraceAsString();
|
$aContext['message'] = $oException->getMessage();
|
||||||
|
$aContext['stack'] = $oException->getTraceAsString();
|
||||||
|
|
||||||
|
if(method_exists($oException, 'GetContext')) {
|
||||||
|
$aContext = array_merge($aContext, $oException->GetContext());
|
||||||
|
}
|
||||||
|
|
||||||
return ['message' => ($isPrevious ? "Previous " : '')."Exception: $sMessage", 'context' => $aContext];
|
return ['message' => ($isPrevious ? "Previous " : '')."Exception: $sMessage", 'context' => $aContext];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,45 +248,6 @@ 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
|
* @package RESTAPI
|
||||||
* @api
|
* @api
|
||||||
@@ -539,22 +500,15 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'core/get':
|
case 'core/get':
|
||||||
$sClassParam = RestUtils::GetMandatoryParam($aParams, 'class');
|
$sClass = RestUtils::GetClass($aParams, 'class');
|
||||||
$key = RestUtils::GetMandatoryParam($aParams, 'key');
|
$key = RestUtils::GetMandatoryParam($aParams, 'key');
|
||||||
$sShowFields = RestUtils::GetOptionalParam($aParams, 'output_fields', '*');
|
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
|
||||||
|
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
|
||||||
$iLimit = (int)RestUtils::GetOptionalParam($aParams, 'limit', 0);
|
$iLimit = (int)RestUtils::GetOptionalParam($aParams, 'limit', 0);
|
||||||
$iPage = (int)RestUtils::GetOptionalParam($aParams, 'page', 1);
|
$iPage = (int)RestUtils::GetOptionalParam($aParams, 'page', 1);
|
||||||
|
|
||||||
// Validate the class(es)
|
$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
|
||||||
$aClass = explode(',', $sClassParam);
|
$sTargetClass = $oObjectSet->GetFilter()->GetClass();
|
||||||
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) {
|
if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ) != UR_ALLOWED_YES) {
|
||||||
$oResult->code = RestResult::UNAUTHORIZED;
|
$oResult->code = RestResult::UNAUTHORIZED;
|
||||||
@@ -565,67 +519,19 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
|
|||||||
} elseif ($iPage < 1) {
|
} elseif ($iPage < 1) {
|
||||||
$oResult->code = RestResult::INVALID_PAGE;
|
$oResult->code = RestResult::INVALID_PAGE;
|
||||||
$oResult->message = "The request page number is not valid. It must be an integer greater than 0";
|
$oResult->message = "The request page number is not valid. It must be an integer greater than 0";
|
||||||
} 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 {
|
} else {
|
||||||
$aShowFields =[];
|
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*') {
|
||||||
foreach ($aClass as $sSelectedClass) {
|
$aFields = $aShowFields[$sClass];
|
||||||
$sSelectedClass = trim($sSelectedClass);
|
//Id is not a valid attribute to optimize
|
||||||
$aShowFields = array_merge($aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
|
if (in_array('id', $aFields)) {
|
||||||
}
|
unset($aFields[array_search('id', $aFields)]);
|
||||||
|
}
|
||||||
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];
|
$aAttToLoad = [$oObjectSet->GetClassAlias() => $aFields];
|
||||||
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
|
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
while ($oObject = $oObjectSet->Fetch()) {
|
while ($oObject = $oObjectSet->Fetch()) {
|
||||||
$oResult->AddObject(0, '', $oObject, $aShowFields, RestUtils::HasRequestedExtendedOutput($sShowFields));
|
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
|
||||||
}
|
}
|
||||||
$oResult->message = "Found: ".$oObjectSet->Count();
|
$oResult->message = "Found: ".$oObjectSet->Count();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ return array(
|
|||||||
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => $baseDir . '/sources/Application/EventRegister/ApplicationEvents.php',
|
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => $baseDir . '/sources/Application/EventRegister/ApplicationEvents.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\BulkHelper' => $baseDir . '/sources/Application/Helper/BulkHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\BulkHelper' => $baseDir . '/sources/Application/Helper/BulkHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php',
|
||||||
|
'Combodo\\iTop\\Application\\Helper\\ExceptionHandlerHelper' => $baseDir . '/sources/Application/Helper/ExceptionHandlerHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\FormHelper' => $baseDir . '/sources/Application/Helper/FormHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\FormHelper' => $baseDir . '/sources/Application/Helper/FormHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => $baseDir . '/sources/Application/Helper/ImportHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => $baseDir . '/sources/Application/Helper/ImportHelper.php',
|
||||||
@@ -446,6 +447,7 @@ return array(
|
|||||||
'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => $baseDir . '/sources/Dependencies/NPM/iTopNPM.php',
|
'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => $baseDir . '/sources/Dependencies/NPM/iTopNPM.php',
|
||||||
'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php',
|
'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php',
|
||||||
'Combodo\\iTop\\DesignElement' => $baseDir . '/core/designdocument.class.inc.php',
|
'Combodo\\iTop\\DesignElement' => $baseDir . '/core/designdocument.class.inc.php',
|
||||||
|
'Combodo\\iTop\\Exception\\ItopException' => $baseDir . '/sources/Exception/ItopException.php',
|
||||||
'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => $baseDir . '/sources/Form/Field/AbstractSimpleField.php',
|
'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => $baseDir . '/sources/Form/Field/AbstractSimpleField.php',
|
||||||
'Combodo\\iTop\\Form\\Field\\BlobField' => $baseDir . '/sources/Form/Field/BlobField.php',
|
'Combodo\\iTop\\Form\\Field\\BlobField' => $baseDir . '/sources/Form/Field/BlobField.php',
|
||||||
'Combodo\\iTop\\Form\\Field\\CaseLogField' => $baseDir . '/sources/Form/Field/CaseLogField.php',
|
'Combodo\\iTop\\Form\\Field\\CaseLogField' => $baseDir . '/sources/Form/Field/CaseLogField.php',
|
||||||
|
|||||||
@@ -534,6 +534,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685
|
|||||||
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => __DIR__ . '/../..' . '/sources/Application/EventRegister/ApplicationEvents.php',
|
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => __DIR__ . '/../..' . '/sources/Application/EventRegister/ApplicationEvents.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\BulkHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/BulkHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\BulkHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/BulkHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php',
|
||||||
|
'Combodo\\iTop\\Application\\Helper\\ExceptionHandlerHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExceptionHandlerHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\FormHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/FormHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\FormHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/FormHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ImportHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ImportHelper.php',
|
||||||
@@ -847,6 +848,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685
|
|||||||
'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => __DIR__ . '/../..' . '/sources/Dependencies/NPM/iTopNPM.php',
|
'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => __DIR__ . '/../..' . '/sources/Dependencies/NPM/iTopNPM.php',
|
||||||
'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
|
'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
|
||||||
'Combodo\\iTop\\DesignElement' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
|
'Combodo\\iTop\\DesignElement' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
|
||||||
|
'Combodo\\iTop\\Exception\\ItopException' => __DIR__ . '/../..' . '/sources/Exception/ItopException.php',
|
||||||
'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => __DIR__ . '/../..' . '/sources/Form/Field/AbstractSimpleField.php',
|
'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => __DIR__ . '/../..' . '/sources/Form/Field/AbstractSimpleField.php',
|
||||||
'Combodo\\iTop\\Form\\Field\\BlobField' => __DIR__ . '/../..' . '/sources/Form/Field/BlobField.php',
|
'Combodo\\iTop\\Form\\Field\\BlobField' => __DIR__ . '/../..' . '/sources/Form/Field/BlobField.php',
|
||||||
'Combodo\\iTop\\Form\\Field\\CaseLogField' => __DIR__ . '/../..' . '/sources/Form/Field/CaseLogField.php',
|
'Combodo\\iTop\\Form\\Field\\CaseLogField' => __DIR__ . '/../..' . '/sources/Form/Field/CaseLogField.php',
|
||||||
|
|||||||
38
pages/UI.php
38
pages/UI.php
@@ -20,12 +20,12 @@ use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
|
|||||||
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
|
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
|
||||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
|
||||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||||
use Combodo\iTop\Application\WebPage\WebPage;
|
use Combodo\iTop\Application\WebPage\WebPage;
|
||||||
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||||
use Combodo\iTop\Controller\Base\Layout\ObjectController;
|
use Combodo\iTop\Controller\Base\Layout\ObjectController;
|
||||||
use Combodo\iTop\Controller\WelcomePopupController;
|
use Combodo\iTop\Controller\WelcomePopupController;
|
||||||
|
use Combodo\iTop\Exception\ItopException;
|
||||||
use Combodo\iTop\Service\Router\Router;
|
use Combodo\iTop\Service\Router\Router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1321,37 +1321,7 @@ try {
|
|||||||
$oKPI->ComputeAndReport('Compute page');
|
$oKPI->ComputeAndReport('Compute page');
|
||||||
$oP->output();
|
$oP->output();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$oErrorPage = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
throw new ItopException("Unable to handle UI operation", previous: $e, aContext: [
|
||||||
if ($e instanceof SecurityException) {
|
'operation' => ($operation ?? 'N/A'),
|
||||||
$oErrorPage->add("<h1>".Dict::S('UI:SystemIntrusion')."</h1>\n");
|
]);
|
||||||
} else {
|
|
||||||
$oErrorPage->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
|
||||||
}
|
|
||||||
$sErrorDetails = ($e instanceof CoreException) ? $e->getHtmlDesc() : $e->getMessage();
|
|
||||||
$oErrorPage->error(Dict::Format('UI:Error_Details', utils::EscapeHtml($sErrorDetails)), $e);
|
|
||||||
$oErrorPage->output();
|
|
||||||
|
|
||||||
$sErrorStackTrace = ($e instanceof CoreException) ? $e->getFullStackTraceAsString() : $e->getTraceAsString();
|
|
||||||
if (MetaModel::IsLogEnabledIssue()) {
|
|
||||||
if (MetaModel::IsValidClass('EventIssue')) {
|
|
||||||
try {
|
|
||||||
$oLog = new EventIssue();
|
|
||||||
|
|
||||||
$oLog->Set('message', $e->getMessage());
|
|
||||||
$oLog->Set('userinfo', '');
|
|
||||||
$sIssue = ($e instanceof CoreException) ? $e->GetIssue() : 'PHP Exception';
|
|
||||||
$oLog->Set('issue', $sIssue);
|
|
||||||
$oLog->Set('impact', 'Page could not be displayed');
|
|
||||||
$oLog->Set('callstack', $sErrorStackTrace);
|
|
||||||
$aData = ($e instanceof CoreException) ? $e->getContextData() : [];
|
|
||||||
$oLog->Set('data', $aData);
|
|
||||||
$oLog->DBInsertNoReload();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
IssueLog::Exception("Failed to log issue into the DB", $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$sOperationToLog = $operation ?? 'N/A';
|
|
||||||
IssueLog::Debug('UI.php operation='.$sOperationToLog.', error='.$e->getMessage()."\n".$sErrorStackTrace, LogChannels::CONSOLE);
|
|
||||||
}
|
}
|
||||||
|
|||||||
119
sources/Application/Helper/ExceptionHandlerHelper.php
Normal file
119
sources/Application/Helper/ExceptionHandlerHelper.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Combodo\iTop\Application\Helper;
|
||||||
|
|
||||||
|
use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||||
|
use IssueLog;
|
||||||
|
use SimpleXMLElement;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class ExceptionHandlerHelper
|
||||||
|
{
|
||||||
|
public static array $aSupportedMimeTypes = [
|
||||||
|
'application/json' => 'json',
|
||||||
|
'application/xml' => 'xml',
|
||||||
|
'text/html' => 'html',
|
||||||
|
'text/plain' => 'text',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function HandleException(Throwable $oException)
|
||||||
|
{
|
||||||
|
$aStatus = ob_get_status();
|
||||||
|
if (count($aStatus) !== 0) {
|
||||||
|
ob_end_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the exception
|
||||||
|
IssueLog::Exception('Fatal error', $oException);
|
||||||
|
|
||||||
|
$mime = self::NegotiateMimeType();
|
||||||
|
|
||||||
|
if ($mime === null) {
|
||||||
|
http_response_code(406);
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Vary: Accept');
|
||||||
|
echo json_encode(['error' => 'Not Acceptable'], JSON_UNESCAPED_UNICODE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(500);
|
||||||
|
header("Content-Type: {$mime}; charset=utf-8");
|
||||||
|
header('Vary: Accept');
|
||||||
|
|
||||||
|
$aData = [
|
||||||
|
'error' => 'Fatal error',
|
||||||
|
'message' => 'We are sorry, an unexpected error has occurred. Please try again later.',
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
switch (self::$aSupportedMimeTypes[$mime]) {
|
||||||
|
case 'json':
|
||||||
|
echo json_encode($aData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'xml':
|
||||||
|
$oXml = new SimpleXMLElement('<response/>');
|
||||||
|
array_walk_recursive($aData, function ($sValue, $sKey) use ($oXml) {
|
||||||
|
$oXml->addChild((string)$sKey, htmlspecialchars((string)$sValue, ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||||
|
});
|
||||||
|
echo $oXml->asXML();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'html':
|
||||||
|
// Create error page
|
||||||
|
$oErrorPage = new ErrorPage('Fatal error');
|
||||||
|
$oErrorPage->error('We are sorry, an unexpected error has occurred. Please try again later.<br><br>', $oException);
|
||||||
|
$oErrorPage->output();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
echo "Fatal error\n";
|
||||||
|
echo "We are sorry, an unexpected error has occurred. Please try again later.\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function NegotiateMimeType(): ?string
|
||||||
|
{
|
||||||
|
$supportedMimes = array_keys(self::$aSupportedMimeTypes);
|
||||||
|
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '*/*';
|
||||||
|
|
||||||
|
if (trim($acceptHeader) === '' || $acceptHeader === '*/*') {
|
||||||
|
return in_array('application/json', $supportedMimes, true) ? 'application/json' : $supportedMimes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$accepted = [];
|
||||||
|
foreach (explode(',', $acceptHeader) as $part) {
|
||||||
|
$part = trim($part);
|
||||||
|
$q = 1.0;
|
||||||
|
if (str_contains($part, ';')) {
|
||||||
|
[$type, $params] = array_map('trim', explode(';', $part, 2));
|
||||||
|
if (preg_match('/q=([0-9.]+)/', $params, $m)) {
|
||||||
|
$q = (float)$m[1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$type = $part;
|
||||||
|
}
|
||||||
|
$accepted[] = ['type' => $type, 'q' => $q];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($accepted, fn ($a, $b) => $b['q'] <=> $a['q']);
|
||||||
|
|
||||||
|
foreach ($accepted as $a) {
|
||||||
|
foreach ($supportedMimes as $mime) {
|
||||||
|
if ($a['type'] === $mime || $a['type'] === '*/*') {
|
||||||
|
return $mime;
|
||||||
|
}
|
||||||
|
// Ex: application/* match application/json
|
||||||
|
if (str_ends_with($a['type'], '/*')) {
|
||||||
|
$prefix = explode('/', $a['type'])[0].'/';
|
||||||
|
if (str_starts_with($mime, $prefix)) {
|
||||||
|
return $mime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,6 @@ require_once APPROOT.'setup/setuppage.class.inc.php';
|
|||||||
use ApplicationMenu;
|
use ApplicationMenu;
|
||||||
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
|
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
|
||||||
use Combodo\iTop\Application\WebPage\AjaxPage;
|
use Combodo\iTop\Application\WebPage\AjaxPage;
|
||||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
|
||||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||||
use Combodo\iTop\Application\WebPage\WebPage;
|
use Combodo\iTop\Application\WebPage\WebPage;
|
||||||
use Combodo\iTop\Controller\AbstractController;
|
use Combodo\iTop\Controller\AbstractController;
|
||||||
@@ -263,29 +262,29 @@ abstract class Controller extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function HandleOperation(): void
|
public function HandleOperation(): void
|
||||||
{
|
{
|
||||||
try {
|
// try {
|
||||||
$this->CheckAccess();
|
$this->CheckAccess();
|
||||||
$this->m_sOperation = utils::ReadParam('operation', $this->sDefaultOperation);
|
$this->m_sOperation = utils::ReadParam('operation', $this->sDefaultOperation);
|
||||||
|
|
||||||
if ($this->CallOperation(utils::ToCamelCase($this->m_sOperation))) {
|
if ($this->CallOperation(utils::ToCamelCase($this->m_sOperation))) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to unchanged names for compatibility
|
|
||||||
if ($this->CallOperation($this->m_sOperation)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->DisplayBadRequest();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
http_response_code(500);
|
|
||||||
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
|
||||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
|
||||||
$oP->add(get_class($e).' : '.utils::EscapeHtml($e->GetMessage()));
|
|
||||||
$oP->output();
|
|
||||||
|
|
||||||
IssueLog::Exception('HandleOperation failed for '.json_encode($this->m_sOperation), $e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to unchanged names for compatibility
|
||||||
|
if ($this->CallOperation($this->m_sOperation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->DisplayBadRequest();
|
||||||
|
// } catch (Exception $e) {
|
||||||
|
// http_response_code(500);
|
||||||
|
// $oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||||
|
// $oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||||
|
// $oP->add(get_class($e).' : '.utils::EscapeHtml($e->GetMessage()));
|
||||||
|
// $oP->output();
|
||||||
|
//
|
||||||
|
// IssueLog::Exception('HandleOperation failed for '.json_encode($this->m_sOperation), $e);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
28
sources/Exception/ItopException.php
Normal file
28
sources/Exception/ItopException.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Combodo\iTop\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use IssueLog;
|
||||||
|
use LogChannels;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class ItopException extends Exception
|
||||||
|
{
|
||||||
|
private array $aContext;
|
||||||
|
|
||||||
|
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null, array $aContext = [])
|
||||||
|
{
|
||||||
|
$aContext['code'] = $code;
|
||||||
|
IssueLog::Debug($message, LogChannels::EXCEPTION, $aContext);
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
$this->aContext = $aContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContext(): array
|
||||||
|
{
|
||||||
|
return $this->aContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
483
tests/php-code-style/composer.lock
generated
483
tests/php-code-style/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -391,10 +391,6 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: This test is not optimal!
|
|
||||||
* It checks if a dict entry is present in the same file but *accross different `Dict::Add()` sections*.
|
|
||||||
* If the dict entry is present multiple times within the same `Dict::Add()` section, it won't be detected as the test evaluates the PHP files which deduplicate the hash array, making the algorythm unaccurate.
|
|
||||||
*
|
|
||||||
* @dataProvider DictionaryFileProvider
|
* @dataProvider DictionaryFileProvider
|
||||||
*/
|
*/
|
||||||
public function testDictKeyDefinedOncePerFile(string $sDictFileToTestFullPath): void
|
public function testDictKeyDefinedOncePerFile(string $sDictFileToTestFullPath): void
|
||||||
@@ -413,10 +409,6 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: This test is not optimal!
|
|
||||||
* It checks if a dict entry is present in the same file but *accross different `Dict::Add()` sections*.
|
|
||||||
* If the dict entry is present multiple times within the same `Dict::Add()` section, it won't be detected as the test evaluates the PHP files which deduplicate the hash array, making the algorythm unaccurate.
|
|
||||||
*
|
|
||||||
* @dataProvider GetLanguagesData
|
* @dataProvider GetLanguagesData
|
||||||
*
|
*
|
||||||
* @param string $sLang
|
* @param string $sLang
|
||||||
|
|||||||
@@ -139,158 +139,6 @@ JSON;
|
|||||||
$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
|
$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(<<<JSON
|
|
||||||
{
|
|
||||||
"operation": "core/get",
|
|
||||||
"class": "UserRequest, Change",
|
|
||||||
"key": "SELECT UserRequest WHERE id=$iIdUserRequest UNION SELECT Change WHERE id=$iIdChange",
|
|
||||||
"output_fields": "id, description, outage"
|
|
||||||
}
|
|
||||||
JSON);
|
|
||||||
|
|
||||||
$sExpectedJsonOuput = <<<JSON
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "Found: 2",
|
|
||||||
"objects": {
|
|
||||||
"UserRequest::$iIdUserRequest": {
|
|
||||||
"class": "UserRequest",
|
|
||||||
"code": 0,
|
|
||||||
"fields": {
|
|
||||||
"description": "<p>$description</p>",
|
|
||||||
"id": "$iIdUserRequest"
|
|
||||||
},
|
|
||||||
"key": "$iIdUserRequest",
|
|
||||||
"message": ""
|
|
||||||
},
|
|
||||||
"Change::$iIdChange": {
|
|
||||||
"class": "Change",
|
|
||||||
"code": 0,
|
|
||||||
"fields": {
|
|
||||||
"description": "<p>$description</p>",
|
|
||||||
"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(<<<JSON
|
|
||||||
{
|
|
||||||
"operation": "core/get",
|
|
||||||
"class": "UserRequest, Change",
|
|
||||||
"key": "SELECT UR, P FROM UserRequest AS UR JOIN Person AS P ON UR.caller_id = P.id WHERE UR.id=$iIdUserRequest ",
|
|
||||||
"output_fields": "id, title, description, name, email"
|
|
||||||
}
|
|
||||||
JSON);
|
|
||||||
|
|
||||||
$sExpectedJsonOuput = <<<JSON
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "Found: 1",
|
|
||||||
"objects": [{
|
|
||||||
"UR": {
|
|
||||||
"class": "UserRequest",
|
|
||||||
"code": 0,
|
|
||||||
"fields": {
|
|
||||||
"description": "<p>$description</p>",
|
|
||||||
"id": "$iIdUserRequest",
|
|
||||||
"title": "Houston, got a problem"
|
|
||||||
},
|
|
||||||
"key": "$iIdUserRequest",
|
|
||||||
"message": ""
|
|
||||||
},
|
|
||||||
"P": {
|
|
||||||
"class": "Person",
|
|
||||||
"code": 0,
|
|
||||||
"fields": {
|
|
||||||
"email": "",
|
|
||||||
"id": "$iIdCaller",
|
|
||||||
"name": "Person_1"
|
|
||||||
},
|
|
||||||
"key": "$iIdCaller",
|
|
||||||
"message": ""
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
JSON;
|
|
||||||
$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCoreApiGetWithUnionAndDifferentOutputFields(){
|
|
||||||
// Create ticket
|
|
||||||
$description = date('dmY H:i:s');
|
|
||||||
$oUserRequest = $this->CreateSampleTicket($description);
|
|
||||||
$oChange = $this->CreateSampleTicket($description, 'Change');
|
|
||||||
$iUserRequestId = $oUserRequest->GetKey();
|
|
||||||
$sUserRequestRef = $oUserRequest->Get('ref');
|
|
||||||
$iChangeId = $oChange->GetKey();
|
|
||||||
$sChangeRef = $oChange->Get('ref');
|
|
||||||
|
|
||||||
$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
|
|
||||||
{
|
|
||||||
"operation": "core/get",
|
|
||||||
"class": "Ticket",
|
|
||||||
"key": "SELECT UserRequest WHERE id=$iUserRequestId UNION SELECT Change WHERE id=$iChangeId",
|
|
||||||
"output_fields": "Ticket:ref;UserRequest:ref,status,origin;Change:ref,status,outage"
|
|
||||||
}
|
|
||||||
JSON);
|
|
||||||
|
|
||||||
$sExpectedJsonOuput = <<<JSON
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "Found: 2",
|
|
||||||
"objects": {
|
|
||||||
"Change::$iChangeId": {
|
|
||||||
"class": "Change",
|
|
||||||
"code": 0,
|
|
||||||
"fields": {
|
|
||||||
"outage": "no",
|
|
||||||
"ref": "$sChangeRef",
|
|
||||||
"status": "new"
|
|
||||||
},
|
|
||||||
"key": "$iChangeId",
|
|
||||||
"message": ""
|
|
||||||
},
|
|
||||||
"UserRequest::$iUserRequestId": {
|
|
||||||
"class": "UserRequest",
|
|
||||||
"code": 0,
|
|
||||||
"fields": {
|
|
||||||
"origin": "phone",
|
|
||||||
"ref": "$sUserRequestRef",
|
|
||||||
"status": "new"
|
|
||||||
},
|
|
||||||
"key": "$iUserRequestId",
|
|
||||||
"message": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JSON;
|
|
||||||
$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
|
|
||||||
}
|
|
||||||
public function testCoreApiCreate()
|
public function testCoreApiCreate()
|
||||||
{
|
{
|
||||||
// Create ticket
|
// Create ticket
|
||||||
@@ -403,13 +251,12 @@ JSON;
|
|||||||
//
|
//
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private function CreateSampleTicket($description, $sType = 'UserRequest', $iIdCaller = null)
|
private function CreateSampleTicket($description)
|
||||||
{
|
{
|
||||||
$oTicket = $this->createObject($sType, [
|
$oTicket = $this->createObject('UserRequest', [
|
||||||
'org_id' => $this->getTestOrgId(),
|
'org_id' => $this->getTestOrgId(),
|
||||||
"title" => "Houston, got a problem",
|
"title" => "Houston, got a problem",
|
||||||
"description" => $description,
|
"description" => $description,
|
||||||
"caller_id" => $iIdCaller,
|
|
||||||
]);
|
]);
|
||||||
return $oTicket;
|
return $oTicket;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Combodo\iTop\Test\UnitTest\Webservices;
|
|
||||||
|
|
||||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
|
||||||
use MetaModel;
|
|
||||||
use RestUtils;
|
|
||||||
use Ticket;
|
|
||||||
use UserRequest;
|
|
||||||
|
|
||||||
|
|
||||||
class RestUtilsTest extends ItopDataTestCase
|
|
||||||
{
|
|
||||||
public function testGetFieldListForSingleClass(): void
|
|
||||||
{
|
|
||||||
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'ref,start_date,end_date'], 'output_fields');
|
|
||||||
$this->assertSame([Ticket::class => ['ref', 'start_date', 'end_date']], $aList);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetFieldListForSingleClassWithInvalidFieldNameFails(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
$this->expectExceptionMessage('output_fields: invalid attribute code \'something\' for class \'Ticket\'');
|
|
||||||
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'ref,something'], 'output_fields');
|
|
||||||
$this->assertSame([Ticket::class => ['ref', 'start_date', 'end_date']], $aList);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetFieldListWithAsteriskOnParentClass(): void
|
|
||||||
{
|
|
||||||
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => '*'], 'output_fields');
|
|
||||||
$this->assertArrayHasKey(Ticket::class, $aList);
|
|
||||||
$this->assertContains('operational_status', $aList[Ticket::class]);
|
|
||||||
$this->assertNotContains('status', $aList[Ticket::class], 'Representation of Class Ticket should not contain status, since it is defined by children');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetFieldListWithAsteriskPlusOnParentClass(): void
|
|
||||||
{
|
|
||||||
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => '*+'], 'output_fields');
|
|
||||||
$this->assertArrayHasKey(Ticket::class, $aList);
|
|
||||||
$this->assertArrayHasKey(UserRequest::class, $aList);
|
|
||||||
$this->assertContains('operational_status', $aList[Ticket::class]);
|
|
||||||
$this->assertContains('status', $aList[UserRequest::class]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetFieldListForMultipleClasses(): void
|
|
||||||
{
|
|
||||||
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'Ticket:ref,start_date,end_date;UserRequest:ref,status'], 'output_fields');
|
|
||||||
$this->assertArrayHasKey(Ticket::class, $aList);
|
|
||||||
$this->assertArrayHasKey(UserRequest::class, $aList);
|
|
||||||
$this->assertContains('ref', $aList[Ticket::class]);
|
|
||||||
$this->assertContains('end_date', $aList[Ticket::class]);
|
|
||||||
$this->assertNotContains('status', $aList[Ticket::class]);
|
|
||||||
$this->assertContains('status', $aList[UserRequest::class]);
|
|
||||||
$this->assertNotContains('end_date', $aList[UserRequest::class]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetFieldListForMultipleClassesWithInvalidFieldNameFails(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
$this->expectExceptionMessage('output_fields: invalid attribute code \'something\'');
|
|
||||||
RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'Ticket:ref;UserRequest:ref,something'], 'output_fields');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function testGetFieldListForMultipleClassesWithInvalidFieldName(): void
|
|
||||||
{
|
|
||||||
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'ref, something'], 'output_fields', false);
|
|
||||||
$this->assertContains('ref', $aList[Ticket::class]);
|
|
||||||
$this->assertNotContains('something', $aList[Ticket::class]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function extendedOutputDataProvider(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[false, 'ref,start_date,end_date'],
|
|
||||||
[false, '*'],
|
|
||||||
[true, '*+'],
|
|
||||||
[false, 'Ticket:ref'],
|
|
||||||
[true, 'Ticket:ref;UserRequest:ref'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider extendedOutputDataProvider
|
|
||||||
*/
|
|
||||||
public function testIsExtendedOutputRequest(bool $bExpected, string $sFields): void
|
|
||||||
{
|
|
||||||
$this->assertSame($bExpected, RestUtils::HasRequestedExtendedOutput($sFields));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function allFieldsOutputDataProvider(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[false, 'ref,start_date,end_date'],
|
|
||||||
[true, '*'],
|
|
||||||
[true, '*+'],
|
|
||||||
[false, 'Ticket:ref'],
|
|
||||||
[false, 'Ticket:ref;UserRequest:ref'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider allFieldsOutputDataProvider
|
|
||||||
*/
|
|
||||||
public function testIsAllFieldsOutputRequest(bool $bExpected, string $sFields): void
|
|
||||||
{
|
|
||||||
$this->assertSame($bExpected, RestUtils::HasRequestedAllOutputFields($sFields));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user