restservices.class.inc.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <?php
  2. // Copyright (C) 2013 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * REST/json services
  20. *
  21. * Definition of common structures + the very minimum service provider (manage objects)
  22. *
  23. * @package REST Services
  24. * @copyright Copyright (C) 2013 Combodo SARL
  25. * @license http://opensource.org/licenses/AGPL-3.0
  26. * @api
  27. */
  28. /**
  29. * Element of the response formed by RestResultWithObjects
  30. *
  31. * @package REST Services
  32. */
  33. class ObjectResult
  34. {
  35. public $code;
  36. public $message;
  37. public $fields;
  38. /**
  39. * Default constructor
  40. */
  41. public function __construct()
  42. {
  43. $this->code = RestResult::OK;
  44. $this->message = '';
  45. $this->fields = array();
  46. }
  47. /**
  48. * Helper to make an output value for a given attribute
  49. *
  50. * @param DBObject $oObject The object being reported
  51. * @param string $sAttCode The attribute code (must be valid)
  52. * @return string A scalar representation of the value
  53. */
  54. protected function MakeResultValue(DBObject $oObject, $sAttCode)
  55. {
  56. if ($sAttCode == 'id')
  57. {
  58. $value = $oObject->GetKey();
  59. }
  60. else
  61. {
  62. $sClass = get_class($oObject);
  63. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  64. if ($oAttDef instanceof AttributeLinkedSet)
  65. {
  66. $value = array();
  67. // Make the list of required attributes
  68. // - Skip attributes pointing to the current object (redundant data)
  69. // - Skip link sets refering to the current data (infinite recursion!)
  70. $aRelevantAttributes = array();
  71. $sLnkClass = $oAttDef->GetLinkedClass();
  72. foreach (MetaModel::ListAttributeDefs($sLnkClass) as $sLnkAttCode => $oLnkAttDef)
  73. {
  74. // Skip any attribute of the link that points to the current object
  75. //
  76. if ($sLnkAttCode == $oAttDef->GetExtKeyToMe()) continue;
  77. if (method_exists($oLnkAttDef, 'GetKeyAttCode'))
  78. {
  79. if ($oLnkAttDef->GetKeyAttCode() ==$oAttDef->GetExtKeyToMe()) continue;
  80. }
  81. $aRelevantAttributes[] = $sLnkAttCode;
  82. }
  83. // Iterate on the set and build an array of array of attcode=>value
  84. $oSet = $oObject->Get($sAttCode);
  85. while ($oLnk = $oSet->Fetch())
  86. {
  87. $aLnkValues = array();
  88. foreach ($aRelevantAttributes as $sLnkAttCode)
  89. {
  90. $aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode);
  91. }
  92. $value[] = $aLnkValues;
  93. }
  94. }
  95. elseif ($oAttDef->IsExternalKey())
  96. {
  97. $value = $oObject->Get($sAttCode);
  98. }
  99. else
  100. {
  101. // Still to be refined...
  102. $value = $oObject->GetEditValue($sAttCode);
  103. }
  104. }
  105. return $value;
  106. }
  107. /**
  108. * Report the value for the given object attribute
  109. *
  110. * @param DBObject $oObject The object being reported
  111. * @param string $sAttCode The attribute code (must be valid)
  112. * @return void
  113. */
  114. public function AddField(DBObject $oObject, $sAttCode)
  115. {
  116. $this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode);
  117. }
  118. }
  119. /**
  120. * REST response for services managing objects. Derive this structure to add information and/or constants
  121. *
  122. * @package Extensibility
  123. * @package REST Services
  124. * @api
  125. */
  126. class RestResultWithObjects extends RestResult
  127. {
  128. public $objects;
  129. /**
  130. * Report the given object
  131. *
  132. * @param int An error code (RestResult::OK is no issue has been found)
  133. * @param string $sMessage Description of the error if any, an empty string otherwise
  134. * @param DBObject $oObject The object being reported
  135. * @param array $aFields An array of attribute codes. List of the attributes to be reported.
  136. * @return void
  137. */
  138. public function AddObject($iCode, $sMessage, $oObject = null, $aFields = null)
  139. {
  140. $oObjRes = new ObjectResult();
  141. $oObjRes->code = $iCode;
  142. $oObjRes->message = $sMessage;
  143. if ($oObject)
  144. {
  145. $oObjRes->class = get_class($oObject);
  146. foreach ($aFields as $sAttCode)
  147. {
  148. $oObjRes->AddField($oObject, $sAttCode);
  149. }
  150. }
  151. $this->objects[] = $oObjRes;
  152. }
  153. }
  154. /**
  155. * Deletion result codes for a target object (either deleted or updated)
  156. *
  157. * @package Extensibility
  158. * @api
  159. * @since 2.0.1
  160. */
  161. class RestDelete
  162. {
  163. /**
  164. * Result: Object deleted as per the initial request
  165. */
  166. const OK = 0;
  167. /**
  168. * Result: general issue (user rights or ... ?)
  169. */
  170. const ISSUE = 1;
  171. /**
  172. * Result: Must be deleted to preserve database integrity
  173. */
  174. const AUTO_DELETE = 2;
  175. /**
  176. * Result: Must be deleted to preserve database integrity, but that is NOT possible
  177. */
  178. const AUTO_DELETE_ISSUE = 3;
  179. /**
  180. * Result: Must be deleted to preserve database integrity, but this must be requested explicitely
  181. */
  182. const REQUEST_EXPLICITELY = 4;
  183. /**
  184. * Result: Must be updated to preserve database integrity
  185. */
  186. const AUTO_UPDATE = 5;
  187. /**
  188. * Result: Must be updated to preserve database integrity, but that is NOT possible
  189. */
  190. const AUTO_UPDATE_ISSUE = 6;
  191. }
  192. /**
  193. * Implementation of core REST services (create/get/update... objects)
  194. *
  195. * @package Core
  196. */
  197. class CoreServices implements iRestServiceProvider
  198. {
  199. /**
  200. * Enumerate services delivered by this class
  201. *
  202. * @param string $sVersion The version (e.g. 1.0) supported by the services
  203. * @return array An array of hash 'verb' => verb, 'description' => description
  204. */
  205. public function ListOperations($sVersion)
  206. {
  207. $aOps = array();
  208. if ($sVersion == '1.0')
  209. {
  210. $aOps[] = array(
  211. 'verb' => 'core/create',
  212. 'description' => 'Create an object'
  213. );
  214. $aOps[] = array(
  215. 'verb' => 'core/update',
  216. 'description' => 'Update an object'
  217. );
  218. $aOps[] = array(
  219. 'verb' => 'core/apply_stimulus',
  220. 'description' => 'Apply a stimulus to change the state of an object'
  221. );
  222. $aOps[] = array(
  223. 'verb' => 'core/get',
  224. 'description' => 'Search for objects'
  225. );
  226. $aOps[] = array(
  227. 'verb' => 'core/delete',
  228. 'description' => 'Delete objects'
  229. );
  230. }
  231. return $aOps;
  232. }
  233. /**
  234. * Enumerate services delivered by this class
  235. * @param string $sVersion The version (e.g. 1.0) supported by the services
  236. * @return RestResult The standardized result structure (at least a message)
  237. * @throws Exception in case of internal failure.
  238. */
  239. public function ExecOperation($sVersion, $sVerb, $aParams)
  240. {
  241. $oResult = new RestResultWithObjects();
  242. switch ($sVerb)
  243. {
  244. case 'core/create':
  245. RestUtils::InitTrackingComment($aParams);
  246. $sClass = RestUtils::GetClass($aParams, 'class');
  247. $aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
  248. $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
  249. $oObject = RestUtils::MakeObjectFromFields($sClass, $aFields);
  250. $oObject->DBInsert();
  251. $oResult->AddObject(0, 'created', $oObject, $aShowFields);
  252. break;
  253. case 'core/update':
  254. RestUtils::InitTrackingComment($aParams);
  255. $sClass = RestUtils::GetClass($aParams, 'class');
  256. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  257. $aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
  258. $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
  259. $oObject = RestUtils::FindObjectFromKey($sClass, $key);
  260. RestUtils::UpdateObjectFromFields($oObject, $aFields);
  261. $oObject->DBUpdate();
  262. $oResult->AddObject(0, 'updated', $oObject, $aShowFields);
  263. break;
  264. case 'core/apply_stimulus':
  265. RestUtils::InitTrackingComment($aParams);
  266. $sClass = RestUtils::GetClass($aParams, 'class');
  267. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  268. $aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
  269. $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
  270. $sStimulus = RestUtils::GetMandatoryParam($aParams, 'stimulus');
  271. $oObject = RestUtils::FindObjectFromKey($sClass, $key);
  272. RestUtils::UpdateObjectFromFields($oObject, $aFields);
  273. $aTransitions = $oObject->EnumTransitions();
  274. $aStimuli = MetaModel::EnumStimuli(get_class($oObject));
  275. if (!isset($aTransitions[$sStimulus]))
  276. {
  277. // Invalid stimulus
  278. $oResult->code = RestResult::INTERNAL_ERROR;
  279. $oResult->message = "Invalid stimulus: '$sStimulus' on the object ".$oObject->GetName()." in state '".$oObject->GetState()."'";
  280. }
  281. else
  282. {
  283. $aTransition = $aTransitions[$sStimulus];
  284. $sTargetState = $aTransition['target_state'];
  285. $aStates = MetaModel::EnumStates($sClass);
  286. $aTargetStateDef = $aStates[$sTargetState];
  287. $aExpectedAttributes = $aTargetStateDef['attribute_list'];
  288. $aMissingMandatory = array();
  289. foreach($aExpectedAttributes as $sAttCode => $iExpectCode)
  290. {
  291. if ( ($iExpectCode & OPT_ATT_MANDATORY) && ($oObject->Get($sAttCode) == ''))
  292. {
  293. $aMissingMandatory[] = $sAttCode;
  294. }
  295. }
  296. if (count($aMissingMandatory) == 0)
  297. {
  298. // If all the mandatory fields are already present, just apply the transition silently...
  299. if ($oObject->ApplyStimulus($sStimulus))
  300. {
  301. $oObject->DBUpdate();
  302. $oResult->AddObject(0, 'updated', $oObject, $aShowFields);
  303. }
  304. }
  305. else
  306. {
  307. // Missing mandatory attributes for the transition
  308. $oResult->code = RestResult::INTERNAL_ERROR;
  309. $oResult->message = 'Missing mandatory attribute(s) for applying the stimulus: '.implode(', ', $aMissingMandatory).'.';
  310. }
  311. }
  312. break;
  313. case 'core/get':
  314. $sClass = RestUtils::GetClass($aParams, 'class');
  315. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  316. $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
  317. $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
  318. while ($oObject = $oObjectSet->Fetch())
  319. {
  320. $oResult->AddObject(0, '', $oObject, $aShowFields);
  321. }
  322. $oResult->message = "Found: ".$oObjectSet->Count();
  323. break;
  324. case 'core/delete':
  325. $sClass = RestUtils::GetClass($aParams, 'class');
  326. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  327. $bSimulate = RestUtils::GetOptionalParam($aParams, 'simulate', false);
  328. $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
  329. $aObjects = $oObjectSet->ToArray();
  330. $this->DeleteObjects($oResult, $aObjects, $bSimulate);
  331. break;
  332. default:
  333. // unknown operation: handled at a higher level
  334. }
  335. return $oResult;
  336. }
  337. /**
  338. * Helper for object deletion
  339. */
  340. public function DeleteObjects($oResult, $aObjects, $bSimulate)
  341. {
  342. $oDeletionPlan = new DeletionPlan();
  343. foreach($aObjects as $oObj)
  344. {
  345. if ($bSimulate)
  346. {
  347. $oObj->CheckToDelete($oDeletionPlan);
  348. }
  349. else
  350. {
  351. $oObj->DBDelete($oDeletionPlan);
  352. }
  353. }
  354. foreach ($oDeletionPlan->ListDeletes() as $sTargetClass => $aDeletes)
  355. {
  356. foreach ($aDeletes as $iId => $aData)
  357. {
  358. $oToDelete = $aData['to_delete'];
  359. $bAutoDel = (($aData['mode'] == DEL_SILENT) || ($aData['mode'] == DEL_AUTO));
  360. if (array_key_exists('issue', $aData))
  361. {
  362. if ($bAutoDel)
  363. {
  364. if (isset($aData['requested_explicitely'])) // i.e. in the initial list of objects to delete
  365. {
  366. $iCode = RestDelete::ISSUE;
  367. $sPlanned = 'Cannot be deleted: '.$aData['issue'];
  368. }
  369. else
  370. {
  371. $iCode = RestDelete::AUTO_DELETE_ISSUE;
  372. $sPlanned = 'Should be deleted automatically... but: '.$aData['issue'];
  373. }
  374. }
  375. else
  376. {
  377. $iCode = RestDelete::REQUEST_EXPLICITELY;
  378. $sPlanned = 'Must be deleted explicitely... but: '.$aData['issue'];
  379. }
  380. }
  381. else
  382. {
  383. if ($bAutoDel)
  384. {
  385. if (isset($aData['requested_explicitely']))
  386. {
  387. $iCode = RestDelete::OK;
  388. $sPlanned = '';
  389. }
  390. else
  391. {
  392. $iCode = RestDelete::AUTO_DELETE;
  393. $sPlanned = 'Deleted automatically';
  394. }
  395. }
  396. else
  397. {
  398. $iCode = RestDelete::REQUEST_EXPLICITELY;
  399. $sPlanned = 'Must be deleted explicitely';
  400. }
  401. }
  402. $oResult->AddObject($iCode, $sPlanned, $oToDelete, array('id', 'friendlyname'));
  403. }
  404. }
  405. foreach ($oDeletionPlan->ListUpdates() as $sRemoteClass => $aToUpdate)
  406. {
  407. foreach ($aToUpdate as $iId => $aData)
  408. {
  409. $oToUpdate = $aData['to_reset'];
  410. if (array_key_exists('issue', $aData))
  411. {
  412. $iCode = RestDelete::AUTO_UPDATE_ISSUE;
  413. $sPlanned = 'Should be updated automatically... but: '.$aData['issue'];
  414. }
  415. else
  416. {
  417. $iCode = RestDelete::AUTO_UPDATE;
  418. $sPlanned = 'Reset external keys: '.$aData['attributes_list'];
  419. }
  420. $oResult->AddObject($iCode, $sPlanned, $oToUpdate, array('id', 'friendlyname'));
  421. }
  422. }
  423. if ($oDeletionPlan->FoundStopper())
  424. {
  425. if ($oDeletionPlan->FoundSecurityIssue())
  426. {
  427. $iRes = RestResult::UNAUTHORIZED;
  428. $sRes = 'Deletion not allowed on some objects';
  429. }
  430. elseif ($oDeletionPlan->FoundManualOperation())
  431. {
  432. $iRes = RestResult::UNSAFE;
  433. $sRes = 'The deletion requires that other objects be deleted/updated, and those operations must be requested explicitely';
  434. }
  435. else
  436. {
  437. $iRes = RestResult::INTERNAL_ERROR;
  438. $sRes = 'Some issues have been encountered. See the list of planned changes for more information about the issue(s).';
  439. }
  440. }
  441. else
  442. {
  443. $iRes = RestResult::OK;
  444. $sRes = 'Deleted: '.count($aObjects);
  445. $iIndirect = $oDeletionPlan->GetTargetCount() - count($aObjects);
  446. if ($iIndirect > 0)
  447. {
  448. $sRes .= ' plus (for DB integrity) '.$iIndirect;
  449. }
  450. }
  451. $oResult->code = $iRes;
  452. if ($bSimulate)
  453. {
  454. $oResult->message = 'SIMULATING: '.$sRes;
  455. }
  456. else
  457. {
  458. $oResult->message = $sRes;
  459. }
  460. }
  461. }