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 // +----------------------------------------------------------------------+
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>
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');
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
44 * Main repository, must be an Apache mod_svn interface to subversion. Defaults to AK_PLUGINS_MAIN_REPOSITORY.
48 var $main_repository = AK_PLUGINS_MAIN_REPOSITORY
;
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
59 var $respository_discovery_page = AK_PLUGINS_REPOSITORY_DISCOVERY_PAGE
;
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
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
;
95 * Ads a repository to the know repositories list.
97 * @param string $repository_path An Apache mod_svn interface to subversion.
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
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)){
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"
141 function getPlugins($force_update = false)
143 if($force_update ||
!is_file($this->_getRepositoriesCahePath()) ||
filemtime($this->_getRepositoriesCahePath()) > 3600){
144 if(!$this->_updateRemotePluginsList()){
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
160 function getInstalledPlugins()
162 $Loader = new AkPluginLoader();
163 return $Loader->getAvailablePlugins();
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.
187 function installPlugin($plugin_name, $repository = null, $options = array())
189 $default_options = array(
190 'externals' => false,
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
);
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()){
222 }elseif(!empty($options['checkout']) && $this->_shouldUseSvnCheckout()){
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.
249 function updatePlugin($plugin_name, $repository = null)
252 'externals' => 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
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
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
309 function getRepositoryForPlugin($plugin_name, $repository = null)
311 if(empty($repository)){
312 $available_plugins = $this->getPlugins();
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
);
321 }elseif (empty($repository)){
322 $repository = $available_plugins[$plugin_name];
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
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
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
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
416 function _copyRemoteFile($source, $destination)
418 Ak
::file_put_contents($destination, Ak
::url_get_contents($source));
424 * Performs an update of available cached plugins.
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
);
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'
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
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
499 function _getRepositoriesConfigPath()
501 if(empty($this->tmp_repositories
)){
502 return AK_CONFIG_DIR
.DS
.'plugin_repositories.txt';
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'
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')){
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;
635 function _installUsingHttp($name, $uri)
637 $this->_copyRemoteDir(rtrim($uri, '/').'/'.$name.'/', AK_PLUGINS_DIR
);