sfPluginManager.class.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. <?php
  2. /*
  3. * This file is part of the symfony package.
  4. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  5. *
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. /**
  10. * sfPluginManager allows you to manage plugins installation and uninstallation.
  11. *
  12. * @package symfony
  13. * @subpackage plugin
  14. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  15. * @version SVN: $Id: sfPluginManager.class.php 18030 2009-05-07 07:21:10Z fabien $
  16. */
  17. class sfPluginManager
  18. {
  19. protected
  20. $dispatcher = null,
  21. $environment = null,
  22. $installing = array();
  23. /**
  24. * Constructs a new sfPluginManager.
  25. *
  26. * @param sfEventDispatcher $dispatcher An event dispatcher instance
  27. * @param sfPearEnvironment $environment A sfPearEnvironment instance
  28. */
  29. public function __construct(sfEventDispatcher $dispatcher, sfPearEnvironment $environment)
  30. {
  31. $this->initialize($dispatcher, $environment);
  32. }
  33. /**
  34. * Initializes this sfPluginManager instance.
  35. *
  36. * see sfPearEnvironment for available options.
  37. *
  38. * @param sfEventDispatcher $dispatcher An event dispatcher instance
  39. * @param sfPearEnvironment $environment A sfPearEnvironment instance
  40. */
  41. public function initialize(sfEventDispatcher $dispatcher, $environment)
  42. {
  43. $this->dispatcher = $dispatcher;
  44. $this->environment = $environment;
  45. // configure this plugin manager
  46. $this->configure();
  47. }
  48. /**
  49. * Configures this plugin manager.
  50. */
  51. public function configure()
  52. {
  53. }
  54. /**
  55. * Returns the sfPearEnvironment instance.
  56. *
  57. * @return sfPearEnvironment The sfPearEnvironment instance
  58. */
  59. public function getEnvironment()
  60. {
  61. return $this->environment;
  62. }
  63. /**
  64. * Returns a list of installed plugin.
  65. *
  66. * @return array An array of installed plugins
  67. */
  68. public function getInstalledPlugins()
  69. {
  70. $installed = array();
  71. foreach ($this->environment->getRegistry()->packageInfo(null, null, null) as $channel => $packages)
  72. {
  73. foreach ($packages as $package)
  74. {
  75. $installed[] = $this->environment->getRegistry()->getPackage(isset($package['package']) ? $package['package'] : $package['name'], $channel);
  76. }
  77. }
  78. return $installed;
  79. }
  80. /**
  81. * Installs a plugin.
  82. *
  83. * If you don't pass a version, it will install the latest version available
  84. * for the current project symfony version.
  85. *
  86. * Available options:
  87. *
  88. * * channel: The plugin channel name
  89. * * version: The version to install
  90. * * stability: The stability preference
  91. * * install_deps: Whether to automatically install dependencies (default to false)
  92. *
  93. * @param string $plugin The plugin name
  94. * @param array $options An array of options
  95. */
  96. public function installPlugin($plugin, $options = array())
  97. {
  98. $this->installing = array();
  99. $this->doInstallPlugin($plugin, $options);
  100. }
  101. /**
  102. * Installs a plugin
  103. *
  104. * @see installPlugin()
  105. */
  106. protected function doInstallPlugin($plugin, $options = array())
  107. {
  108. $channel = isset($options['channel']) ? $options['channel'] : $this->environment->getConfig()->get('default_channel');
  109. $stability = isset($options['stability']) ? $options['stability'] : $this->environment->getConfig()->get('preferred_state', null, $channel);
  110. $version = isset($options['version']) ? $options['version'] : null;
  111. $isPackage = true;
  112. if (0 === strpos($plugin, 'http://') || file_exists($plugin))
  113. {
  114. if (0 === strpos($plugin, 'http://plugins.symfony-project.'))
  115. {
  116. throw new sfPluginException("You try to install a symfony 1.0 plugin.\nPlease read the help message of this task to know how to install a plugin for the current version of symfony.");
  117. }
  118. $download = $plugin;
  119. $isPackage = false;
  120. }
  121. else if (false !== strpos($plugin, '/'))
  122. {
  123. list($channel, $plugin) = explode('/', $plugin);
  124. }
  125. $this->dispatcher->notify(new sfEvent($this, 'plugin.pre_install', array('channel' => $channel, 'plugin' => $plugin, 'is_package' => $isPackage)));
  126. if ($isPackage)
  127. {
  128. $this->environment->getRest()->setChannel($channel);
  129. if (!preg_match(PEAR_COMMON_PACKAGE_NAME_PREG, $plugin))
  130. {
  131. throw new sfPluginException(sprintf('Plugin name "%s" is not a valid package name', $plugin));
  132. }
  133. if (!$version)
  134. {
  135. $version = $this->getPluginVersion($plugin, $stability);
  136. }
  137. else
  138. {
  139. if (!$this->isPluginCompatible($plugin, $version))
  140. {
  141. throw new sfPluginDependencyException(sprintf('Plugin "%s" in version "%s" is not compatible with the current application', $plugin, $version));
  142. }
  143. }
  144. if (!preg_match(PEAR_COMMON_PACKAGE_VERSION_PREG, $version))
  145. {
  146. throw new sfPluginException(sprintf('Plugin version "%s" is not a valid version', $version));
  147. }
  148. $existing = $this->environment->getRegistry()->packageInfo($plugin, 'version', $channel);
  149. if (version_compare($existing, $version) === 0)
  150. {
  151. $this->dispatcher->notify(new sfEvent($this, 'application.log', array('Plugin is already installed')));
  152. return true;
  153. }
  154. // skip if the plugin is already installing and we are here through a dependency)
  155. if (isset($this->installing[$channel.'/'.$plugin]))
  156. {
  157. return true;
  158. }
  159. // convert the plugin package into a discrete download URL
  160. $download = $this->environment->getRest()->getPluginDownloadURL($plugin, $version, $stability);
  161. if (PEAR::isError($download))
  162. {
  163. throw new sfPluginException(sprintf('Problem downloading the plugin "%s": %s', $plugin, $download->getMessage()));
  164. }
  165. }
  166. // download the plugin and install
  167. $class = $this->environment->getOption('downloader_base_class');
  168. $downloader = new $class($this, array('upgrade' => true), $this->environment->getConfig());
  169. $this->installing[$channel.'/'.$plugin] = true;
  170. if ($isPackage)
  171. {
  172. $this->checkPluginDependencies($plugin, $version, isset($options['install_deps']) ? (bool) $options['install_deps'] : false);
  173. }
  174. // download the actual URL to the plugin
  175. $downloaded = $downloader->download(array($download));
  176. if (PEAR::isError($downloaded))
  177. {
  178. throw new sfPluginException(sprintf('Problem when downloading "%s": %s', $download, $downloaded->getMessage()));
  179. }
  180. $errors = $downloader->getErrorMsgs();
  181. if (count($errors))
  182. {
  183. $err = array();
  184. foreach ($errors as $error)
  185. {
  186. $err[] = $error;
  187. }
  188. if (!count($downloaded))
  189. {
  190. throw new sfPluginException(sprintf('Plugin "%s" installation failed: %s', $plugin, implode("\n", $err)));
  191. }
  192. }
  193. $pluginPackage = $downloaded[0];
  194. $installer = new PEAR_Installer($this);
  195. $installer->setOptions(array('upgrade' => true));
  196. $packages = array($pluginPackage);
  197. $installer->sortPackagesForInstall($packages);
  198. PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
  199. $err = $installer->setDownloadedPackages($packages);
  200. if (PEAR::isError($err))
  201. {
  202. PEAR::staticPopErrorHandling();
  203. throw new sfPluginException($err->getMessage());
  204. }
  205. $info = $installer->install($pluginPackage, array('upgrade' => true));
  206. PEAR::staticPopErrorHandling();
  207. if (PEAR::isError($info))
  208. {
  209. throw new sfPluginException(sprintf('Installation of "%s" plugin failed: %s', $plugin, $info->getMessage()));
  210. }
  211. if (is_array($info))
  212. {
  213. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Installation successful for plugin "%s"', $plugin))));
  214. $this->dispatcher->notify(new sfEvent($this, 'plugin.post_install', array('channel' => $channel, 'plugin' => $plugin)));
  215. unset($this->installing[$channel.'/'.$plugin]);
  216. return true;
  217. }
  218. else
  219. {
  220. throw new sfPluginException(sprintf('Installation of "%s" plugin failed', $plugin));
  221. }
  222. }
  223. /**
  224. * Uninstalls a plugin.
  225. *
  226. * @param string $plugin The plugin name
  227. * @param string $channel The channel name
  228. */
  229. public function uninstallPlugin($plugin, $channel = null)
  230. {
  231. if (false !== strpos($plugin, '/'))
  232. {
  233. list($channel, $plugin) = explode('/', $plugin);
  234. }
  235. $channel = is_null($channel) ? $this->environment->getConfig()->get('default_channel') : $channel;
  236. $existing = $this->environment->getRegistry()->packageInfo($plugin, 'version', $channel);
  237. if (is_null($existing))
  238. {
  239. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Plugin "%s" is not installed', $plugin))));
  240. return false;
  241. }
  242. $this->dispatcher->notify(new sfEvent($this, 'plugin.pre_uninstall', array('channel' => $channel, 'plugin' => $plugin)));
  243. $package = $this->environment->getRegistry()->parsePackageName($plugin, $channel);
  244. $installer = new PEAR_Installer($this);
  245. $packages = array($this->environment->getRegistry()->getPackage($plugin, $channel));
  246. $installer->setUninstallPackages($packages);
  247. $ret = $installer->uninstall($package);
  248. if (PEAR::isError($ret))
  249. {
  250. throw new sfPluginException(sprintf('Problem uninstalling plugin "%s": %s', $plugin, $ret->getMessage()));
  251. }
  252. if ($ret)
  253. {
  254. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Uninstallation successful for plugin "%s"', $plugin))));
  255. $this->dispatcher->notify(new sfEvent($this, 'plugin.post_uninstall', array('channel' => $channel, 'plugin' => $plugin)));
  256. }
  257. else
  258. {
  259. throw new sfPluginException(sprintf('Uninstallation of "%s" plugin failed', $plugin));
  260. }
  261. return $ret;
  262. }
  263. /**
  264. * Checks all plugin dependencies.
  265. *
  266. * @param string $plugin The plugin name
  267. * @param string $version The plugin version
  268. * @param Boolean $install true if dependencies must be installed, false otherwise
  269. */
  270. public function checkPluginDependencies($plugin, $version, $install = false)
  271. {
  272. $dependencies = $this->environment->getRest()->getPluginDependencies($plugin, $version);
  273. if (!isset($dependencies['required']) || !isset($dependencies['required']['package']))
  274. {
  275. return;
  276. }
  277. $deps = $dependencies['required']['package'];
  278. if (!isset($deps[0]))
  279. {
  280. $deps = array($deps);
  281. }
  282. foreach ($deps as $dependency)
  283. {
  284. if (!$this->checkDependency($dependency))
  285. {
  286. $version = (isset($dependency['min']) ? ' >= '.$dependency['min'] : '').(isset($dependency['max']) ? ' <= '.$dependency['max'] : '').(isset($dependency['exclude']) ? ' exclude '.$dependency['exclude'] : '');
  287. if ($install)
  288. {
  289. try
  290. {
  291. $this->doInstallPlugin($dependency['name'], array('channel' => $dependency['channel'], 'install_deps' => true));
  292. }
  293. catch (sfException $e)
  294. {
  295. throw new sfPluginRecursiveDependencyException(sprintf('Unable to install plugin "%s" (version %s) because it depends on plugin "%s" which cannot be installed automatically: %s', $plugin, $version, $dependency['name'], $e->getMessage()));
  296. }
  297. continue;
  298. }
  299. throw new sfPluginDependencyException(sprintf('Unable to install plugin "%s" (version %s) because it depends on plugin "%s" which is not installed (install dependencies by hand or use the --install_deps option for automatic installation).', $plugin, $version, $dependency['name']));
  300. }
  301. }
  302. }
  303. /**
  304. * Gets the "best" version available for a given plugin.
  305. *
  306. * @param string $plugin The plugin name
  307. * @param string $stability The stability name
  308. *
  309. * @return string The version
  310. */
  311. public function getPluginVersion($plugin, $stability = null)
  312. {
  313. $versions = $this->environment->getRest()->getPluginVersions($plugin, $stability);
  314. foreach ($versions as $version)
  315. {
  316. if (!$this->isPluginCompatible($plugin, $version))
  317. {
  318. continue;
  319. }
  320. return $version;
  321. }
  322. throw new sfPluginDependencyException(sprintf('No release available for plugin "%s" in state "%s" that satisfies the application requirements.', $plugin, $stability));
  323. }
  324. /**
  325. * Returns true if the plugin is comptatible with your environment.
  326. *
  327. * @param string $plugin The plugin name
  328. * @param string $version The plugin version
  329. *
  330. * @return Boolean true if the plugin is compatible, false otherwise
  331. */
  332. public function isPluginCompatible($plugin, $version)
  333. {
  334. $dependencies = $this->environment->getRest()->getPluginDependencies($plugin, $version);
  335. if (!isset($dependencies['required']) || !isset($dependencies['required']['package']))
  336. {
  337. return true;
  338. }
  339. $deps = $dependencies['required']['package'];
  340. if (!isset($deps[0]))
  341. {
  342. $deps = array($deps);
  343. }
  344. foreach ($deps as $dependency)
  345. {
  346. if (!$this->isPluginCompatibleWithDependency($dependency))
  347. {
  348. return false;
  349. }
  350. }
  351. return true;
  352. }
  353. /**
  354. * Returns the license for a given plugin.
  355. *
  356. * @param string $plugin The plugin name
  357. * @param array $options An array of options
  358. *
  359. * @return string The license
  360. *
  361. * @see installPlugin() for available options
  362. */
  363. public function getPluginLicense($plugin, $options = array())
  364. {
  365. $channel = isset($options['channel']) ? $options['channel'] : $this->environment->getConfig()->get('default_channel');
  366. $stability = isset($options['stability']) ? $options['stability'] : $this->environment->getConfig()->get('preferred_state', null, $channel);
  367. $version = isset($options['version']) ? $options['version'] : null;
  368. $rest = $this->environment->getRest();
  369. $rest->setChannel(is_null($channel) ? $this->environment->getConfig()->get('default_channel') : $channel);
  370. if (is_null($version))
  371. {
  372. try
  373. {
  374. $version = $this->getPluginVersion($plugin, $stability);
  375. }
  376. catch (Exception $e)
  377. {
  378. // no release available
  379. return false;
  380. }
  381. }
  382. else
  383. {
  384. if (!$this->isPluginCompatible($plugin, $version))
  385. {
  386. throw new sfPluginDependencyException(sprintf('Plugin "%s" in version "%s" is not compatible with the current application', $plugin, $version));
  387. }
  388. }
  389. return $rest->getPluginLicense($plugin, $version);
  390. }
  391. /**
  392. * Returns true if the plugin is comptatible with the dependency.
  393. *
  394. * @param array $dependency An dependency array
  395. *
  396. * @return Boolean true if the plugin is compatible, false otherwise
  397. */
  398. protected function isPluginCompatibleWithDependency($dependency)
  399. {
  400. return true;
  401. }
  402. /**
  403. * Checks that the dependency is valid.
  404. *
  405. * @param array $dependency A dependency array
  406. *
  407. * @return Boolean true if the dependency is valid, false otherwise
  408. */
  409. protected function checkDependency($dependency)
  410. {
  411. $dependencyChecker = new PEAR_Dependency2($this->environment->getConfig(), array(), array('package' => '', 'channel' => ''));
  412. PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
  413. $e = $dependencyChecker->validatePackageDependency($dependency, true, array());
  414. PEAR::staticPopErrorHandling();
  415. if (PEAR::isError($e))
  416. {
  417. return false;
  418. }
  419. return true;
  420. }
  421. }