modulediscovery.class.inc.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <?php
  2. // Copyright (C) 2010-2012 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. * ModuleDiscovery: list available modules
  20. *
  21. * @copyright Copyright (C) 2010-2012 Combodo SARL
  22. * @license http://opensource.org/licenses/AGPL-3.0
  23. */
  24. class MissingDependencyException extends Exception
  25. {
  26. public $aModulesInfo;
  27. }
  28. class ModuleDiscovery
  29. {
  30. static $m_aModuleArgs = array(
  31. 'label' => 'One line description shown during the interactive setup',
  32. 'dependencies' => 'array of module ids',
  33. 'mandatory' => 'boolean',
  34. 'visible' => 'boolean',
  35. 'datamodel' => 'array of data model files',
  36. //'dictionary' => 'array of dictionary files', // No longer mandatory, now automated
  37. 'data.struct' => 'array of structural data files',
  38. 'data.sample' => 'array of sample data files',
  39. 'doc.manual_setup' => 'url',
  40. 'doc.more_information' => 'url',
  41. );
  42. // Cache the results and the source directories
  43. protected static $m_aSearchDirs = null;
  44. protected static $m_aModules = array();
  45. // All the entries below are list of file paths relative to the module directory
  46. protected static $m_aFilesList = array('datamodel', 'webservice', 'dictionary', 'data.struct', 'data.sample');
  47. // ModulePath is used by AddModule to get the path of the module being included (in ListModuleFiles)
  48. protected static $m_sModulePath = null;
  49. protected static function SetModulePath($sModulePath)
  50. {
  51. self::$m_sModulePath = $sModulePath;
  52. }
  53. public static function AddModule($sFilePath, $sId, $aArgs)
  54. {
  55. if (!array_key_exists('itop_version', $aArgs))
  56. {
  57. // Assume 1.0.2
  58. $aArgs['itop_version'] = '1.0.2';
  59. }
  60. foreach (self::$m_aModuleArgs as $sArgName => $sArgDesc)
  61. {
  62. if (!array_key_exists($sArgName, $aArgs))
  63. {
  64. throw new Exception("Module '$sId': missing argument '$sArgName'");
  65. }
  66. }
  67. $aArgs['root_dir'] = dirname($sFilePath);
  68. $aArgs['module_file'] = $sFilePath;
  69. self::$m_aModules[$sId] = $aArgs;
  70. foreach(self::$m_aFilesList as $sAttribute)
  71. {
  72. if (isset(self::$m_aModules[$sId][$sAttribute]))
  73. {
  74. // All the items below are list of files, that are relative to the current file
  75. // being loaded, let's update their path to store path relative to the application directory
  76. foreach(self::$m_aModules[$sId][$sAttribute] as $idx => $sRelativePath)
  77. {
  78. self::$m_aModules[$sId][$sAttribute][$idx] = self::$m_sModulePath.'/'.$sRelativePath;
  79. }
  80. }
  81. }
  82. // Populate automatically the list of dictionary files
  83. if(preg_match('|^([^/]+)|', $sId, $aMatches)) // ModuleName = everything before the first forward slash
  84. {
  85. $sModuleName = $aMatches[1];
  86. $sDir = dirname($sFilePath);
  87. if ($hDir = opendir($sDir))
  88. {
  89. while (($sFile = readdir($hDir)) !== false)
  90. {
  91. $aMatches = array();
  92. if (preg_match("/^[^\\.]+.dict.$sModuleName.php$/i", $sFile, $aMatches)) // Dictionary files named like <Lang>.dict.<ModuleName>.php are loaded automatically
  93. {
  94. self::$m_aModules[$sId]['dictionary'][] = self::$m_sModulePath.'/'.$sFile;
  95. }
  96. }
  97. closedir($hDir);
  98. }
  99. }
  100. }
  101. /**
  102. * Get the list of "discovered" modules, ordered based on their (inter) dependencies
  103. * @param bool $bAbortOnMissingDependency ...
  104. * @param hash $aModulesToLoad List of modules to search for, defaults to all if ommitted
  105. */
  106. protected static function GetModules($bAbortOnMissingDependency = false, $aModulesToLoad = null)
  107. {
  108. // Order the modules to take into account their inter-dependencies
  109. return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad);
  110. }
  111. /**
  112. * Arrange an list of modules, based on their (inter) dependencies
  113. * @param hash $aModules The list of modules to process: 'id' => $aModuleInfo
  114. * @param bool $bAbortOnMissingDependency ...
  115. * @param hash $aModulesToLoad List of modules to search for, defaults to all if ommitted
  116. * @return hash
  117. */
  118. public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
  119. {
  120. // Order the modules to take into account their inter-dependencies
  121. $aDependencies = array();
  122. $aSelectedModules = array();
  123. foreach($aModules as $sId => $aModule)
  124. {
  125. list($sModuleName, $sModuleVersion) = self::GetModuleName($sId);
  126. if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad))
  127. {
  128. $aDependencies[$sId] = $aModule['dependencies'];
  129. $aSelectedModules[$sModuleName] = true;
  130. }
  131. }
  132. ksort($aDependencies);
  133. $aOrderedModules = array();
  134. $iLoopCount = 1;
  135. while(($iLoopCount < count($aModules)) && (count($aDependencies) > 0) )
  136. {
  137. foreach($aDependencies as $sId => $aRemainingDeps)
  138. {
  139. $bDependenciesSolved = true;
  140. foreach($aRemainingDeps as $sDepId)
  141. {
  142. if (!self::DependencyIsResolved($sDepId, $aOrderedModules, $aSelectedModules))
  143. {
  144. $bDependenciesSolved = false;
  145. }
  146. }
  147. if ($bDependenciesSolved)
  148. {
  149. $aOrderedModules[] = $sId;
  150. unset($aDependencies[$sId]);
  151. }
  152. }
  153. $iLoopCount++;
  154. }
  155. if ($bAbortOnMissingDependency && count($aDependencies) > 0)
  156. {
  157. $aModulesInfo = array();
  158. $aModuleDeps = array();
  159. foreach($aDependencies as $sId => $aDeps)
  160. {
  161. $aModule = $aModules[$sId];
  162. $aModuleDeps[] = "{$aModule['label']} (id: $sId) depends on ".implode(' + ', $aDeps);
  163. $aModulesInfo[$sId] = array('module' => $aModule, 'dependencies' => $aDeps);
  164. }
  165. $sMessage = "The following modules have unmet dependencies: ".implode(', ', $aModuleDeps);
  166. $oException = new MissingDependencyException($sMessage);
  167. $oException->aModulesInfo = $aModulesInfo;
  168. throw $oException;
  169. }
  170. // Return the ordered list, so that the dependencies are met...
  171. $aResult = array();
  172. foreach($aOrderedModules as $sId)
  173. {
  174. $aResult[$sId] = $aModules[$sId];
  175. }
  176. return $aResult;
  177. }
  178. /**
  179. * Remove the duplicate modules (i.e. modules with the same name but with a different version) from the supplied list of modules
  180. * @param hash $aModules
  181. * @return hash The ordered a duplicate-free list of modules
  182. */
  183. public static function RemoveDuplicateModules($aModules)
  184. {
  185. $aRes = array();
  186. $aIndex = array();
  187. foreach($aModules as $sModuleId => $aModuleInfo)
  188. {
  189. if (preg_match('|^([^/]+)/(.*)$|', $sModuleId, $aMatches))
  190. {
  191. $sModuleName = $aMatches[1];
  192. $sModuleVersion = $aMatches[2];
  193. }
  194. else
  195. {
  196. // No version number found, assume 1.0.0
  197. $sModuleName = str_replace('/', '', $sModuleId);
  198. $sModuleVersion = '1.0.0';
  199. }
  200. // The last version encountered has precedence
  201. $aIndex[$sModuleName] = $sModuleVersion;
  202. }
  203. foreach($aModules as $sModuleId => $aModuleInfo)
  204. {
  205. if (preg_match('|^([^/]+)/(.*)$|', $sModuleId, $aMatches))
  206. {
  207. $sModuleName = $aMatches[1];
  208. $sModuleVersion = $aMatches[2];
  209. }
  210. else
  211. {
  212. // No version number found, assume 1.0.0
  213. $sModuleName = str_replace('/', '', $sModuleId);
  214. $sModuleVersion = '1.0.0';
  215. }
  216. if ($aIndex[$sModuleName] == $sModuleVersion)
  217. {
  218. // Ok, this this the last (or only) version of this module in the list, keep it
  219. $aRes[$sModuleId] = $aModuleInfo;
  220. }
  221. else
  222. {
  223. if(version_compare($sModuleVersion, $aIndex[$sModuleName], '<'))
  224. {
  225. SetupPage::log_info("Module $sModuleId will be upgraded to $sModuleName/{$aIndex[$sModuleName]}.");
  226. }
  227. else
  228. {
  229. SetupPage::log_warning("Module $sModuleId will be DOWNGRADED to $sModuleName/{$aIndex[$sModuleName]} since the older version is to be loaded AFTER the more recent version.");
  230. }
  231. }
  232. }
  233. // If needed re-arrange the list ot take care of inter dependencies
  234. $aRes = self::OrderModulesByDependencies($aRes, true);
  235. return $aRes;
  236. }
  237. protected static function DependencyIsResolved($sDepString, $aOrderedModules, $aSelectedModules)
  238. {
  239. $bResult = false;
  240. $aModuleVersions = array();
  241. // Separate the module names from their version for an easier comparison later
  242. foreach($aOrderedModules as $sModuleId)
  243. {
  244. if (preg_match('|^([^/]+)/(.*)$|', $sModuleId, $aMatches))
  245. {
  246. $aModuleVersions[$aMatches[1]] = $aMatches[2];
  247. }
  248. else
  249. {
  250. // No version number found, assume 1.0.0
  251. $aModuleVersions[$sModuleId] = '1.0.0';
  252. }
  253. }
  254. if (preg_match_all('/([^\(\)&| ]+)/', $sDepString, $aMatches))
  255. {
  256. $aReplacements = array();
  257. $aPotentialPrerequisites = array();
  258. foreach($aMatches as $aMatch)
  259. {
  260. foreach($aMatch as $sModuleId)
  261. {
  262. // $sModuleId in the dependency string is made of a <name>/<optional_operator><version>
  263. // where the operator is < <= = > >= (by default >=)
  264. if(preg_match('|^([^/]+)/(<?>?=?)([^><=]+)$|', $sModuleId, $aModuleMatches))
  265. {
  266. $sModuleName = $aModuleMatches[1];
  267. $aPotentialPrerequisites[$sModuleName] = true;
  268. $sOperator = $aModuleMatches[2];
  269. if ($sOperator == '')
  270. {
  271. $sOperator = '>=';
  272. }
  273. $sExpectedVersion = $aModuleMatches[3];
  274. if (array_key_exists($sModuleName, $aModuleVersions))
  275. {
  276. // module is present, check the version
  277. $sCurrentVersion = $aModuleVersions[$sModuleName];
  278. if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator))
  279. {
  280. $aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
  281. // a function call that results in a runtime fatal error
  282. }
  283. else
  284. {
  285. $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
  286. // a function call that results in a runtime fatal error
  287. }
  288. }
  289. else
  290. {
  291. // module is not present
  292. $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
  293. // a function call that results in a runtime fatal error
  294. }
  295. }
  296. }
  297. }
  298. $bMissingPrerequisite = false;
  299. foreach ($aPotentialPrerequisites as $sModuleName => $void)
  300. {
  301. if (array_key_exists($sModuleName, $aSelectedModules))
  302. {
  303. // This module is actually a prerequisite
  304. if (!array_key_exists($sModuleName, $aModuleVersions))
  305. {
  306. $bMissingPrerequisite = true;
  307. }
  308. }
  309. }
  310. if ($bMissingPrerequisite)
  311. {
  312. $bResult = false;
  313. }
  314. else
  315. {
  316. $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString);
  317. $bOk = @eval('$bResult = '.$sBooleanExpr.'; return true;');
  318. if ($bOk == false)
  319. {
  320. SetupPage::log_warning("Eval of '$sBooleanExpr' returned false");
  321. echo "Failed to parse the boolean Expression = '$sBooleanExpr'<br/>";
  322. }
  323. }
  324. }
  325. return $bResult;
  326. }
  327. /**
  328. * Search (on the disk) for all defined iTop modules, load them and returns the list (as an array)
  329. * of the possible iTop modules to install
  330. * @param aSearchDirs Array of directories to search (absolute paths)
  331. * @param bool $bAbortOnMissingDependency ...
  332. * @param hash $aModulesToLoad List of modules to search for, defaults to all if ommitted
  333. * @return Hash A big array moduleID => ModuleData
  334. */
  335. public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
  336. {
  337. if (self::$m_aSearchDirs != $aSearchDirs)
  338. {
  339. self::ResetCache();
  340. }
  341. if (is_null(self::$m_aSearchDirs))
  342. {
  343. self::$m_aSearchDirs = $aSearchDirs;
  344. // Not in cache, let's scan the disk
  345. foreach($aSearchDirs as $sSearchDir)
  346. {
  347. $sLookupDir = realpath($sSearchDir);
  348. if ($sLookupDir == '')
  349. {
  350. throw new Exception("Invalid directory '$sSearchDir'");
  351. }
  352. clearstatcache();
  353. self::ListModuleFiles(basename($sSearchDir), dirname($sSearchDir));
  354. }
  355. return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad);
  356. }
  357. else
  358. {
  359. // Reuse the previous results
  360. return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad);
  361. }
  362. }
  363. public static function ResetCache()
  364. {
  365. self::$m_aSearchDirs = null;
  366. self::$m_aModules = array();
  367. }
  368. /**
  369. * Helper function to interpret the name of a module
  370. * @param $sModuleId string Identifier of the module, in the form 'name/version'
  371. * @return array(name, version)
  372. */
  373. public static function GetModuleName($sModuleId)
  374. {
  375. if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches))
  376. {
  377. $sName = $aMatches[1];
  378. $sVersion = $aMatches[2];
  379. }
  380. else
  381. {
  382. $sName = $sModuleId;
  383. $sVersion = "";
  384. }
  385. return array($sName, $sVersion);
  386. }
  387. /**
  388. * Helper function to browse a directory and get the modules
  389. * @param $sRelDir string Directory to start from
  390. * @return array(name, version)
  391. */
  392. protected static function ListModuleFiles($sRelDir, $sRootDir)
  393. {
  394. static $iDummyClassIndex = 0;
  395. static $aDefinedClasses = array();
  396. $sDirectory = $sRootDir.'/'.$sRelDir;
  397. if ($hDir = opendir($sDirectory))
  398. {
  399. // This is the correct way to loop over the directory. (according to the documentation)
  400. while (($sFile = readdir($hDir)) !== false)
  401. {
  402. $aMatches = array();
  403. if (is_dir($sDirectory.'/'.$sFile))
  404. {
  405. if (($sFile != '.') && ($sFile != '..') && ($sFile != '.svn'))
  406. {
  407. self::ListModuleFiles($sRelDir.'/'.$sFile, $sRootDir);
  408. }
  409. }
  410. else if (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches))
  411. {
  412. self::SetModulePath($sRelDir);
  413. try
  414. {
  415. $sModuleFileContents = file_get_contents($sDirectory.'/'.$sFile);
  416. $sModuleFileContents = str_replace(array('<?php', '?>'), '', $sModuleFileContents);
  417. $sModuleFileContents = str_replace('__FILE__', "'".addslashes($sDirectory.'/'.$sFile)."'", $sModuleFileContents);
  418. preg_match_all('/class ([A-Za-z0-9_]+) extends ([A-Za-z0-9_]+)/', $sModuleFileContents, $aMatches);
  419. //print_r($aMatches);
  420. $idx = 0;
  421. foreach($aMatches[1] as $sClassName)
  422. {
  423. if (class_exists($sClassName))
  424. {
  425. // rename the class inside the code to prevent a "duplicate class" declaration
  426. // and change its parent class as well so that nobody will find it and try to execute it
  427. $sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents);
  428. }
  429. $idx++;
  430. }
  431. $bRet = eval($sModuleFileContents);
  432. if ($bRet === false)
  433. {
  434. SetupPage::log_warning("Eval of $sRelDir/$sFile returned false");
  435. }
  436. //echo "<p>Done.</p>\n";
  437. }
  438. catch(Exception $e)
  439. {
  440. // Continue...
  441. SetupPage::log_warning("Eval of $sRelDir/$sFile caused an exception: ".$e->getMessage());
  442. }
  443. }
  444. }
  445. closedir($hDir);
  446. }
  447. else
  448. {
  449. throw new Exception("Data directory (".$sDirectory.") not found or not readable.");
  450. }
  451. }
  452. } // End of class
  453. /** Alias for backward compatibility with old module files in which
  454. * the declaration of a module invokes SetupWebPage::AddModule()
  455. * whereas the new form is ModuleDiscovery::AddModule()
  456. */
  457. class SetupWebPage extends ModuleDiscovery
  458. {
  459. // For backward compatibility with old modules...
  460. public static function log_error($sText)
  461. {
  462. SetupPage::log_error($sText);
  463. }
  464. public static function log_warning($sText)
  465. {
  466. SetupPage::log_warning($sText);
  467. }
  468. public static function log_info($sText)
  469. {
  470. SetupPage::log_info($sText);
  471. }
  472. public static function log_ok($sText)
  473. {
  474. SetupPage::log_ok($sText);
  475. }
  476. public static function log($sText)
  477. {
  478. SetupPage::log($sText);
  479. }
  480. }
  481. /** Ugly patch !!!
  482. * In order to be able to analyse / load several times
  483. * the same module file, we rename the class (to avoid duplicate class definitions)
  484. * and we make the class extends the dummy class below in order to "deactivate" completely
  485. * the class (in case some piece of code enumerate the classes derived from a well known class)
  486. * Note that this will not work if someone enumerates the classes that implement a given interface
  487. */
  488. class DummyHandler {
  489. }