From 7b2789479d53146990d4be52ef767f8bc1f94f59 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Wed, 30 Jan 2013 09:46:55 +0000 Subject: [PATCH] REST services: alpha2. It is now extensible (implement iRestServiceProvider). Still lacks two verbs: apply_stimulus and delete. SVN:trunk[2592] --- application/applicationextension.inc.php | 422 +++++++++++++++++++ core/restservices.class.inc.php | 253 ++++++++++++ webservices/itoprest.examples.php | 19 +- webservices/rest.php | 499 +++++------------------ 4 files changed, 786 insertions(+), 407 deletions(-) create mode 100644 core/restservices.class.inc.php diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index 28b93d9dd..f33099179 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -526,3 +526,425 @@ interface iPageUIExtension public function GetBannerHtml(iTopWebPage $oPage); } +/** + * Implement this interface to add new operations to the REST/JSON web service + * + * @package Extensibility + * @api + * @since 2.0.1 + */ +interface iRestServiceProvider +{ + /** + * Enumerate services delivered by this class + * @param string $sVersion The version (e.g. 1.0) supported by the services + * @return array An array of hash 'verb' => verb, 'description' => description + */ + public function ListOperations($sVersion); + /** + * Enumerate services delivered by this class + * @param string $sVersion The version (e.g. 1.0) supported by the services + * @return RestResult The standardized result structure (at least a message) + * @throws Exception in case of internal failure. + */ + public function ExecOperation($sVersion, $sVerb, $aParams); +} + +/** + * Minimal REST response structure. Derive this structure to add response data and error codes. + * + * @package Extensibility + * @api + * @since 2.0.1 + */ +class RestResult +{ + /** + * Result: no issue has been encountered + */ + const OK = 0; + /** + * Result: missing/wrong credentials or the user does not have enough rights to perform the requested operation + */ + const UNAUTHORIZED = 1; + /** + * Result: the parameter 'version' is missing + */ + const MISSING_VERSION = 2; + /** + * Result: the parameter 'json_data' is missing + */ + const MISSING_JSON = 3; + /** + * Result: the input structure is not a valid JSON string + */ + const INVALID_JSON = 4; + /** + * Result: no operation is available for the specified version + */ + const UNSUPPORTED_VERSION = 10; + /** + * Result: the requested operation is not valid for the specified version + */ + const UNKNOWN_OPERATION = 11; + /** + * Result: the operation could not be performed, see the message for troubleshooting + */ + const INTERNAL_ERROR = 100; + + /** + * Default constructor - ok! + * + * @param DBObject $oObject The object being reported + * @param string $sAttCode The attribute code (must be valid) + * @return string A scalar representation of the value + */ + public function __construct() + { + $this->code = RestResult::OK; + } + + public $code; + public $message; +} + +/** + * Helpers for implementing REST services + * + * @package Extensibility + * @api + */ +class RestUtils +{ + /** + * Registering tracking information. Any further object modification be associated with the given comment, when the modification gets recorded into the DB + * + * @param StdClass $oData Structured input data. Must contain 'comment'. + * @return void + * @throws Exception + * @api + */ + public static function InitTrackingComment($oData) + { + $sComment = self::GetMandatoryParam($oData, 'comment'); + CMDBObject::SetTrackInfo($sComment); + } + + /** + * Read a mandatory parameter from from a Rest/Json structure. + * + * @param StdClass $oData Structured input data. Must contain the entry defined by sParamName. + * @param string $sParamName Name of the parameter to fetch from the input data + * @return void + * @throws Exception If the parameter is missing + * @api + */ + public static function GetMandatoryParam($oData, $sParamName) + { + if (isset($oData->$sParamName)) + { + return $oData->$sParamName; + } + else + { + throw new Exception("Missing parameter '$sParamName'"); + } + } + + + /** + * Read an optional parameter from from a Rest/Json structure. + * + * @param StdClass $oData Structured input data. + * @param string $sParamName Name of the parameter to fetch from the input data + * @param mixed $default Default value if the parameter is not found in the input data + * @return void + * @throws Exception + * @api + */ + public static function GetOptionalParam($oData, $sParamName, $default) + { + if (isset($oData->$sParamName)) + { + return $oData->$sParamName; + } + else + { + return $default; + } + } + + + /** + * Read a class from a Rest/Json structure. + * + * @param StdClass $oData Structured input data. Must contain the entry defined by sParamName. + * @param string $sParamName Name of the parameter to fetch from the input data + * @return void + * @throws Exception If the parameter is missing or the class is unknown + * @api + */ + public static function GetClass($oData, $sParamName) + { + $sClass = self::GetMandatoryParam($oData, $sParamName); + if (!MetaModel::IsValidClass($sClass)) + { + throw new Exception("$sParamName: '$sClass' is not a valid class'"); + } + return $sClass; + } + + + /** + * Read a list of attribute codes from a Rest/Json structure. + * + * @param string $sClass Name of the class + * @param StdClass $oData Structured input data. + * @param string $sParamName Name of the parameter to fetch from the input data + * @return void + * @throws Exception + * @api + */ + public static function GetFieldList($sClass, $oData, $sParamName) + { + $sFields = self::GetOptionalParam($oData, $sParamName, '*'); + $aShowFields = array(); + if ($sFields == '*') + { + foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + $aShowFields[] = $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[] = $sAttCode; + } + } + return $aShowFields; + } + + /** + * Read and interpret object search criteria from a Rest/Json structure + * + * @param string $sClass Name of the class + * @param StdClass $oCriteria Hash of attribute code => value (can be a substructure or a scalar, depending on the nature of the attriute) + * @return object The object found + * @throws Exception If the input structure is not valid or it could not find exactly one object + */ + protected static function FindObjectFromCriteria($sClass, $oCriteria) + { + $aCriteriaReport = array(); + if (isset($oCriteria->finalclass)) + { + $sClass = $oCriteria->finalclass; + if (!MetaModel::IsValidClass($sClass)) + { + throw new Exception("finalclass: Unknown class '$sClass'"); + } + } + $oSearch = new DBObjectSearch($sClass); + foreach ($oCriteria as $sAttCode => $value) + { + $realValue = self::MakeValue($sClass, $sAttCode, $value); + $oSearch->AddCondition($sAttCode, $realValue); + $aCriteriaReport[] = "$sAttCode: $value ($realValue)"; + } + $oSet = new DBObjectSet($oSearch); + $iCount = $oSet->Count(); + if ($iCount == 0) + { + throw new Exception("No item found with criteria: ".implode(', ', $aCriteriaReport)); + } + elseif ($iCount > 1) + { + throw new Exception("Several items found ($iCount) with criteria: ".implode(', ', $aCriteriaReport)); + } + $res = $oSet->Fetch(); + return $res; + } + + + /** + * Find an object from a polymorph search specification (Rest/Json) + * + * @param string $sClass Name of the class + * @param mixed $key Either search criteria (substructure), or an object or an OQL string. + * @return DBObject The object found + * @throws Exception If the input structure is not valid or it could not find exactly one object + * @api + */ + public static function FindObjectFromKey($sClass, $key) + { + if (is_object($key)) + { + $res = self::FindObjectFromCriteria($sClass, $key); + } + elseif (is_numeric($key)) + { + $res = MetaModel::GetObject($sClass, $key); + } + elseif (is_string($key)) + { + // OQL + $oSearch = DBObjectSearch::FromOQL($key); + $oSet = new DBObjectSet($oSearch); + $iCount = $oSet->Count(); + if ($iCount == 0) + { + throw new Exception("No item found for query: $key"); + } + elseif ($iCount > 1) + { + throw new Exception("Several items found ($iCount) for query: $key"); + } + $res = $oSet->Fetch(); + } + else + { + throw new Exception("Wrong format for key"); + } + return $res; + } + + /** + * Search objects from a polymorph search specification (Rest/Json) + * + * @param string $sClass Name of the class + * @param mixed $key Either search criteria (substructure), or an object or an OQL string. + * @return DBObjectSet The search result set + * @throws Exception If the input structure is not valid + */ + public static function GetObjectSetFromKey($sClass, $key) + { + if (is_object($key)) + { + if (isset($oCriteria->finalclass)) + { + $sClass = $oCriteria->finalclass; + if (!MetaModel::IsValidClass($sClass)) + { + throw new Exception("finalclass: Unknown class '$sClass'"); + } + } + + $oSearch = new DBObjectSearch($sClass); + foreach ($key as $sAttCode => $value) + { + $realValue = self::MakeValue($sClass, $sAttCode, $value); + $oSearch->AddCondition($sAttCode, $realValue); + } + } + elseif (is_numeric($key)) + { + $oSearch = new DBObjectSearch($sClass); + $oSearch->AddCondition('id', $key); + } + elseif (is_string($key)) + { + // OQL + $oSearch = DBObjectSearch::FromOQL($key); + $oObjectSet = new DBObjectSet($oSearch); + } + else + { + throw new Exception("Wrong format for key"); + } + $oObjectSet = new DBObjectSet($oSearch); + return $oObjectSet; + } + + /** + * Interpret the Rest/Json value and get a valid attribute value + * + * @param string $sClass Name of the class + * @param string $sAttCode Attribute code + * @param mixed $value Depending on the type of attribute (a scalar, or search criteria, or list of related objects...) + * @return mixed The value that can be used with DBObject::Set() + * @throws Exception If the specification of the value is not valid. + * @api + */ + public static function MakeValue($sClass, $sAttCode, $value) + { + try + { + if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + throw new Exception("Unknown attribute"); + } + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef instanceof AttributeExternalKey) + { + $oExtKeyObject = self::FindObjectFromKey($oAttDef->GetTargetClass(), $value); + $value = $oExtKeyObject->GetKey(); + } + elseif ($oAttDef instanceof AttributeLinkedSet) + { + if (!is_array($value)) + { + throw new Exception("A link set must be defined by an array of objects"); + } + $sLnkClass = $oAttDef->GetLinkedClass(); + $aLinks = array(); + foreach($value as $oValues) + { + $oLnk = self::MakeObjectFromFields($sLnkClass, $oValues); + $aLinks[] = $oLnk; + } + $value = DBObjectSet::FromArray($sLnkClass, $aLinks); + } + } + catch (Exception $e) + { + throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode()); + } + return $value; + } + + /** + * Interpret a Rest/Json structure that defines attribute values, and build an object + * + * @param string $sClass Name of the class + * @param array $aFields A hash of attribute code => value specification. + * @return DBObject The newly created object + * @throws Exception If the specification of the values is not valid + * @api + */ + public static function MakeObjectFromFields($sClass, $aFields) + { + $oObject = MetaModel::NewObject($sClass); + foreach ($aFields as $sAttCode => $value) + { + $realValue = self::MakeValue($sClass, $sAttCode, $value); + $oObject->Set($sAttCode, $realValue); + } + return $oObject; + } + + /** + * Interpret a Rest/Json structure that defines attribute values, and update the given object + * + * @param DBObject $oObject The object being modified + * @param array $aFields A hash of attribute code => value specification. + * @return DBObject The object modified + * @throws Exception If the specification of the values is not valid + * @api + */ + public static function UpdateObjectFromFields($oObject, $aFields) + { + $sClass = get_class($oObject); + foreach ($aFields as $sAttCode => $value) + { + $realValue = self::MakeValue($sClass, $sAttCode, $value); + $oObject->Set($sAttCode, $realValue); + } + return $oObject; + } +} diff --git a/core/restservices.class.inc.php b/core/restservices.class.inc.php new file mode 100644 index 000000000..2636b4cff --- /dev/null +++ b/core/restservices.class.inc.php @@ -0,0 +1,253 @@ + + +/** + * REST/json services + * + * Definition of common structures + the very minimum service provider (manage objects) + * + * @package REST Services + * @copyright Copyright (C) 2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @api + */ + +/** + * Element of the response formed by RestResultWithObjects + * + * @package REST Services + */ +class ObjectResult +{ + public $code; + public $message; + public $fields; + + /** + * Default constructor + */ + public function __construct() + { + $this->code = RestResult::OK; + $this->message = ''; + $this->fields = array(); + } + + /** + * Helper to make an output value for a given attribute + * + * @param DBObject $oObject The object being reported + * @param string $sAttCode The attribute code (must be valid) + * @return string A scalar representation of the value + */ + protected function MakeResultValue(DBObject $oObject, $sAttCode) + { + if ($sAttCode == 'id') + { + $value = $oObject->GetKey(); + } + else + { + $sClass = get_class($oObject); + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef instanceof AttributeLinkedSet) + { + $value = array(); + + // Make the list of required attributes + // - Skip attributes pointing to the current object (redundant data) + // - Skip link sets refering to the current data (infinite recursion!) + $aRelevantAttributes = array(); + $sLnkClass = $oAttDef->GetLinkedClass(); + foreach (MetaModel::ListAttributeDefs($sLnkClass) as $sLnkAttCode => $oLnkAttDef) + { + // Skip any attribute of the link that points to the current object + // + if ($sLnkAttCode == $oAttDef->GetExtKeyToMe()) continue; + if (method_exists($oLnkAttDef, 'GetKeyAttCode')) + { + if ($oLnkAttDef->GetKeyAttCode() ==$oAttDef->GetExtKeyToMe()) continue; + } + + $aRelevantAttributes[] = $sLnkAttCode; + } + + // Iterate on the set and build an array of array of attcode=>value + $oSet = $oObject->Get($sAttCode); + while ($oLnk = $oSet->Fetch()) + { + $aLnkValues = array(); + foreach ($aRelevantAttributes as $sLnkAttCode) + { + $aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode); + } + $value[] = $aLnkValues; + } + } + else + { + $value = $oObject->GetEditValue($sAttCode); + } + } + return $value; + } + + /** + * Report the value for the given object attribute + * + * @param DBObject $oObject The object being reported + * @param string $sAttCode The attribute code (must be valid) + * @return void + */ + public function AddField(DBObject $oObject, $sAttCode) + { + $this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode); + } +} + + + +/** + * REST response for services managing objects. Derive this structure to add information and/or constants + * + * @package Extensibility + * @package REST Services + * @api + */ +class RestResultWithObjects extends RestResult +{ + public $objects; + + /** + * Report the given object + * + * @param int 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 $aFields An array of attribute codes. List of the attributes to be reported. + * @return void + */ + public function AddObject($iCode, $sMessage, $oObject = null, $aFields = null) + { + $oObjRes = new ObjectResult(); + $oObjRes->code = $iCode; + $oObjRes->message = $sMessage; + + if ($oObject) + { + foreach ($aFields as $sAttCode) + { + $oObjRes->AddField($oObject, $sAttCode); + } + } + + $this->objects[] = $oObjRes; + } +} + + +/** + * Implementation of core REST services (create/get/update... objects) + * + * @package Core + */ +class CoreServices implements iRestServiceProvider +{ + /** + * Enumerate services delivered by this class + * + * @param string $sVersion The version (e.g. 1.0) supported by the services + * @return array An array of hash 'verb' => verb, 'description' => description + */ + public function ListOperations($sVersion) + { + $aOps = array(); + if ($sVersion == '1.0') + { + $aOps[] = array( + 'verb' => 'core/create', + 'description' => 'Create an object' + ); + $aOps[] = array( + 'verb' => 'core/update', + 'description' => 'Update an object' + ); + $aOps[] = array( + 'verb' => 'core/get', + 'description' => 'Search for objects' + ); + } + return $aOps; + } + + /** + * Enumerate services delivered by this class + * @param string $sVersion The version (e.g. 1.0) supported by the services + * @return RestResult The standardized result structure (at least a message) + * @throws Exception in case of internal failure. + */ + public function ExecOperation($sVersion, $sVerb, $aParams) + { + $oResult = new RestResultWithObjects(); + switch ($sVerb) + { + case 'core/create': + RestUtils::InitTrackingComment($aParams); + $sClass = RestUtils::GetClass($aParams, 'class'); + $aFields = RestUtils::GetMandatoryParam($aParams, 'fields'); + $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); + + $oObject = RestUtils::MakeObjectFromFields($sClass, $aFields); + $oObject->DBInsert(); + + $oResult->AddObject(0, 'created', $oObject, $aShowFields); + break; + + case 'core/update': + RestUtils::InitTrackingComment($aParams); + $sClass = RestUtils::GetClass($aParams, 'class'); + $key = RestUtils::GetMandatoryParam($aParams, 'key'); + $aFields = RestUtils::GetMandatoryParam($aParams, 'fields'); + $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); + + $oObject = RestUtils::FindObjectFromKey($sClass, $key); + RestUtils::UpdateObjectFromFields($oObject, $aFields); + $oObject->DBUpdate(); + + $oResult->AddObject(0, 'updated', $oObject, $aShowFields); + break; + + case 'core/get': + $sClass = RestUtils::GetClass($aParams, 'class'); + $key = RestUtils::GetMandatoryParam($aParams, 'key'); + $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); + + $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key); + while ($oObject = $oObjectSet->Fetch()) + { + $oResult->AddObject(0, '', $oObject, $aShowFields); + } + $oResult->message = "Found: ".$oObjectSet->Count(); + break; + + default: + // unknown operation: handled at a higher level + } + return $oResult; + } +} diff --git a/webservices/itoprest.examples.php b/webservices/itoprest.examples.php index 3c92cc1aa..eb27ac30c 100644 --- a/webservices/itoprest.examples.php +++ b/webservices/itoprest.examples.php @@ -92,10 +92,13 @@ function DoPostRequest_curl($sUrl, $aData) $aOperations = array( array( - 'operation' => 'object_create', // operation code + 'operation' => 'list_operations', // operation code + ), + array( + 'operation' => 'core/create', // operation code 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log 'class' => 'UserRequest', - 'results' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c) + 'output_fields' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c) // Values for the object to create 'fields' => array( 'org_id' => "SELECT Organization WHERE name = 'Demo'", @@ -105,11 +108,11 @@ $aOperations = array( ), ), array( - 'operation' => 'object_update', // operation code + 'operation' => 'core/update', // operation code 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log 'class' => 'UserRequest', 'key' => 'SELECT UserRequest WHERE id=1', - 'results' => 'id, friendlyname, title', // list of fields to show in the results (* or a,b,c) + 'output_fields' => 'id, friendlyname, title', // list of fields to show in the results (* or a,b,c) // Values for the object to create 'fields' => array( 'title' => 'Issue #'.rand(0, 100), @@ -122,15 +125,14 @@ $aOperations = array( ), ), array( - 'operation' => 'object_get', // operation code + 'operation' => 'core/get', // operation code 'class' => 'UserRequest', 'key' => 'SELECT UserRequest', - 'results' => 'id, friendlyname, title, contacts_list', // list of fields to show in the results (* or a,b,c) + 'output_fields' => 'id, friendlyname, title, contacts_list', // list of fields to show in the results (* or a,b,c) ), ); - -$sUrl = "http://localhost/rest-services/webservices/rest.php?version=0.9"; +$sUrl = "http://localhost/rest-services/webservices/rest.php?version=1.0"; $aData = array(); $aData['auth_user'] = 'admin'; @@ -148,6 +150,7 @@ foreach ($aOperations as $iOp => $aOperation) $response = DoPostRequest($sUrl, $aData); $aResults = json_decode($response); + $aResults = $response; if ($aResults) { echo "--------------------------------------\n"; diff --git a/webservices/rest.php b/webservices/rest.php index 0069f4426..51303dc03 100644 --- a/webservices/rest.php +++ b/webservices/rest.php @@ -61,372 +61,35 @@ if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); require_once(__DIR__.'/../approot.inc.php'); require_once(APPROOT.'/application/application.inc.php'); -require_once(APPROOT.'/application/clipage.class.inc.php'); +require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); require_once(APPROOT.'/application/startup.inc.php'); +require_once(APPROOT.'core/restservices.class.inc.php'); -class RestServices +/** + * Result structure that is specific to the hardcoded verb 'list_operations' + */ +class RestResultListOperations extends RestResult { - public function InitTrackingComment($oData) + public $version; + public $operations; + + public function AddOperation($sVerb, $sDescription, $sServiceProviderClass) { - $sComment = $this->GetMandatoryParam($oData, 'comment'); - CMDBObject::SetTrackInfo($sComment); - } - - - public function GetMandatoryParam($oData, $sParamName) - { - if (isset($oData->$sParamName)) - { - return $oData->$sParamName; - } - else - { - throw new Exception("Missing parameter '$sParamName'"); - } - } - - - public function GetOptionalParam($oData, $sParamName, $default) - { - if (isset($oData->$sParamName)) - { - return $oData->$sParamName; - } - else - { - return $default; - } - } - - - public function GetClass($oData, $sParamName) - { - $sClass = $this->GetMandatoryParam($oData, $sParamName); - if (!MetaModel::IsValidClass($sClass)) - { - throw new Exception("$sParamName: '$sClass' is not a valid class'"); - } - return $sClass; - } - - - public function GetFieldList($sClass, $oData, $sParamName) - { - $sFields = $this->GetOptionalParam($oData, $sParamName, '*'); - $aShowFields = array(); - if ($sFields == '*') - { - foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - $aShowFields[] = $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[] = $sAttCode; - } - } - return $aShowFields; - } - - protected function FindObjectFromCriteria($sClass, $oCriteria) - { - $aCriteriaReport = array(); - if (isset($oCriteria->finalclass)) - { - $sClass = $oCriteria->finalclass; - if (!MetaModel::IsValidClass($sClass)) - { - throw new Exception("finalclass: Unknown class '$sClass'"); - } - } - $oSearch = new DBObjectSearch($sClass); - foreach ($oCriteria as $sAttCode => $value) - { - $realValue = $this->MakeValue($sClass, $sAttCode, $value); - $oSearch->AddCondition($sAttCode, $realValue); - $aCriteriaReport[] = "$sAttCode: $value ($realValue)"; - } - $oSet = new DBObjectSet($oSearch); - $iCount = $oSet->Count(); - if ($iCount == 0) - { - throw new Exception("No item found for criteria: ".implode(', ', $aCriteriaReport)); - } - elseif ($iCount > 1) - { - throw new Exception("Several items found ($iCount) for criteria: ".implode(', ', $aCriteriaReport)); - } - $res = $oSet->Fetch(); - return $res; - } - - - public function FindObjectFromKey($sClass, $key) - { - if (is_object($key)) - { - $res = $this->FindObjectFromCriteria($sClass, $key); - } - elseif (is_numeric($key)) - { - $res = MetaModel::GetObject($sClass, $key); - } - elseif (is_string($key)) - { - // OQL - $oSearch = DBObjectSearch::FromOQL($key); - $oSet = new DBObjectSet($oSearch); - $iCount = $oSet->Count(); - if ($iCount == 0) - { - throw new Exception("No item found for query: $key"); - } - elseif ($iCount > 1) - { - throw new Exception("Several items found ($iCount) for query: $key"); - } - $res = $oSet->Fetch(); - } - else - { - throw new Exception("Wrong format for key"); - } - return $res; - } - - - public function GetObjectSetFromKey($sClass, $key) - { - if (is_object($key)) - { - if (isset($oCriteria->finalclass)) - { - $sClass = $oCriteria->finalclass; - if (!MetaModel::IsValidClass($sClass)) - { - throw new Exception("finalclass: Unknown class '$sClass'"); - } - } - - $oSearch = new DBObjectSearch($sClass); - foreach ($key as $sAttCode => $value) - { - $realValue = $this->MakeValue($sClass, $sAttCode, $value); - $oSearch->AddCondition($sAttCode, $realValue); - } - } - elseif (is_numeric($key)) - { - $oSearch = new DBObjectSearch($sClass); - $oSearch->AddCondition('id', $key); - } - elseif (is_string($key)) - { - // OQL - $oSearch = DBObjectSearch::FromOQL($key); - $oObjectSet = new DBObjectSet($oSearch); - } - else - { - throw new Exception("Wrong format for key"); - } - $oObjectSet = new DBObjectSet($oSearch); - return $oObjectSet; - } - - - protected function MakeValue($sClass, $sAttCode, $value) - { - try - { - if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) - { - throw new Exception("Unknown attribute"); - } - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef instanceof AttributeExternalKey) - { - $oExtKeyObject = $this->FindObjectFromKey($oAttDef->GetTargetClass(), $value); - $value = $oExtKeyObject->GetKey(); - } - elseif ($oAttDef instanceof AttributeLinkedSet) - { - if (!is_array($value)) - { - throw new Exception("A link set must be defined by an array of objects"); - } - $sLnkClass = $oAttDef->GetLinkedClass(); - $aLinks = array(); - foreach($value as $oValues) - { - $oLnk = $this->MakeObjectFromFields($sLnkClass, $oValues); - $aLinks[] = $oLnk; - } - $value = DBObjectSet::FromArray($sLnkClass, $aLinks); - } - } - catch (Exception $e) - { - throw new Exception("$sAttCode: ".$e->getMessage()); - } - return $value; - } - - - public function MakeObjectFromFields($sClass, $aFields) - { - $oObject = MetaModel::NewObject($sClass); - foreach ($aFields as $sAttCode => $value) - { - $realValue = $this->MakeValue($sClass, $sAttCode, $value); - $oObject->Set($sAttCode, $realValue); - } - return $oObject; - } - - - public function UpdateObjectFromFields($oObject, $aFields) - { - $sClass = get_class($oObject); - foreach ($aFields as $sAttCode => $value) - { - $realValue = $this->MakeValue($sClass, $sAttCode, $value); - $oObject->Set($sAttCode, $realValue); - } - return $oObject; + $this->operations[] = array( + 'verb' => $sVerb, + 'description' => $sDescription, + 'extension' => $sServiceProviderClass + ); } } -class FieldResult -{ - protected $value; - - public function __construct() - { - } - - public function GetValue() - { - } -} - -class ObjectResult -{ - public $code; - public $message; - public $fields; - - public function __construct() - { - $this->code = 0; - $this->message = ''; - $this->fields = array(); - } - - protected function MakeResultValue($oObject, $sAttCode) - { - if ($sAttCode == 'id') - { - $value = $oObject->GetKey(); - } - else - { - $sClass = get_class($oObject); - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef instanceof AttributeLinkedSet) - { - $value = array(); - - // Make the list of required attributes - // - Skip attributes pointing to the current object (redundant data) - // - Skip link sets refering to the current data (infinite recursion!) - $aRelevantAttributes = array(); - $sLnkClass = $oAttDef->GetLinkedClass(); - foreach (MetaModel::ListAttributeDefs($sLnkClass) as $sLnkAttCode => $oLnkAttDef) - { - // Skip any attribute of the link that points to the current object - // - if ($sLnkAttCode == $oAttDef->GetExtKeyToMe()) continue; - if (method_exists($oLnkAttDef, 'GetKeyAttCode')) - { - if ($oLnkAttDef->GetKeyAttCode() ==$oAttDef->GetExtKeyToMe()) continue; - } - - $aRelevantAttributes[] = $sLnkAttCode; - } - - // Iterate on the set and build an array of array of attcode=>value - $oSet = $oObject->Get($sAttCode); - while ($oLnk = $oSet->Fetch()) - { - $aLnkValues = array(); - foreach ($aRelevantAttributes as $sLnkAttCode) - { - $aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode); - } - $value[] = $aLnkValues; - } - } - else - { - $value = $oObject->GetEditValue($sAttCode); - } - } - return $value; - } - - public function AddField($oObject, $sAttCode) - { - $this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode); - } -} - -class RestResult -{ - public function __construct() - { - } - - public $code; - public $message; - public $objects; - - public function AddObject($iCode, $sMessage, $oObject = null, $aFields = null) - { - $oObjRes = new ObjectResult(); - $oObjRes->code = $iCode; - $oObjRes->message = $sMessage; - - if ($oObject) - { - foreach ($aFields as $sAttCode) - { - $oObjRes->AddField($oObject, $sAttCode); - } - } - - $this->objects[] = $oObjRes; - } -} - - - //////////////////////////////////////////////////////////////////////////////// // // Main // -$oP = new CLIPage("iTop - REST"); -$oResult = new RestResult(); +$oP = new ajax_page('rest'); try { @@ -440,69 +103,107 @@ try } else { - throw new Exception("Invalid login '$sAuthUser'"); + throw new Exception("Invalid login '$sAuthUser'", RestResult::UNAUTHORIZED); + } + + $sVersion = utils::ReadParam('version', null, false, 'raw_data'); + if ($sVersion == null) + { + throw new Exception("Missing parameter 'version' (e.g. '1.0')", RestResult::MISSING_VERSION); } - $aJsonData = json_decode(utils::ReadPostedParam('json_data', null, 'raw_data')); + $sJsonString = utils::ReadPostedParam('json_data', null, 'raw_data'); + if ($sJsonString == null) + { + throw new Exception("Missing parameter 'json_data", RestResult::MISSING_JSON); + } + $aJsonData = json_decode($sJsonString); if ($aJsonData == null) { - throw new Exception('Parameter json_data is not a valid JSON structure'); + throw new Exception("Parameter json_data is not a valid JSON structure", RestResult::INVALID_JSON); } - $oRS = new RestServices(); - $sOperation = $oRS->GetMandatoryParam($aJsonData, 'operation'); - switch ($sOperation) + $aProviders = array(); + foreach(get_declared_classes() as $sPHPClass) { - case 'object_create': - $oRS->InitTrackingComment($aJsonData); - $sClass = $oRS->GetClass($aJsonData, 'class'); - $aFields = $oRS->GetMandatoryParam($aJsonData, 'fields'); - $aShowFields = $oRS->GetFieldList($sClass, $aJsonData, 'results'); - - $oObject = $oRS->MakeObjectFromFields($sClass, $aFields); - $oObject->DBInsert(); - - $oResult->AddObject(0, 'created', $oObject, $aShowFields); - break; - - case 'object_update': - $oRS->InitTrackingComment($aJsonData); - $sClass = $oRS->GetClass($aJsonData, 'class'); - $key = $oRS->GetMandatoryParam($aJsonData, 'key'); - $aFields = $oRS->GetMandatoryParam($aJsonData, 'fields'); - $aShowFields = $oRS->GetFieldList($sClass, $aJsonData, 'results'); - - $oObject = $oRS->FindObjectFromKey($sClass, $key); - $oRS->UpdateObjectFromFields($oObject, $aFields); - $oObject->DBUpdate(); - - $oResult->AddObject(0, 'updated', $oObject, $aShowFields); - break; - - case 'object_get': - $sClass = $oRS->GetClass($aJsonData, 'class'); - $key = $oRS->GetMandatoryParam($aJsonData, 'key'); - $aShowFields = $oRS->GetFieldList($sClass, $aJsonData, 'results'); - - $oObjectSet = $oRS->GetObjectSetFromKey($sClass, $key); - while ($oObject = $oObjectSet->Fetch()) + $oRefClass = new ReflectionClass($sPHPClass); + if ($oRefClass->implementsInterface('iRestServiceProvider')) { - $oResult->AddObject(0, '', $oObject, $aShowFields); + $aProviders[] = new $sPHPClass; } - $oResult->message = "Found: ".$oObjectSet->Count(); - break; + } - default: - throw new Exception("Uknown operation '$sOperation'"); + $aOpToRestService = array(); // verb => $oRestServiceProvider + foreach ($aProviders as $oRestSP) + { + $aOperations = $oRestSP->ListOperations($sVersion); + foreach ($aOperations as $aOpData) + { + $aOpToRestService[$aOpData['verb']] = array + ( + 'service_provider' => $oRestSP, + 'description' => $aOpData['description'], + ); + } + } + + if (count($aOpToRestService) == 0) + { + throw new Exception("There is no service available for version '$sVersion'", RestResult::UNSUPPORTED_VERSION); + } + + + $sOperation = RestUtils::GetMandatoryParam($aJsonData, 'operation'); + if ($sOperation == 'list_operations') + { + $oResult = new RestResultListOperations(); + $oResult->message = "Operations: ".count($aOpToRestService); + $oResult->version = $sVersion; + foreach ($aOpToRestService as $sVerb => $aOpData) + { + $oResult->AddOperation($sVerb, $aOpData['description'], get_class($aOpData['service_provider'])); + } + } + else + { + if (!array_key_exists($sOperation, $aOpToRestService)) + { + throw new Exception("Unknown verb '$sVersion'", RestResult::UNKNOWN_OPERATION); + } + $oRS = $aOpToRestService[$sOperation]['service_provider']; + + $oResult = $oRS->ExecOperation($sVersion, $sOperation, $aJsonData); } } catch(Exception $e) { - $oResult->code = 1234; + $oResult = new RestResult(); + if ($e->GetCode() == 0) + { + $oResult->code = RestResult::INTERNAL_ERROR; + } + else + { + $oResult->code = $e->GetCode(); + } $oResult->message = "Error: ".$e->GetMessage(); } -$oP->add(json_encode($oResult)); +// Output the results +// +$oP->add_header('Access-Control-Allow-Origin: *'); + +$sCallback = utils::ReadParam('callback', null); +if ($sCallback == null) +{ + $oP->SetContentType('application/json'); + $oP->add(json_encode($oResult)); +} +else +{ + $oP->SetContentType('application/javascript'); + $oP->add($sCallback.'('.json_encode($oResult).')'); +} $oP->Output(); ?> \ No newline at end of file