restservices.class.inc.php 20 KB

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