diff options
author | Frederic Guillot <fred@kanboard.net> | 2016-05-20 12:51:05 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2016-05-20 12:51:05 -0400 |
commit | 8d69c49da595c60dae51c77d48f397ab97fdf318 (patch) | |
tree | 7fba4edb18c5c4c161e76828d5847733aca8d27b | |
parent | cbf896e74e666f102f475787202d3402f229a919 (diff) |
Manage plugins from the user interface and from the command line
-rw-r--r-- | ChangeLog | 1 | ||||
-rw-r--r-- | app/Console/BaseCommand.php | 2 | ||||
-rw-r--r-- | app/Console/PluginInstallCommand.php | 35 | ||||
-rw-r--r-- | app/Console/PluginUninstallCommand.php | 35 | ||||
-rw-r--r-- | app/Console/PluginUpgradeCommand.php | 53 | ||||
-rw-r--r-- | app/Controller/PluginController.php | 93 | ||||
-rw-r--r-- | app/Core/Plugin/Installer.php | 162 | ||||
-rw-r--r-- | app/Core/Plugin/Loader.php | 15 | ||||
-rw-r--r-- | app/Core/Plugin/PluginInstallerException.php | 15 | ||||
-rw-r--r-- | app/ServiceProvider/RouteProvider.php | 2 | ||||
-rw-r--r-- | app/Template/board/table_column.php | 2 | ||||
-rw-r--r-- | app/Template/plugin/directory.php | 65 | ||||
-rw-r--r-- | app/Template/plugin/remove.php | 13 | ||||
-rw-r--r-- | app/Template/plugin/show.php | 22 | ||||
-rw-r--r-- | app/constants.php | 6 | ||||
-rw-r--r-- | doc/cli.markdown | 26 | ||||
-rw-r--r-- | doc/config.markdown | 13 | ||||
-rw-r--r-- | doc/index.markdown | 1 | ||||
-rw-r--r-- | doc/plugin-directory.markdown | 15 | ||||
-rwxr-xr-x | kanboard | 48 |
20 files changed, 563 insertions, 61 deletions
@@ -3,6 +3,7 @@ Version 1.0.29 (unreleased) New features: +* Manage plugin from the user interface and from the command line * Added the possibility to convert a subtask to a task * Added menu entry to add tasks from all project views * Add tasks in bulk from the board diff --git a/app/Console/BaseCommand.php b/app/Console/BaseCommand.php index 4444ceba..ca566266 100644 --- a/app/Console/BaseCommand.php +++ b/app/Console/BaseCommand.php @@ -26,6 +26,8 @@ use Symfony\Component\Console\Command\Command; * @property \Kanboard\Model\UserNotification $userNotification * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter * @property \Kanboard\Model\ProjectUserRole $projectUserRole + * @property \Kanboard\Core\Plugin\Loader $pluginLoader + * @property \Kanboard\Core\Http\Client $httpClient * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */ abstract class BaseCommand extends Command diff --git a/app/Console/PluginInstallCommand.php b/app/Console/PluginInstallCommand.php new file mode 100644 index 00000000..1c6e14b3 --- /dev/null +++ b/app/Console/PluginInstallCommand.php @@ -0,0 +1,35 @@ +<?php + +namespace Kanboard\Console; + +use Kanboard\Core\Plugin\Installer; +use Kanboard\Core\Plugin\PluginInstallerException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class PluginInstallCommand extends BaseCommand +{ + protected function configure() + { + $this + ->setName('plugin:install') + ->setDescription('Install a plugin from a remote Zip archive') + ->addArgument('url', InputArgument::REQUIRED, 'Archive URL'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!Installer::isConfigured()) { + $output->writeln('<error>Kanboard is not configured to install plugins itself</error>'); + } + + try { + $installer = new Installer($this->container); + $installer->install($input->getArgument('url')); + $output->writeln('<info>Plugin installed successfully</info>'); + } catch (PluginInstallerException $e) { + $output->writeln('<error>'.$e->getMessage().'</error>'); + } + } +} diff --git a/app/Console/PluginUninstallCommand.php b/app/Console/PluginUninstallCommand.php new file mode 100644 index 00000000..c645e03f --- /dev/null +++ b/app/Console/PluginUninstallCommand.php @@ -0,0 +1,35 @@ +<?php + +namespace Kanboard\Console; + +use Kanboard\Core\Plugin\Installer; +use Kanboard\Core\Plugin\PluginInstallerException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class PluginUninstallCommand extends BaseCommand +{ + protected function configure() + { + $this + ->setName('plugin:uninstall') + ->setDescription('Remove a plugin') + ->addArgument('pluginId', InputArgument::REQUIRED, 'Plugin directory name'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!Installer::isConfigured()) { + $output->writeln('<error>Kanboard is not configured to remove plugins itself</error>'); + } + + try { + $installer = new Installer($this->container); + $installer->uninstall($input->getArgument('pluginId')); + $output->writeln('<info>Plugin removed successfully</info>'); + } catch (PluginInstallerException $e) { + $output->writeln('<error>'.$e->getMessage().'</error>'); + } + } +} diff --git a/app/Console/PluginUpgradeCommand.php b/app/Console/PluginUpgradeCommand.php new file mode 100644 index 00000000..6ec5836d --- /dev/null +++ b/app/Console/PluginUpgradeCommand.php @@ -0,0 +1,53 @@ +<?php + +namespace Kanboard\Console; + +use Kanboard\Core\Plugin\Base as BasePlugin; +use Kanboard\Core\Plugin\Installer; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class PluginUpgradeCommand extends BaseCommand +{ + protected function configure() + { + $this + ->setName('plugin:upgrade') + ->setDescription('Update all installed plugins') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!Installer::isConfigured()) { + $output->writeln('<error>Kanboard is not configured to upgrade plugins itself</error>'); + } + + $installer = new Installer($this->container); + $availablePlugins = $this->httpClient->getJson(PLUGIN_API_URL); + + foreach ($this->pluginLoader->getPlugins() as $installedPlugin) { + $pluginDetails = $this->getPluginDetails($availablePlugins, $installedPlugin); + + if ($pluginDetails === null) { + $output->writeln('<error>* Plugin not available in the directory: '.$installedPlugin->getPluginName().'</error>'); + } elseif ($pluginDetails['version'] > $installedPlugin->getPluginVersion()) { + $output->writeln('<comment>* Updating plugin: '.$installedPlugin->getPluginName().'</comment>'); + $installer->update($pluginDetails['download']); + } else { + $output->writeln('<info>* Plugin up to date: '.$installedPlugin->getPluginName().'</info>'); + } + } + } + + protected function getPluginDetails(array $availablePlugins, BasePlugin $installedPlugin) + { + foreach ($availablePlugins as $availablePlugin) { + if ($availablePlugin['title'] === $installedPlugin->getPluginName()) { + return $availablePlugin; + } + } + + return null; + } +} diff --git a/app/Controller/PluginController.php b/app/Controller/PluginController.php index 8d5628f1..b6f9a33b 100644 --- a/app/Controller/PluginController.php +++ b/app/Controller/PluginController.php @@ -2,6 +2,9 @@ namespace Kanboard\Controller; +use Kanboard\Core\Plugin\Installer; +use Kanboard\Core\Plugin\PluginInstallerException; + /** * Class PluginController * @@ -18,8 +21,9 @@ class PluginController extends BaseController public function show() { $this->response->html($this->helper->layout->plugin('plugin/show', array( - 'plugins' => $this->pluginLoader->plugins, + 'plugins' => $this->pluginLoader->getPlugins(), 'title' => t('Installed Plugins'), + 'is_configured' => Installer::isConfigured(), ))); } @@ -28,11 +32,94 @@ class PluginController extends BaseController */ public function directory() { - $plugins = $this->httpClient->getJson(PLUGIN_API_URL); + $installedPlugins = array(); + + foreach ($this->pluginLoader->getPlugins() as $plugin) { + $installedPlugins[$plugin->getPluginName()] = $plugin->getPluginVersion(); + } $this->response->html($this->helper->layout->plugin('plugin/directory', array( - 'plugins' => $plugins, + 'installed_plugins' => $installedPlugins, + 'available_plugins' => $this->httpClient->getJson(PLUGIN_API_URL), 'title' => t('Plugin Directory'), + 'is_configured' => Installer::isConfigured(), ))); } + + /** + * Install plugin from URL + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + */ + public function install() + { + $this->checkCSRFParam(); + $pluginArchiveUrl = urldecode($this->request->getStringParam('archive_url')); + + try { + $installer = new Installer($this->container); + $installer->install($pluginArchiveUrl); + $this->flash->success(t('Plugin installed successfully.')); + } catch (PluginInstallerException $e) { + $this->flash->failure($e->getMessage()); + } + + $this->response->redirect($this->helper->url->to('PluginController', 'show')); + } + + /** + * Update plugin from URL + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + */ + public function update() + { + $this->checkCSRFParam(); + $pluginArchiveUrl = urldecode($this->request->getStringParam('archive_url')); + + try { + $installer = new Installer($this->container); + $installer->update($pluginArchiveUrl); + $this->flash->success(t('Plugin updated successfully.')); + } catch (PluginInstallerException $e) { + $this->flash->failure($e->getMessage()); + } + + $this->response->redirect($this->helper->url->to('PluginController', 'show')); + } + + /** + * Confirmation before to remove the plugin + */ + public function confirm() + { + $pluginId = $this->request->getStringParam('pluginId'); + $plugins = $this->pluginLoader->getPlugins(); + + $this->response->html($this->template->render('plugin/remove', array( + 'plugin_id' => $pluginId, + 'plugin' => $plugins[$pluginId], + ))); + } + + /** + * Remove a plugin + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + */ + public function uninstall() + { + $this->checkCSRFParam(); + $pluginId = $this->request->getStringParam('pluginId'); + + try { + $installer = new Installer($this->container); + $installer->uninstall($pluginId); + $this->flash->success(t('Plugin removed successfully.')); + } catch (PluginInstallerException $e) { + $this->flash->failure($e->getMessage()); + } + + $this->response->redirect($this->helper->url->to('PluginController', 'show')); + } } diff --git a/app/Core/Plugin/Installer.php b/app/Core/Plugin/Installer.php new file mode 100644 index 00000000..48c4d978 --- /dev/null +++ b/app/Core/Plugin/Installer.php @@ -0,0 +1,162 @@ +<?php + +namespace Kanboard\Core\Plugin; + +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use ZipArchive; + +/** + * Class Installer + * + * @package Kanboard\Core\Plugin + * @author Frederic Guillot + */ +class Installer extends \Kanboard\Core\Base +{ + /** + * Return true if Kanboard is configured to install plugins + * + * @static + * @access public + * @return bool + */ + public static function isConfigured() + { + return PLUGIN_INSTALLER && is_writable(PLUGINS_DIR) && extension_loaded('zip'); + } + + /** + * Install a plugin + * + * @access public + * @param string $archiveUrl + * @throws PluginInstallerException + */ + public function install($archiveUrl) + { + $zip = $this->downloadPluginArchive($archiveUrl); + + if (! $zip->extractTo(PLUGINS_DIR)) { + $this->cleanupArchive($zip); + throw new PluginInstallerException(t('Unable to extract plugin archive.')); + } + + $this->cleanupArchive($zip); + } + + /** + * Uninstall a plugin + * + * @access public + * @param string $pluginId + * @throws PluginInstallerException + */ + public function uninstall($pluginId) + { + $pluginFolder = PLUGINS_DIR.DIRECTORY_SEPARATOR.basename($pluginId); + + if (! file_exists($pluginFolder)) { + throw new PluginInstallerException(t('Plugin not found.')); + } + + if (! is_writable($pluginFolder)) { + throw new PluginInstallerException(e('You don\'t have the permission to remove this plugin.')); + } + + $this->removeAllDirectories($pluginFolder); + } + + /** + * Update a plugin + * + * @access public + * @param string $archiveUrl + * @throws PluginInstallerException + */ + public function update($archiveUrl) + { + $zip = $this->downloadPluginArchive($archiveUrl); + + $firstEntry = $zip->statIndex(0); + $this->uninstall($firstEntry['name']); + + if (! $zip->extractTo(PLUGINS_DIR)) { + $this->cleanupArchive($zip); + throw new PluginInstallerException(t('Unable to extract plugin archive.')); + } + + $this->cleanupArchive($zip); + } + + /** + * Download archive from URL + * + * @access protected + * @param string $archiveUrl + * @return ZipArchive + * @throws PluginInstallerException + */ + protected function downloadPluginArchive($archiveUrl) + { + $zip = new ZipArchive(); + $archiveData = $this->httpClient->get($archiveUrl); + $archiveFile = tempnam(sys_get_temp_dir(), 'kb_plugin'); + + if (empty($archiveData)) { + unlink($archiveFile); + throw new PluginInstallerException(t('Unable to download plugin archive.')); + } + + if (file_put_contents($archiveFile, $archiveData) === false) { + unlink($archiveFile); + throw new PluginInstallerException(t('Unable to write temporary file for plugin.')); + } + + if ($zip->open($archiveFile) !== true) { + unlink($archiveFile); + throw new PluginInstallerException(t('Unable to open plugin archive.')); + } + + if ($zip->numFiles === 0) { + unlink($archiveFile); + throw new PluginInstallerException(t('There is no file in the plugin archive.')); + } + + return $zip; + } + + /** + * Remove archive file + * + * @access protected + * @param ZipArchive $zip + */ + protected function cleanupArchive(ZipArchive $zip) + { + unlink($zip->filename); + $zip->close(); + } + + /** + * Remove recursively a directory + * + * @access protected + * @param string $directory + */ + protected function removeAllDirectories($directory) + { + $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); + $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($files as $file) { + if ($file->isDir()) { + rmdir($file->getRealPath()); + } else { + unlink($file->getRealPath()); + } + } + + rmdir($directory); + } +} diff --git a/app/Core/Plugin/Loader.php b/app/Core/Plugin/Loader.php index 400517b7..f2f6add7 100644 --- a/app/Core/Plugin/Loader.php +++ b/app/Core/Plugin/Loader.php @@ -18,16 +18,16 @@ class Loader extends \Kanboard\Core\Base /** * Plugin instances * - * @access public + * @access protected * @var array */ - public $plugins = array(); + protected $plugins = array(); /** * Get list of loaded plugins * * @access public - * @return array + * @return Base[] */ public function getPlugins() { @@ -52,7 +52,7 @@ class Loader extends \Kanboard\Core\Base if ($fileInfo->isDir() && substr($fileInfo->getFilename(), 0, 1) !== '.') { $pluginName = $fileInfo->getFilename(); $this->loadSchema($pluginName); - $this->initializePlugin($this->loadPlugin($pluginName)); + $this->initializePlugin($pluginName, $this->loadPlugin($pluginName)); } } } @@ -95,9 +95,10 @@ class Loader extends \Kanboard\Core\Base * Initialize plugin * * @access public - * @param Base $plugin + * @param string $pluginName + * @param Base $plugin */ - public function initializePlugin(Base $plugin) + public function initializePlugin($pluginName, Base $plugin) { if (method_exists($plugin, 'onStartup')) { $this->dispatcher->addListener('app.bootstrap', array($plugin, 'onStartup')); @@ -107,6 +108,6 @@ class Loader extends \Kanboard\Core\Base Tool::buildDICHelpers($this->container, $plugin->getHelpers()); $plugin->initialize(); - $this->plugins[] = $plugin; + $this->plugins[$pluginName] = $plugin; } } diff --git a/app/Core/Plugin/PluginInstallerException.php b/app/Core/Plugin/PluginInstallerException.php new file mode 100644 index 00000000..7d356c9b --- /dev/null +++ b/app/Core/Plugin/PluginInstallerException.php @@ -0,0 +1,15 @@ +<?php + +namespace Kanboard\Core\Plugin; + +use Exception; + +/** + * Class PluginInstallerException + * + * @package Kanboard\Core\Plugin + * @author Frederic Guillot + */ +class PluginInstallerException extends Exception +{ +} diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php index 9001f176..a7338994 100644 --- a/app/ServiceProvider/RouteProvider.php +++ b/app/ServiceProvider/RouteProvider.php @@ -178,7 +178,7 @@ class RouteProvider implements ServiceProviderInterface // Plugins $container['route']->addRoute('extensions', 'PluginController', 'show'); - $container['route']->addRoute('extensions/list', 'PluginController', 'directory'); + $container['route']->addRoute('extensions/directory', 'PluginController', 'directory'); // Doc $container['route']->addRoute('documentation/:file', 'doc', 'show'); diff --git a/app/Template/board/table_column.php b/app/Template/board/table_column.php index a356849c..eced52dc 100644 --- a/app/Template/board/table_column.php +++ b/app/Template/board/table_column.php @@ -37,7 +37,7 @@ </li> <?php if ($this->user->hasProjectAccess('TaskCreationController', 'show', $column['project_id'])): ?> <li> - <i class="fa fa-align-justify" aria-hidden="true"></i> + <i class="fa fa-align-justify fa-fw" aria-hidden="true"></i> <?= $this->url->link(t('Create tasks in bulk'), 'TaskBulkController', 'show', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?> </li> <?php if ($column['nb_tasks'] > 0): ?> diff --git a/app/Template/plugin/directory.php b/app/Template/plugin/directory.php index 82b9a441..b6c6734c 100644 --- a/app/Template/plugin/directory.php +++ b/app/Template/plugin/directory.php @@ -2,29 +2,54 @@ <h2><?= t('Plugin Directory') ?></h2> </div> -<?php if (empty($plugins)): ?> +<?php if (! $is_configured): ?> +<p class="alert alert-error"> + <?= t('Your Kanboard instance is not configured to install plugins from the user interface.') ?> +</p> +<?php endif ?> + +<?php if (empty($available_plugins)): ?> <p class="alert"><?= t('There is no plugin available.') ?></p> <?php else: ?> - <table class="table-stripped"> + <?php foreach ($available_plugins as $plugin): ?> + <table> <tr> - <th class="column-20"><?= t('Name') ?></th> - <th class="column-20"><?= t('Author') ?></th> - <th class="column-10"><?= t('Version') ?></th> - <th><?= t('Description') ?></th> - <th><?= t('Action') ?></th> + <th colspan="3"> + <a href="<?= $plugin['homepage'] ?>" target="_blank" rel="noreferrer"><?= $this->text->e($plugin['title']) ?></a> + </th> + </tr> + <tr> + <td class="column-40"> + <?= $this->text->e($plugin['author']) ?> + </td> + <td class="column-30"> + <?= $this->text->e($plugin['version']) ?> + </td> + <td> + <?php if ($is_configured): ?> + <?php if (! isset($installed_plugins[$plugin['title']])): ?> + <i class="fa fa-cloud-download fa-fw" aria-hidden="true"></i> + <?= $this->url->link(t('Install'), 'PluginController', 'install', array('archive_url' => urlencode($plugin['download'])), true) ?> + <?php elseif ($installed_plugins[$plugin['title']] < $plugin['version']): ?> + <i class="fa fa-refresh fa-fw" aria-hidden="true"></i> + <?= $this->url->link(t('Update'), 'PluginController', 'update', array('archive_url' => urlencode($plugin['download'])), true) ?> + <?php else: ?> + <i class="fa fa-check-circle-o" aria-hidden="true"></i> + <?= t('Up to date') ?> + <?php endif ?> + <?php else: ?> + <i class="fa fa-ban fa-fw" aria-hidden="true"></i> + <?= t('Not available') ?> + <?php endif ?> + </td> + </tr> + <tr> + <td colspan="3"> + <div class="markdown"> + <?= $this->text->markdown($plugin['description']) ?> + </div> + </td> </tr> - - <?php foreach ($plugins as $plugin): ?> - <tr> - <td> - <a href="<?= $plugin['homepage'] ?>" target="_blank" rel="noreferrer"><?= $this->text->e($plugin['title']) ?></a> - </td> - <td><?= $this->text->e($plugin['author']) ?></td> - <td><?= $this->text->e($plugin['version']) ?></td> - <td><?= $this->text->e($plugin['description']) ?></td> - <td> - </td> - </tr> - <?php endforeach ?> </table> + <?php endforeach ?> <?php endif ?> diff --git a/app/Template/plugin/remove.php b/app/Template/plugin/remove.php new file mode 100644 index 00000000..bd8f4eb8 --- /dev/null +++ b/app/Template/plugin/remove.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Remove plugin') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this plugin: "%s"?', $plugin->getPluginName()) ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'PluginController', 'uninstall', array('pluginId' => $plugin_id), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'PluginController', 'show', array(), false, 'close-popover') ?> + </div> +</div> diff --git a/app/Template/plugin/show.php b/app/Template/plugin/show.php index 8358fb2a..9c3d6d20 100644 --- a/app/Template/plugin/show.php +++ b/app/Template/plugin/show.php @@ -5,15 +5,17 @@ <?php if (empty($plugins)): ?> <p class="alert"><?= t('There is no plugin loaded.') ?></p> <?php else: ?> - <table class="table-stripped"> + <table> <tr> - <th class="column-20"><?= t('Name') ?></th> - <th class="column-20"><?= t('Author') ?></th> + <th class="column-35"><?= t('Name') ?></th> + <th class="column-30"><?= t('Author') ?></th> <th class="column-10"><?= t('Version') ?></th> - <th><?= t('Description') ?></th> + <?php if ($is_configured): ?> + <th><?= t('Action') ?></th> + <?php endif ?> </tr> - <?php foreach ($plugins as $plugin): ?> + <?php foreach ($plugins as $pluginFolder => $plugin): ?> <tr> <td> <?php if ($plugin->getPluginHomepage()): ?> @@ -24,7 +26,15 @@ </td> <td><?= $this->text->e($plugin->getPluginAuthor()) ?></td> <td><?= $this->text->e($plugin->getPluginVersion()) ?></td> - <td><?= $this->text->e($plugin->getPluginDescription()) ?></td> + <?php if ($is_configured): ?> + <td> + <i class="fa fa-trash-o fa-fw" aria-hidden="true"></i> + <?= $this->url->link(t('Uninstall'), 'PluginController', 'confirm', array('pluginId' => $pluginFolder), false, 'popover') ?> + </td> + <?php endif ?> + </tr> + <tr> + <td colspan="<?= $is_configured ? 4 : 3 ?>"><?= $this->text->e($plugin->getPluginDescription()) ?></td> </tr> <?php endforeach ?> </table> diff --git a/app/constants.php b/app/constants.php index 31510c5f..3c404d8b 100644 --- a/app/constants.php +++ b/app/constants.php @@ -12,8 +12,10 @@ defined('DATA_DIR') or define('DATA_DIR', ROOT_DIR.DIRECTORY_SEPARATOR.'data'); // Files directory (attachments) defined('FILES_DIR') or define('FILES_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'files'); -// Plugins directory +// Plugins settings defined('PLUGINS_DIR') or define('PLUGINS_DIR', ROOT_DIR.DIRECTORY_SEPARATOR.'plugins'); +defined('PLUGIN_API_URL') or define('PLUGIN_API_URL', 'https://kanboard.net/plugins.json'); +defined('PLUGIN_INSTALLER') or define('PLUGIN_INSTALLER', true); // Enable/disable debug defined('DEBUG') or define('DEBUG', strtolower(getenv('DEBUG')) === 'true'); @@ -131,5 +133,3 @@ defined('HTTP_PROXY_HOSTNAME') or define('HTTP_PROXY_HOSTNAME', ''); defined('HTTP_PROXY_PORT') or define('HTTP_PROXY_PORT', '3128'); defined('HTTP_PROXY_USERNAME') or define('HTTP_PROXY_USERNAME', ''); defined('HTTP_PROXY_PASSWORD') or define('HTTP_PROXY_PASSWORD', ''); - -defined('PLUGIN_API_URL') or define('PLUGIN_API_URL', 'https://kanboard.net/plugins.json'); diff --git a/doc/cli.markdown b/doc/cli.markdown index 20e3566a..96bffe2d 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -41,6 +41,10 @@ Available commands: locale:sync Synchronize all translations based on the fr_FR locale notification notification:overdue-tasks Send notifications for overdue tasks + plugin + plugin:install Install a plugin from a remote Zip archive + plugin:uninstall Remove a plugin + plugin:upgrade Update all installed plugins projects projects:daily-stats Calculate daily statistics for all projects trigger @@ -170,3 +174,25 @@ You will be prompted for a password and confirmation. Characters are not printed ```bash ./kanboard user:reset-2fa my_user ``` + +### Install a plugin + +```bash +./kanboard plugin:install https://github.com/kanboard/plugin-github-auth/releases/download/v1.0.1/GithubAuth-1.0.1.zip +``` + +Note: Installed files will have the same permissions as the current user + +### Remove a plugin + +```bash +./kanboard plugin:uninstall Budget +``` + +### Upgrade all plugins + +```bash +./kanboard plugin:upgrade +* Updating plugin: Budget Planning +* Plugin up to date: Github Authentication +``` diff --git a/doc/config.markdown b/doc/config.markdown index 0e3c3198..0325358d 100644 --- a/doc/config.markdown +++ b/doc/config.markdown @@ -15,14 +15,21 @@ define('LOG_DRIVER', 'file'); // Other drivers are: syslog, stdout, stderr or fi The log driver must be defined if you enable the debug mode. The debug mode logs all SQL queries and the time taken to generate pages. -Plugins folder --------------- +Plugins +------- + +Plugin folder: ```php -// Plugin directory define('PLUGINS_DIR', 'data/plugins'); ``` +Enable/disable plugin installation from the user interface: + +```php +define('PLUGIN_INSTALLER', true); // Default is true +``` + Folder for uploaded files ------------------------- diff --git a/doc/index.markdown b/doc/index.markdown index 5fc576d8..ee982dbb 100644 --- a/doc/index.markdown +++ b/doc/index.markdown @@ -110,6 +110,7 @@ Technical details - [Environment variables](env.markdown) - [Email configuration](email-configuration.markdown) - [URL rewriting](nice-urls.markdown) +- [Plugin Directory](plugin-directory.markdown) ### Database diff --git a/doc/plugin-directory.markdown b/doc/plugin-directory.markdown new file mode 100644 index 00000000..385e3360 --- /dev/null +++ b/doc/plugin-directory.markdown @@ -0,0 +1,15 @@ +Plugin Directory Configuration +============================== + +To install, update and remove plugins from the user interface, you must have those requirements: + +- The plugin directory must be writeable by the web server user +- The Zip extension must be available on your server +- The config parameter `PLUGIN_INSTALLER` must be set at `true` + +To disable this feature, change the value of `PLUGIN_INSTALLER` to `false` in your config file. +You can also change the permissions of the plugin folder on the filesystem. + +Only administrators are allowed to install plugins from the user interface. + +By default, only plugin listed on Kanboard's website are available. @@ -1,8 +1,9 @@ #!/usr/bin/env php <?php -require __DIR__.'/app/common.php'; - +use Kanboard\Console\PluginInstallCommand; +use Kanboard\Console\PluginUninstallCommand; +use Kanboard\Console\PluginUpgradeCommand; use Kanboard\Console\ResetPasswordCommand; use Kanboard\Console\ResetTwoFactorCommand; use Symfony\Component\Console\Application; @@ -18,19 +19,32 @@ use Kanboard\Console\LocaleComparatorCommand; use Kanboard\Console\TaskTriggerCommand; use Kanboard\Console\CronjobCommand; -$container['dispatcher']->dispatch('app.bootstrap', new Event); -$application = new Application('Kanboard', APP_VERSION); -$application->add(new TaskOverdueNotificationCommand($container)); -$application->add(new SubtaskExportCommand($container)); -$application->add(new TaskExportCommand($container)); -$application->add(new ProjectDailyStatsCalculationCommand($container)); -$application->add(new ProjectDailyColumnStatsExportCommand($container)); -$application->add(new TransitionExportCommand($container)); -$application->add(new LocaleSyncCommand($container)); -$application->add(new LocaleComparatorCommand($container)); -$application->add(new TaskTriggerCommand($container)); -$application->add(new CronjobCommand($container)); -$application->add(new ResetPasswordCommand($container)); -$application->add(new ResetTwoFactorCommand($container)); -$application->run(); +try { + + require __DIR__.'/app/common.php'; + + $container['dispatcher']->dispatch('app.bootstrap', new Event); + + $application = new Application('Kanboard', APP_VERSION); + $application->add(new TaskOverdueNotificationCommand($container)); + $application->add(new SubtaskExportCommand($container)); + $application->add(new TaskExportCommand($container)); + $application->add(new ProjectDailyStatsCalculationCommand($container)); + $application->add(new ProjectDailyColumnStatsExportCommand($container)); + $application->add(new TransitionExportCommand($container)); + $application->add(new LocaleSyncCommand($container)); + $application->add(new LocaleComparatorCommand($container)); + $application->add(new TaskTriggerCommand($container)); + $application->add(new CronjobCommand($container)); + $application->add(new ResetPasswordCommand($container)); + $application->add(new ResetTwoFactorCommand($container)); + $application->add(new PluginUpgradeCommand($container)); + $application->add(new PluginInstallCommand($container)); + $application->add(new PluginUninstallCommand($container)); + $application->run(); + +} catch (Exception $e) { + echo $e->getMessage().PHP_EOL; + exit(255); +} |