restservices.class.inc.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  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 $class;
  38. public $key;
  39. public $fields;
  40. /**
  41. * Default constructor
  42. */
  43. public function __construct($sClass = null, $iId = null)
  44. {
  45. $this->code = RestResult::OK;
  46. $this->message = '';
  47. $this->class = $sClass;
  48. $this->key = $iId;
  49. $this->fields = array();
  50. }
  51. /**
  52. * Helper to make an output value for a given attribute
  53. *
  54. * @param DBObject $oObject The object being reported
  55. * @param string $sAttCode The attribute code (must be valid)
  56. * @return string A scalar representation of the value
  57. */
  58. protected function MakeResultValue(DBObject $oObject, $sAttCode)
  59. {
  60. if ($sAttCode == 'id')
  61. {
  62. $value = $oObject->GetKey();
  63. }
  64. else
  65. {
  66. $sClass = get_class($oObject);
  67. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  68. if ($oAttDef instanceof AttributeLinkedSet)
  69. {
  70. $value = array();
  71. // Make the list of required attributes
  72. // - Skip attributes pointing to the current object (redundant data)
  73. // - Skip link sets refering to the current data (infinite recursion!)
  74. $aRelevantAttributes = array();
  75. $sLnkClass = $oAttDef->GetLinkedClass();
  76. foreach (MetaModel::ListAttributeDefs($sLnkClass) as $sLnkAttCode => $oLnkAttDef)
  77. {
  78. // Skip any attribute of the link that points to the current object
  79. //
  80. if ($sLnkAttCode == $oAttDef->GetExtKeyToMe()) continue;
  81. if (method_exists($oLnkAttDef, 'GetKeyAttCode'))
  82. {
  83. if ($oLnkAttDef->GetKeyAttCode() ==$oAttDef->GetExtKeyToMe()) continue;
  84. }
  85. $aRelevantAttributes[] = $sLnkAttCode;
  86. }
  87. // Iterate on the set and build an array of array of attcode=>value
  88. $oSet = $oObject->Get($sAttCode);
  89. while ($oLnk = $oSet->Fetch())
  90. {
  91. $aLnkValues = array();
  92. foreach ($aRelevantAttributes as $sLnkAttCode)
  93. {
  94. $aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode);
  95. }
  96. $value[] = $aLnkValues;
  97. }
  98. }
  99. else
  100. {
  101. $value = $oAttDef->GetForJSON($oObject->Get($sAttCode));
  102. }
  103. }
  104. return $value;
  105. }
  106. /**
  107. * Report the value for the given object attribute
  108. *
  109. * @param DBObject $oObject The object being reported
  110. * @param string $sAttCode The attribute code (must be valid)
  111. * @return void
  112. */
  113. public function AddField(DBObject $oObject, $sAttCode)
  114. {
  115. $this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode);
  116. }
  117. }
  118. /**
  119. * REST response for services managing objects. Derive this structure to add information and/or constants
  120. *
  121. * @package Extensibility
  122. * @package REST Services
  123. * @api
  124. */
  125. class RestResultWithObjects extends RestResult
  126. {
  127. public $objects;
  128. /**
  129. * Report the given object
  130. *
  131. * @param int An error code (RestResult::OK is no issue has been found)
  132. * @param string $sMessage Description of the error if any, an empty string otherwise
  133. * @param DBObject $oObject The object being reported
  134. * @param array $aFieldSpec An array of class => attribute codes (Cf. RestUtils::GetFieldList). List of the attributes to be reported.
  135. * @return void
  136. */
  137. public function AddObject($iCode, $sMessage, $oObject, $aFieldSpec = null)
  138. {
  139. $sClass = get_class($oObject);
  140. $oObjRes = new ObjectResult($sClass, $oObject->GetKey());
  141. $oObjRes->code = $iCode;
  142. $oObjRes->message = $sMessage;
  143. $aFields = null;
  144. if (!is_null($aFieldSpec))
  145. {
  146. // Enum all classes in the hierarchy, starting with the current one
  147. foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sRefClass)
  148. {
  149. if (array_key_exists($sRefClass, $aFieldSpec))
  150. {
  151. $aFields = $aFieldSpec[$sRefClass];
  152. break;
  153. }
  154. }
  155. }
  156. if (is_null($aFields))
  157. {
  158. // No fieldspec given, or not found...
  159. $aFields = array('id', 'friendlyname');
  160. }
  161. foreach ($aFields as $sAttCode)
  162. {
  163. $oObjRes->AddField($oObject, $sAttCode);
  164. }
  165. $sObjKey = get_class($oObject).'::'.$oObject->GetKey();
  166. $this->objects[$sObjKey] = $oObjRes;
  167. }
  168. }
  169. class RestResultWithRelations extends RestResultWithObjects
  170. {
  171. public $relations;
  172. public function __construct()
  173. {
  174. parent::__construct();
  175. $this->relations = array();
  176. }
  177. public function AddRelation($sSrcKey, $sDestKey)
  178. {
  179. if (!array_key_exists($sSrcKey, $this->relations))
  180. {
  181. $this->relations[$sSrcKey] = array();
  182. }
  183. $this->relations[$sSrcKey][] = array('key' => $sDestKey);
  184. }
  185. }
  186. /**
  187. * Deletion result codes for a target object (either deleted or updated)
  188. *
  189. * @package Extensibility
  190. * @api
  191. * @since 2.0.1
  192. */
  193. class RestDelete
  194. {
  195. /**
  196. * Result: Object deleted as per the initial request
  197. */
  198. const OK = 0;
  199. /**
  200. * Result: general issue (user rights or ... ?)
  201. */
  202. const ISSUE = 1;
  203. /**
  204. * Result: Must be deleted to preserve database integrity
  205. */
  206. const AUTO_DELETE = 2;
  207. /**
  208. * Result: Must be deleted to preserve database integrity, but that is NOT possible
  209. */
  210. const AUTO_DELETE_ISSUE = 3;
  211. /**
  212. * Result: Must be deleted to preserve database integrity, but this must be requested explicitely
  213. */
  214. const REQUEST_EXPLICITELY = 4;
  215. /**
  216. * Result: Must be updated to preserve database integrity
  217. */
  218. const AUTO_UPDATE = 5;
  219. /**
  220. * Result: Must be updated to preserve database integrity, but that is NOT possible
  221. */
  222. const AUTO_UPDATE_ISSUE = 6;
  223. }
  224. /**
  225. * Implementation of core REST services (create/get/update... objects)
  226. *
  227. * @package Core
  228. */
  229. class CoreServices implements iRestServiceProvider
  230. {
  231. /**
  232. * Enumerate services delivered by this class
  233. *
  234. * @param string $sVersion The version (e.g. 1.0) supported by the services
  235. * @return array An array of hash 'verb' => verb, 'description' => description
  236. */
  237. public function ListOperations($sVersion)
  238. {
  239. // 1.1 - In the reply, objects have a 'key' entry so that it is no more necessary to split class::key programmaticaly
  240. //
  241. $aOps = array();
  242. if (in_array($sVersion, array('1.0', '1.1')))
  243. {
  244. $aOps[] = array(
  245. 'verb' => 'core/create',
  246. 'description' => 'Create an object'
  247. );
  248. $aOps[] = array(
  249. 'verb' => 'core/update',
  250. 'description' => 'Update an object'
  251. );
  252. $aOps[] = array(
  253. 'verb' => 'core/apply_stimulus',
  254. 'description' => 'Apply a stimulus to change the state of an object'
  255. );
  256. $aOps[] = array(
  257. 'verb' => 'core/get',
  258. 'description' => 'Search for objects'
  259. );
  260. $aOps[] = array(
  261. 'verb' => 'core/delete',
  262. 'description' => 'Delete objects'
  263. );
  264. $aOps[] = array(
  265. 'verb' => 'core/get_related',
  266. 'description' => 'Get related objects through the specified relation'
  267. );
  268. }
  269. return $aOps;
  270. }
  271. /**
  272. * Enumerate services delivered by this class
  273. * @param string $sVersion The version (e.g. 1.0) supported by the services
  274. * @return RestResult The standardized result structure (at least a message)
  275. * @throws Exception in case of internal failure.
  276. */
  277. public function ExecOperation($sVersion, $sVerb, $aParams)
  278. {
  279. $oResult = new RestResultWithObjects();
  280. switch ($sVerb)
  281. {
  282. case 'core/create':
  283. RestUtils::InitTrackingComment($aParams);
  284. $sClass = RestUtils::GetClass($aParams, 'class');
  285. $aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
  286. $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
  287. $oObject = RestUtils::MakeObjectFromFields($sClass, $aFields);
  288. $oObject->DBInsert();
  289. $oResult->AddObject(0, 'created', $oObject, $aShowFields);
  290. break;
  291. case 'core/update':
  292. RestUtils::InitTrackingComment($aParams);
  293. $sClass = RestUtils::GetClass($aParams, 'class');
  294. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  295. $aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
  296. $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
  297. $oObject = RestUtils::FindObjectFromKey($sClass, $key);
  298. RestUtils::UpdateObjectFromFields($oObject, $aFields);
  299. $oObject->DBUpdate();
  300. $oResult->AddObject(0, 'updated', $oObject, $aShowFields);
  301. break;
  302. case 'core/apply_stimulus':
  303. RestUtils::InitTrackingComment($aParams);
  304. $sClass = RestUtils::GetClass($aParams, 'class');
  305. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  306. $aFields = RestUtils::GetMandatoryParam($aParams, 'fields');
  307. $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
  308. $sStimulus = RestUtils::GetMandatoryParam($aParams, 'stimulus');
  309. $oObject = RestUtils::FindObjectFromKey($sClass, $key);
  310. RestUtils::UpdateObjectFromFields($oObject, $aFields);
  311. $aTransitions = $oObject->EnumTransitions();
  312. $aStimuli = MetaModel::EnumStimuli(get_class($oObject));
  313. if (!isset($aTransitions[$sStimulus]))
  314. {
  315. // Invalid stimulus
  316. $oResult->code = RestResult::INTERNAL_ERROR;
  317. $oResult->message = "Invalid stimulus: '$sStimulus' on the object ".$oObject->GetName()." in state '".$oObject->GetState()."'";
  318. }
  319. else
  320. {
  321. $aTransition = $aTransitions[$sStimulus];
  322. $sTargetState = $aTransition['target_state'];
  323. $aStates = MetaModel::EnumStates($sClass);
  324. $aTargetStateDef = $aStates[$sTargetState];
  325. $aExpectedAttributes = $aTargetStateDef['attribute_list'];
  326. $aMissingMandatory = array();
  327. foreach($aExpectedAttributes as $sAttCode => $iExpectCode)
  328. {
  329. if ( ($iExpectCode & OPT_ATT_MANDATORY) && ($oObject->Get($sAttCode) == ''))
  330. {
  331. $aMissingMandatory[] = $sAttCode;
  332. }
  333. }
  334. if (count($aMissingMandatory) == 0)
  335. {
  336. // If all the mandatory fields are already present, just apply the transition silently...
  337. if ($oObject->ApplyStimulus($sStimulus))
  338. {
  339. $oObject->DBUpdate();
  340. $oResult->AddObject(0, 'updated', $oObject, $aShowFields);
  341. }
  342. }
  343. else
  344. {
  345. // Missing mandatory attributes for the transition
  346. $oResult->code = RestResult::INTERNAL_ERROR;
  347. $oResult->message = 'Missing mandatory attribute(s) for applying the stimulus: '.implode(', ', $aMissingMandatory).'.';
  348. }
  349. }
  350. break;
  351. case 'core/get':
  352. $sClass = RestUtils::GetClass($aParams, 'class');
  353. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  354. $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
  355. $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
  356. while ($oObject = $oObjectSet->Fetch())
  357. {
  358. $oResult->AddObject(0, '', $oObject, $aShowFields);
  359. }
  360. $oResult->message = "Found: ".$oObjectSet->Count();
  361. break;
  362. case 'core/delete':
  363. $sClass = RestUtils::GetClass($aParams, 'class');
  364. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  365. $bSimulate = RestUtils::GetOptionalParam($aParams, 'simulate', false);
  366. $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
  367. $aObjects = $oObjectSet->ToArray();
  368. $this->DeleteObjects($oResult, $aObjects, $bSimulate);
  369. break;
  370. case 'core/get_related':
  371. $oResult = new RestResultWithRelations();
  372. $sClass = RestUtils::GetClass($aParams, 'class');
  373. $key = RestUtils::GetMandatoryParam($aParams, 'key');
  374. $sRelation = RestUtils::GetMandatoryParam($aParams, 'relation');
  375. $iMaxRecursionDepth = RestUtils::GetOptionalParam($aParams, 'depth', 20 /* = MAX_RECURSION_DEPTH */);
  376. $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
  377. $aIndexByClass = array();
  378. while ($oObject = $oObjectSet->Fetch())
  379. {
  380. $aRelated = array();
  381. $aGraph = array();
  382. $aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null;
  383. $oResult->AddObject(0, '', $oObject);
  384. $this->GetRelatedObjects($oObject, $sRelation, $iMaxRecursionDepth, $aRelated, $aGraph);
  385. foreach($aRelated as $sClass => $aObjects)
  386. {
  387. foreach($aObjects as $oRelatedObj)
  388. {
  389. $aIndexByClass[get_class($oRelatedObj)][$oRelatedObj->GetKey()] = null;
  390. $oResult->AddObject(0, '', $oRelatedObj);
  391. }
  392. }
  393. foreach($aGraph as $sSrcKey => $aDestinations)
  394. {
  395. foreach ($aDestinations as $sDestKey)
  396. {
  397. $oResult->AddRelation($sSrcKey, $sDestKey);
  398. }
  399. }
  400. }
  401. if (count($aIndexByClass) > 0)
  402. {
  403. $aStats = array();
  404. foreach ($aIndexByClass as $sClass => $aIds)
  405. {
  406. $aStats[] = $sClass.'= '.count($aIds);
  407. }
  408. $oResult->message = "Scope: ".$oObjectSet->Count()."; Related objects: ".implode(', ', $aStats);
  409. }
  410. else
  411. {
  412. $oResult->message = "Nothing found";
  413. }
  414. break;
  415. default:
  416. // unknown operation: handled at a higher level
  417. }
  418. return $oResult;
  419. }
  420. /**
  421. * Helper for object deletion
  422. */
  423. public function DeleteObjects($oResult, $aObjects, $bSimulate)
  424. {
  425. $oDeletionPlan = new DeletionPlan();
  426. foreach($aObjects as $oObj)
  427. {
  428. if ($bSimulate)
  429. {
  430. $oObj->CheckToDelete($oDeletionPlan);
  431. }
  432. else
  433. {
  434. $oObj->DBDelete($oDeletionPlan);
  435. }
  436. }
  437. foreach ($oDeletionPlan->ListDeletes() as $sTargetClass => $aDeletes)
  438. {
  439. foreach ($aDeletes as $iId => $aData)
  440. {
  441. $oToDelete = $aData['to_delete'];
  442. $bAutoDel = (($aData['mode'] == DEL_SILENT) || ($aData['mode'] == DEL_AUTO));
  443. if (array_key_exists('issue', $aData))
  444. {
  445. if ($bAutoDel)
  446. {
  447. if (isset($aData['requested_explicitely'])) // i.e. in the initial list of objects to delete
  448. {
  449. $iCode = RestDelete::ISSUE;
  450. $sPlanned = 'Cannot be deleted: '.$aData['issue'];
  451. }
  452. else
  453. {
  454. $iCode = RestDelete::AUTO_DELETE_ISSUE;
  455. $sPlanned = 'Should be deleted automatically... but: '.$aData['issue'];
  456. }
  457. }
  458. else
  459. {
  460. $iCode = RestDelete::REQUEST_EXPLICITELY;
  461. $sPlanned = 'Must be deleted explicitely... but: '.$aData['issue'];
  462. }
  463. }
  464. else
  465. {
  466. if ($bAutoDel)
  467. {
  468. if (isset($aData['requested_explicitely']))
  469. {
  470. $iCode = RestDelete::OK;
  471. $sPlanned = '';
  472. }
  473. else
  474. {
  475. $iCode = RestDelete::AUTO_DELETE;
  476. $sPlanned = 'Deleted automatically';
  477. }
  478. }
  479. else
  480. {
  481. $iCode = RestDelete::REQUEST_EXPLICITELY;
  482. $sPlanned = 'Must be deleted explicitely';
  483. }
  484. }
  485. $oResult->AddObject($iCode, $sPlanned, $oToDelete);
  486. }
  487. }
  488. foreach ($oDeletionPlan->ListUpdates() as $sRemoteClass => $aToUpdate)
  489. {
  490. foreach ($aToUpdate as $iId => $aData)
  491. {
  492. $oToUpdate = $aData['to_reset'];
  493. if (array_key_exists('issue', $aData))
  494. {
  495. $iCode = RestDelete::AUTO_UPDATE_ISSUE;
  496. $sPlanned = 'Should be updated automatically... but: '.$aData['issue'];
  497. }
  498. else
  499. {
  500. $iCode = RestDelete::AUTO_UPDATE;
  501. $sPlanned = 'Reset external keys: '.$aData['attributes_list'];
  502. }
  503. $oResult->AddObject($iCode, $sPlanned, $oToUpdate);
  504. }
  505. }
  506. if ($oDeletionPlan->FoundStopper())
  507. {
  508. if ($oDeletionPlan->FoundSecurityIssue())
  509. {
  510. $iRes = RestResult::UNAUTHORIZED;
  511. $sRes = 'Deletion not allowed on some objects';
  512. }
  513. elseif ($oDeletionPlan->FoundManualOperation())
  514. {
  515. $iRes = RestResult::UNSAFE;
  516. $sRes = 'The deletion requires that other objects be deleted/updated, and those operations must be requested explicitely';
  517. }
  518. else
  519. {
  520. $iRes = RestResult::INTERNAL_ERROR;
  521. $sRes = 'Some issues have been encountered. See the list of planned changes for more information about the issue(s).';
  522. }
  523. }
  524. else
  525. {
  526. $iRes = RestResult::OK;
  527. $sRes = 'Deleted: '.count($aObjects);
  528. $iIndirect = $oDeletionPlan->GetTargetCount() - count($aObjects);
  529. if ($iIndirect > 0)
  530. {
  531. $sRes .= ' plus (for DB integrity) '.$iIndirect;
  532. }
  533. }
  534. $oResult->code = $iRes;
  535. if ($bSimulate)
  536. {
  537. $oResult->message = 'SIMULATING: '.$sRes;
  538. }
  539. else
  540. {
  541. $oResult->message = $sRes;
  542. }
  543. }
  544. /**
  545. * Helper function to get the related objects up to the given depth along with the "graph" of the relation
  546. * @param DBObject $oObject Starting point of the computation
  547. * @param string $sRelation Code of the relation (i.e; 'impact', 'depends on'...)
  548. * @param integer $iMaxRecursionDepth Maximum level of recursion
  549. * @param Hash $aRelated Two dimensions hash of the already related objects: array( 'class' => array(key => ))
  550. * @param Hash $aGraph Hash array for the topology of the relation: source => related: array('class:key' => array( DBObjects ))
  551. * @param integer $iRecursionDepth Current level of recursion
  552. */
  553. protected function GetRelatedObjects(DBObject $oObject, $sRelation, $iMaxRecursionDepth, &$aRelated, &$aGraph, $iRecursionDepth = 1)
  554. {
  555. // Avoid loops
  556. if ((array_key_exists(get_class($oObject), $aRelated)) && (array_key_exists($oObject->GetKey(), $aRelated[get_class($oObject)]))) return;
  557. // Stop at maximum recursion level
  558. if ($iRecursionDepth > $iMaxRecursionDepth) return;
  559. $sSrcKey = get_class($oObject).'::'.$oObject->GetKey();
  560. $aNewRelated = array();
  561. $oObject->GetRelatedObjects($sRelation, 1, $aNewRelated);
  562. foreach($aNewRelated as $sClass => $aObjects)
  563. {
  564. if (!array_key_exists($sSrcKey, $aGraph))
  565. {
  566. $aGraph[$sSrcKey] = array();
  567. }
  568. foreach($aObjects as $oRelatedObject)
  569. {
  570. $aRelated[$sClass][$oRelatedObject->GetKey()] = $oRelatedObject;
  571. $aGraph[$sSrcKey][] = get_class($oRelatedObject).'::'.$oRelatedObject->GetKey();
  572. $this->GetRelatedObjects($oRelatedObject, $sRelation, $iMaxRecursionDepth, $aRelated, $aGraph, $iRecursionDepth+1);
  573. }
  574. }
  575. }
  576. }