Fixing bug (no index on array warning) on the plugin manager introduced on [822]
[akelos.git] / lib / AkPlugin / AkPluginManager.php
blobbb8fa7ad5042a0a0a43434e23a57477dde8205bc
1 <?php
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
5 // +----------------------------------------------------------------------+
6 // | Akelos Framework - http://www.akelos.org |
7 // +----------------------------------------------------------------------+
8 // | Copyright (c) 2002-2006, Akelos Media, S.L. & Bermi Ferrer Martinez |
9 // | Released under the GNU Lesser General Public License, see LICENSE.txt|
10 // +----------------------------------------------------------------------+
13 /**
14 * Plugin manager
16 * @package Plugins
17 * @subpackage Manager
18 * @author Bermi Ferrer <bermi a.t akelos c.om> 2007
19 * @copyright Copyright (c) 2002-2007, Akelos Media, S.L. http://www.akelos.org
20 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
23 @set_time_limit(0);
24 @ini_set('memory_limit', -1);
26 require_once(AK_LIB_DIR.DS.'AkPlugin.php');
28 defined('AK_PLUGINS_MAIN_REPOSITORY') ? null : define('AK_PLUGINS_MAIN_REPOSITORY', 'http://svn.akelos.org/plugins');
29 defined('AK_PLUGINS_REPOSITORY_DISCOVERY_PAGE') ? null : define('AK_PLUGINS_REPOSITORY_DISCOVERY_PAGE', 'http://wiki.akelos.org/plugins');
31 /**
32 * Plugin manager
34 * @package Plugins
35 * @subpackage Manager
36 * @author Bermi Ferrer <bermi a.t akelos c.om> 2007
37 * @copyright Copyright (c) 2002-2007, Akelos Media, S.L. http://www.akelos.org
38 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
40 class AkPluginManager extends AkObject
43 /**
44 * Main repository, must be an Apache mod_svn interface to subversion. Defaults to AK_PLUGINS_MAIN_REPOSITORY.
45 * @var string
46 * @access public
48 var $main_repository = AK_PLUGINS_MAIN_REPOSITORY;
50 /**
51 * Repository discovery page.
53 * A wiki page containing links to repositories. Links on that wiki page
54 * must link to an http:// protocol (no SSL yet) and end in plugins.
55 * Defaults to AK_PLUGINS_REPOSITORY_DISCOVERY_PAGE
56 * @var string
57 * @access public
59 var $respository_discovery_page = AK_PLUGINS_REPOSITORY_DISCOVERY_PAGE;
63 /**
64 * Gets a list of available repositories.
66 * @param boolean $force_reload Forces reloading, useful for testing and when running as an application server.
67 * @return array List of repository URLs
68 * @access public
70 function getAvailableRepositories($force_reload = false)
72 if(!empty($this->tmp_repositories)){
73 return $this->tmp_repositories;
76 if($force_reload || empty($this->repositories)){
77 $this->repositories = array($this->main_repository);
78 if(file_exists($this->_getRepositoriesConfigPath())){
79 $repository_candidates = array_diff(array_map('trim', explode("\n",Ak::file_get_contents($this->_getRepositoriesConfigPath()))), array(''));
80 if(!empty($repository_candidates)){
81 foreach ($repository_candidates as $repository_candidate){
82 if(strlen($repository_candidate) > 0 && $repository_candidate[0] != '#' && strstr($repository_candidate,'plugins')){
83 $this->repositories[] = $repository_candidate;
89 return $this->repositories;
94 /**
95 * Ads a repository to the know repositories list.
97 * @param string $repository_path An Apache mod_svn interface to subversion.
98 * @return void
99 * @access public
101 function addRepository($repository_path)
103 if(!in_array(trim($repository_path), $this->getAvailableRepositories(true))){
104 Ak::file_add_contents($this->_getRepositoriesConfigPath(), $repository_path."\n");
111 * Removes a repository to the know repositories list.
113 * @param string $repository_path An Apache mod_svn interface to subversion.
114 * @return boolean Returns false if the repository was not available
115 * @access public
117 function removeRepository($repository_path)
119 if(file_exists($this->_getRepositoriesConfigPath())){
120 $repositories = Ak::file_get_contents($this->_getRepositoriesConfigPath());
121 if(!strstr($repositories, $repository_path)){
122 return false;
124 $repositories = str_replace(array($repository_path, "\r", "\n\n"), array('', "\n", "\n"), $repositories);
125 Ak::file_put_contents($this->_getRepositoriesConfigPath(), $repositories);
132 * Gets a list of available plugins.
134 * Goes through each trusted plugin server and retrieves the name of the
135 * folders (plugins) on the repository path.
137 * @param boolean $force_update If it is not set to true, it will only check remote sources once per hour
138 * @return array Returns an array containing "plugin_name" => "repository URL"
139 * @access public
141 function getPlugins($force_update = false)
143 if($force_update || !is_file($this->_getRepositoriesCahePath()) || filemtime($this->_getRepositoriesCahePath()) > 3600){
144 if(!$this->_updateRemotePluginsList()){
145 return array();
149 return array_map('trim', Ak::convert('yaml', 'array', Ak::file_get_contents($this->_getRepositoriesCahePath())));
155 * Retrieves a list of installed plugins
157 * @return array Returns an array with the plugins available at AK_PLUGINS_DIR
158 * @access public
160 function getInstalledPlugins()
162 $Loader = new AkPluginLoader();
163 return $Loader->getAvailablePlugins();
169 * Installs a plugin
171 * Install a plugin from a remote resource.
173 * Plugins can have an Akelos installer at located at "plugin_name/installer/plugin_name_installer.php"
174 * If the installer is available, it will run the "PluginNameInstaller::install()" method, which will trigger
175 * all the up_* methods for the installer.
177 * @param string $plugin_name Plugin name
178 * @param unknown $repository An Apache mod_svn interface to subversion. If not provided it will use a trusted repository.
179 * @param array $options
180 * - externals: Use svn:externals to grab the plugin. Enables plugin updates and plugin versioning.
181 * - checkout: Use svn checkout to grab the plugin. Enables updating but does not add a svn:externals entry.
182 * - revision: Checks out the given revision from subversion. Ignored if subversion is not used.
183 * - force: Overwrite existing files.
184 * @return mixed Returns false if the plugin can't be found.
185 * @access public
187 function installPlugin($plugin_name, $repository = null, $options = array())
189 $default_options = array(
190 'externals' => false,
191 'checkout' => false,
192 'force' => false,
193 'revision' => null,
196 $options = array_merge($default_options, $options);
198 $plugin_name = Ak::sanitize_include($plugin_name, 'high');
200 $install_method = $this->guessBestInstallMethod($options);
202 if($install_method != 'local directory'){
203 $repository = $this->getRepositoryForPlugin($plugin_name, $repository);
205 if(!$options['force'] && is_dir(AK_PLUGINS_DIR.DS.$plugin_name)){
206 trigger_error(Ak::t('Destination directory is not empty. Use force option to overwrite exiting files.'), E_USER_NOTICE);
207 }else{
208 $method = '_installUsing'.AkInflector::camelize($install_method);
209 $this->$method($plugin_name, rtrim($repository, '/'), $options['revision'], $options['force']);
210 $this->_runInstaller($plugin_name, 'install', $options);
215 function guessBestInstallMethod($options = array())
217 if(!empty($options['parameters']) && is_dir($options['parameters'])){
218 return 'local directory';
219 }elseif($this->canUseSvn()){
220 if(!empty($options['externals']) && $this->_shouldUseSvnExternals()){
221 return 'externals';
222 }elseif(!empty($options['checkout']) && $this->_shouldUseSvnCheckout()){
223 return 'checkout';
225 return 'export';
226 }else{
227 return 'http';
231 function canUseSvn()
233 return strstr(`svn --version`, 'CollabNet');
238 * Updates a plugin if there are changes.
240 * Uses subversion update if available. If http update is used, it will
241 * download the whole plugin unless there is a CHANGELOG file, in which case
242 * it will only perform the update if there are changes.
244 * @param string $plugin_name Plugin name
245 * @param string $repository An Apache mod_svn interface to subversion. If not provided it will use a trusted repository.
246 * @return null
247 * @access public
249 function updatePlugin($plugin_name, $repository = null)
251 $options = array(
252 'externals' => false,
253 'checkout' => false
256 $plugin_name = Ak::sanitize_include($plugin_name, 'high');
258 $method = '_updateUsing'.AkInflector::camelize($this->guessBestInstallMethod($options));
259 $this->$method($plugin_name, rtrim($this->getRepositoryForPlugin($plugin_name, $repository), '/'));
261 $this->_runInstaller($plugin_name, 'install');
266 * Uninstalls an existing plugin
268 * Plugins can have an Akelos installer at located at "plugin_name/installer/plugin_name_installer.php"
269 * If the installer is available, it will run the "PluginNameInstaller::uninstall()" method, which will trigger
270 * all the down_* methods for the installer.
272 * @param string $plugin_name Plugin name
273 * @return void
274 * @access public
276 function uninstallPlugin($plugin_name)
278 $plugin_name = Ak::sanitize_include($plugin_name, 'high');
279 $this->_runInstaller($plugin_name, 'uninstall');
280 if(is_dir(AK_PLUGINS_DIR.DS.$plugin_name)){
281 Ak::directory_delete(AK_PLUGINS_DIR.DS.$plugin_name);
283 if($this->_shouldUseSvnExternals()){
284 $this->_uninstallExternals($plugin_name);
290 * Gets a list of repositories available at the web page defined by AK_PLUGINS_REPOSITORY_DISCOVERY_PAGE (http://wiki.akelos.org/plugins by default)
292 * @return array An array of non trusted repositories available at http://wiki.akelos.org/plugins
293 * @access public
295 function getDiscoveredRepositories()
297 return array_diff($this->_getRepositoriesFromRemotePage(), $this->getAvailableRepositories(true));
302 * Returns the repository for a given $plugin_name
304 * @param string $plugin_name The name of the plugin
305 * @param string $repository If a repository name is provided it will check for the plugin name existance.
306 * @return mixed Repository URL or false if plugin can't be found
307 * @access public
309 function getRepositoryForPlugin($plugin_name, $repository = null)
311 if(empty($repository)){
312 $available_plugins = $this->getPlugins();
313 }else{
314 $available_plugins = array();
315 $this->_addAvailablePlugins_($repository, &$available_plugins);
318 if(empty($available_plugins[$plugin_name])){
319 trigger_error(Ak::t('Could not find %plugin_name plugin', array('%plugin_name' => $plugin_name)), E_USER_NOTICE);
320 return false;
321 }elseif (empty($repository)){
322 $repository = $available_plugins[$plugin_name];
324 return $repository;
328 * Runs the plugin installer/uninstaller if available
330 * Plugins can have an Akelos installer at located at "plugin_name/installer/plugin_name_installer.php"
331 * If the installer is available, it will run the "PluginNameInstaller::install/uninstall()" method, which will trigger
332 * all the up/down_* methods for the installer.
334 * @param string $plugin_name The name of the plugin
335 * @param string $install_or_uninstall What to do, options are install or uninstall
336 * @return void
337 * @access private
339 function _runInstaller($plugin_name, $install_or_uninstall = 'install', $options = array())
341 $plugin_dir = AK_PLUGINS_DIR.DS.$plugin_name;
342 if(file_exists($plugin_dir.DS.'installer'.DS.$plugin_name.'_installer.php')){
343 require_once(AK_LIB_DIR.DS.'AkInstaller.php');
344 require_once($plugin_dir.DS.'installer'.DS.$plugin_name.'_installer.php');
345 $class_name = AkInflector::camelize($plugin_name.'_installer');
346 if(class_exists($class_name)){
347 $Installer =& new $class_name();
348 $Installer->options = $options;
349 $Installer->db->debug = false;
350 $Installer->warn_if_same_version = false;
351 $Installer->$install_or_uninstall();
358 * Retrieves the URL's from the AK_PLUGINS_REPOSITORY_DISCOVERY_PAGE (http://wiki.akelos.org/plugins by default)
360 * Plugins in that page must follow this convention:
362 * * Only http:// protocol. No https:// or svn:// support yet
363 * * The URL must en in plugins to be fetched automatically
365 * @return array An array of existing repository URLs
366 * @access private
368 function _getRepositoriesFromRemotePage()
371 $repositories = array();
372 if(preg_match_all('/href="(http:\/\/(?!wiki\.akelos\.org)[^"]*plugins)/', Ak::url_get_contents($this->respository_discovery_page), $matches)){
373 $repositories = array_unique($matches[1]);
375 return $repositories;
379 * Copy recursively a remote svn dir into a local path.
381 * Downloads recursively the contents of remote directories from a mod_svn Apache subversion interface to a local destination.
383 * File or directory permissions are not copied, so you will need to use installers to fix it if required.
385 * @param string $source An Apache mod_svn interface to subversion URL.
386 * @param string $destination Destination directory
387 * @return void
388 * @access private
390 function _copyRemoteDir($source, $destination)
392 $dir_name = trim(substr($source, strrpos(rtrim($source, '/'), '/')),'/');
393 Ak::make_dir($destination.DS.$dir_name);
395 list($directories, $files) = $this->_parseRemoteAndGetDirectoriesAndFiles($source);
397 foreach ($files as $file){
398 $this->_copyRemoteFile($source.$file, $destination.DS.$dir_name.DS.$file);
401 foreach ($directories as $directory){
402 $this->_copyRemoteDir($source.$directory.'/', $destination.DS.$dir_name);
409 * Copies a remote file into a local destination
411 * @param string $source Source URL
412 * @param string $destination Destination directory
413 * @return void
414 * @access private
416 function _copyRemoteFile($source, $destination)
418 Ak::file_put_contents($destination, Ak::url_get_contents($source));
424 * Performs an update of available cached plugins.
426 * @return boolean
427 * @access private
429 function _updateRemotePluginsList()
431 $new_plugins = array();
432 foreach ($this->getAvailableRepositories() as $repository){
433 $this->_addAvailablePlugins_($repository, $new_plugins);
435 if(empty($new_plugins)){
436 trigger_error(Ak::t('Could not fetch remote plugins from one of these repositories: %repositories', array('%repositories' => "\n".join("\n", $this->getAvailableRepositories()))), E_USER_NOTICE);
437 return false;
439 return Ak::file_put_contents($this->_getRepositoriesCahePath(), Ak::convert('array', 'yaml', $new_plugins));
445 * Modifies $plugins_list adding the plugins available at $repository
447 * @param string $repository Repository URL
448 * @param array $plugins_list Plugins list in the format 'plugin_name' => 'repository'
449 * @return void
450 * @access private
452 function _addAvailablePlugins_($repository, &$plugins_list)
454 list($directories) = $this->_parseRemoteAndGetDirectoriesAndFiles($repository);
455 foreach ($directories as $plugin){
456 if(empty($plugins_list[$plugin])){
457 $plugins_list[$plugin] = $repository;
465 * Parses a remote Apache svn web page and returns a list of available files and directories
467 * @param string $remote_path Repository URL
468 * @return array an array like array($directories, $files). Use list($directories, $files) = $this->_parseRemoteAndGetDirectoriesAndFiles($remote_path) for getting the results of this method
469 * @access private
471 function _parseRemoteAndGetDirectoriesAndFiles($remote_path)
473 $directories = $files = array();
474 $remote_contents = Ak::url_get_contents(rtrim($remote_path, '/').'/');
476 if(preg_match_all('/href="([A-Za-z\-_0-9]+)\/"/', $remote_contents, $matches)){
477 foreach ($matches[1] as $directory){
478 $directories[] = trim($directory);
481 if(preg_match_all('/href="(\.?[A-Za-z\-_0-9\.]+)"/', $remote_contents, $matches)){
482 foreach ($matches[1] as $file){
483 $files[] = trim($file);
486 return array($directories, $files);
492 * Trusted repositories location
494 * By default trusted repositories are located at config/plugin_repositories.txt
496 * @return string Trusted repositories path
497 * @access private
499 function _getRepositoriesConfigPath()
501 if(empty($this->tmp_repositories)){
502 return AK_CONFIG_DIR.DS.'plugin_repositories.txt';
503 }else{
504 return AK_TMP_DIR.DS.'plugin_repositories.'.md5(serialize($this->tmp_repositories));
511 * Cached informations about available plugins
513 * @return string Plugin information cache path. By default AK_TMP_DIR.DS.'plugin_repositories.yaml'
514 * @access private
516 function _getRepositoriesCahePath()
518 return AK_TMP_DIR.DS.'plugin_repositories.yaml';
523 function _shouldUseSvnExternals()
525 return is_dir(AK_PLUGINS_DIR.DS.'.svn');
528 function _shouldUseSvnCheckout()
530 return is_dir(AK_PLUGINS_DIR.DS.'.svn');
533 function _installUsingCheckout($name, $uri, $rev = null, $force = false)
535 $rev = empty($rev) ? '' : " -r $rev ";
536 $force = $force ? ' --force ' : '';
537 $plugin_dir = AK_PLUGINS_DIR.DS.$name;
538 `svn co $force $rev $uri/$name $plugin_dir`;
541 function _updateUsingCheckout($name)
543 $plugin_dir = AK_PLUGINS_DIR.DS.$name;
544 `svn update $plugin_dir`;
547 function _installUsingLocalDirectory($name, $path, $rev = null)
549 $source = $path.DS.$name;
550 $plugin_dir = AK_PLUGINS_DIR;
551 $command = AK_OS == 'UNIX' ? 'cp -rf ' : 'xcopy /h /r /k /x /y /S /E ';
552 `$command $source $plugin_dir`;
555 function _updateUsingLocalDirectory($name)
557 trigger_error(Ak::t('Updating from local targets it\'s not supported yet. Please use install --force instead.'));
560 function _installUsingExport($name, $uri, $rev = null, $force = false)
562 $rev = empty($rev) ? '' : " -r $rev ";
563 $force = $force ? ' --force ' : '';
564 $plugin_dir = AK_PLUGINS_DIR.DS.$name;
565 `svn export $force $rev $uri/$name $plugin_dir`;
568 function _updateUsingExport($name, $uri)
570 $plugin_dir = AK_PLUGINS_DIR.DS.$name;
571 `svn export --force $uri/$name $plugin_dir`;
574 function _installUsingExternals($name, $uri, $rev = null, $force = false)
576 $extras = empty($rev) ? '' : " -r $rev ";
577 $extras .= ($force ? ' --force ' : '');
578 $externals = $this->_getExternals();
579 $externals[$name] = $uri;
580 $this->_setExternals($externals, $extras);
581 $this->_installUsingCheckout($name, $uri, $rev, $force);
584 function _updateUsingExternals($name)
586 $this->_updateUsingCheckout($name);
589 function _updateUsingHttp($name, $uri)
591 if(is_file(AK_PLUGINS_DIR.DS.$name.DS.'CHANGELOG') &&
592 md5(Ak::url_get_contents(rtrim($uri, '/').'/'.$name.'/CHANGELOG')) == md5_file(AK_PLUGINS_DIR.DS.$name.DS.'CHANGELOG')){
593 return false;
595 $this->_copyRemoteDir(rtrim($uri, '/').'/'.$name.'/', AK_PLUGINS_DIR);
599 function _setExternals($items, $extras = '')
601 $externals = array();
602 foreach ($items as $name => $uri){
603 $externals[] = "$name ".rtrim($uri, '/');
605 $tmp_file = AK_TMP_DIR.DS.Ak::uuid();
606 $plugins_dir = AK_PLUGINS_DIR;
607 Ak::file_put_contents($tmp_file, join("\n", $externals));
608 `svn propset $extras -q svn:externals -F "$tmp_file" "$plugins_dir"`;
609 Ak::file_delete($tmp_file);
612 function _uninstallExternals($name)
614 $externals = $this->_getExternals();
615 unset($externals[$name]);
616 $this->_setExternals($externals);
619 function _getExternals()
621 if($this->_shouldUseSvnExternals()){
622 $plugins_dir = AK_PLUGINS_DIR;
623 $svn_externals = array_diff(array_map('trim',(array)explode("\n", `svn propget svn:externals "$plugins_dir"`)), array(''));
624 $externals = array();
625 foreach ($svn_externals as $svn_external){
626 list($name, $uri) = explode(' ', trim($svn_external));
627 $externals[$name] = $uri;
629 return $externals;
630 }else{
631 return array();
635 function _installUsingHttp($name, $uri)
637 $this->_copyRemoteDir(rtrim($uri, '/').'/'.$name.'/', AK_PLUGINS_DIR);