Manage plugins from the user interface and from the command line

This commit is contained in:
Frederic Guillot
2016-05-20 12:51:05 -04:00
parent cbf896e74e
commit 8d69c49da5
20 changed files with 563 additions and 61 deletions

View File

@@ -3,6 +3,7 @@ Version 1.0.29 (unreleased)
New features: New features:
* Manage plugin from the user interface and from the command line
* Added the possibility to convert a subtask to a task * Added the possibility to convert a subtask to a task
* Added menu entry to add tasks from all project views * Added menu entry to add tasks from all project views
* Add tasks in bulk from the board * Add tasks in bulk from the board

View File

@@ -26,6 +26,8 @@ use Symfony\Component\Console\Command\Command;
* @property \Kanboard\Model\UserNotification $userNotification * @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Kanboard\Model\ProjectUserRole $projectUserRole * @property \Kanboard\Model\ProjectUserRole $projectUserRole
* @property \Kanboard\Core\Plugin\Loader $pluginLoader
* @property \Kanboard\Core\Http\Client $httpClient
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/ */
abstract class BaseCommand extends Command abstract class BaseCommand extends Command

View File

@@ -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>');
}
}
}

View File

@@ -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>');
}
}
}

View File

@@ -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;
}
}

View File

@@ -2,6 +2,9 @@
namespace Kanboard\Controller; namespace Kanboard\Controller;
use Kanboard\Core\Plugin\Installer;
use Kanboard\Core\Plugin\PluginInstallerException;
/** /**
* Class PluginController * Class PluginController
* *
@@ -18,8 +21,9 @@ class PluginController extends BaseController
public function show() public function show()
{ {
$this->response->html($this->helper->layout->plugin('plugin/show', array( $this->response->html($this->helper->layout->plugin('plugin/show', array(
'plugins' => $this->pluginLoader->plugins, 'plugins' => $this->pluginLoader->getPlugins(),
'title' => t('Installed Plugins'), 'title' => t('Installed Plugins'),
'is_configured' => Installer::isConfigured(),
))); )));
} }
@@ -28,11 +32,94 @@ class PluginController extends BaseController
*/ */
public function directory() 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( $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'), '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'));
}
} }

View File

@@ -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);
}
}

View File

@@ -18,16 +18,16 @@ class Loader extends \Kanboard\Core\Base
/** /**
* Plugin instances * Plugin instances
* *
* @access public * @access protected
* @var array * @var array
*/ */
public $plugins = array(); protected $plugins = array();
/** /**
* Get list of loaded plugins * Get list of loaded plugins
* *
* @access public * @access public
* @return array * @return Base[]
*/ */
public function getPlugins() public function getPlugins()
{ {
@@ -52,7 +52,7 @@ class Loader extends \Kanboard\Core\Base
if ($fileInfo->isDir() && substr($fileInfo->getFilename(), 0, 1) !== '.') { if ($fileInfo->isDir() && substr($fileInfo->getFilename(), 0, 1) !== '.') {
$pluginName = $fileInfo->getFilename(); $pluginName = $fileInfo->getFilename();
$this->loadSchema($pluginName); $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 * Initialize plugin
* *
* @access public * @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')) { if (method_exists($plugin, 'onStartup')) {
$this->dispatcher->addListener('app.bootstrap', array($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()); Tool::buildDICHelpers($this->container, $plugin->getHelpers());
$plugin->initialize(); $plugin->initialize();
$this->plugins[] = $plugin; $this->plugins[$pluginName] = $plugin;
} }
} }

View File

@@ -0,0 +1,15 @@
<?php
namespace Kanboard\Core\Plugin;
use Exception;
/**
* Class PluginInstallerException
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class PluginInstallerException extends Exception
{
}

View File

@@ -178,7 +178,7 @@ class RouteProvider implements ServiceProviderInterface
// Plugins // Plugins
$container['route']->addRoute('extensions', 'PluginController', 'show'); $container['route']->addRoute('extensions', 'PluginController', 'show');
$container['route']->addRoute('extensions/list', 'PluginController', 'directory'); $container['route']->addRoute('extensions/directory', 'PluginController', 'directory');
// Doc // Doc
$container['route']->addRoute('documentation/:file', 'doc', 'show'); $container['route']->addRoute('documentation/:file', 'doc', 'show');

View File

@@ -37,7 +37,7 @@
</li> </li>
<?php if ($this->user->hasProjectAccess('TaskCreationController', 'show', $column['project_id'])): ?> <?php if ($this->user->hasProjectAccess('TaskCreationController', 'show', $column['project_id'])): ?>
<li> <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') ?> <?= $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> </li>
<?php if ($column['nb_tasks'] > 0): ?> <?php if ($column['nb_tasks'] > 0): ?>

View File

@@ -2,29 +2,54 @@
<h2><?= t('Plugin Directory') ?></h2> <h2><?= t('Plugin Directory') ?></h2>
</div> </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> <p class="alert"><?= t('There is no plugin available.') ?></p>
<?php else: ?> <?php else: ?>
<table class="table-stripped"> <?php foreach ($available_plugins as $plugin): ?>
<table>
<tr> <tr>
<th class="column-20"><?= t('Name') ?></th> <th colspan="3">
<th class="column-20"><?= t('Author') ?></th> <a href="<?= $plugin['homepage'] ?>" target="_blank" rel="noreferrer"><?= $this->text->e($plugin['title']) ?></a>
<th class="column-10"><?= t('Version') ?></th> </th>
<th><?= t('Description') ?></th> </tr>
<th><?= t('Action') ?></th> <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> </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> </table>
<?php endforeach ?>
<?php endif ?> <?php endif ?>

View File

@@ -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>

View File

@@ -5,15 +5,17 @@
<?php if (empty($plugins)): ?> <?php if (empty($plugins)): ?>
<p class="alert"><?= t('There is no plugin loaded.') ?></p> <p class="alert"><?= t('There is no plugin loaded.') ?></p>
<?php else: ?> <?php else: ?>
<table class="table-stripped"> <table>
<tr> <tr>
<th class="column-20"><?= t('Name') ?></th> <th class="column-35"><?= t('Name') ?></th>
<th class="column-20"><?= t('Author') ?></th> <th class="column-30"><?= t('Author') ?></th>
<th class="column-10"><?= t('Version') ?></th> <th class="column-10"><?= t('Version') ?></th>
<th><?= t('Description') ?></th> <?php if ($is_configured): ?>
<th><?= t('Action') ?></th>
<?php endif ?>
</tr> </tr>
<?php foreach ($plugins as $plugin): ?> <?php foreach ($plugins as $pluginFolder => $plugin): ?>
<tr> <tr>
<td> <td>
<?php if ($plugin->getPluginHomepage()): ?> <?php if ($plugin->getPluginHomepage()): ?>
@@ -24,7 +26,15 @@
</td> </td>
<td><?= $this->text->e($plugin->getPluginAuthor()) ?></td> <td><?= $this->text->e($plugin->getPluginAuthor()) ?></td>
<td><?= $this->text->e($plugin->getPluginVersion()) ?></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> </tr>
<?php endforeach ?> <?php endforeach ?>
</table> </table>

View File

@@ -12,8 +12,10 @@ defined('DATA_DIR') or define('DATA_DIR', ROOT_DIR.DIRECTORY_SEPARATOR.'data');
// Files directory (attachments) // Files directory (attachments)
defined('FILES_DIR') or define('FILES_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'files'); 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('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 // Enable/disable debug
defined('DEBUG') or define('DEBUG', strtolower(getenv('DEBUG')) === 'true'); 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_PORT') or define('HTTP_PROXY_PORT', '3128');
defined('HTTP_PROXY_USERNAME') or define('HTTP_PROXY_USERNAME', ''); defined('HTTP_PROXY_USERNAME') or define('HTTP_PROXY_USERNAME', '');
defined('HTTP_PROXY_PASSWORD') or define('HTTP_PROXY_PASSWORD', ''); defined('HTTP_PROXY_PASSWORD') or define('HTTP_PROXY_PASSWORD', '');
defined('PLUGIN_API_URL') or define('PLUGIN_API_URL', 'https://kanboard.net/plugins.json');

View File

@@ -41,6 +41,10 @@ Available commands:
locale:sync Synchronize all translations based on the fr_FR locale locale:sync Synchronize all translations based on the fr_FR locale
notification notification
notification:overdue-tasks Send notifications for overdue tasks 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
projects:daily-stats Calculate daily statistics for all projects projects:daily-stats Calculate daily statistics for all projects
trigger trigger
@@ -170,3 +174,25 @@ You will be prompted for a password and confirmation. Characters are not printed
```bash ```bash
./kanboard user:reset-2fa my_user ./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
```

View File

@@ -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 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. The debug mode logs all SQL queries and the time taken to generate pages.
Plugins folder Plugins
-------------- -------
Plugin folder:
```php ```php
// Plugin directory
define('PLUGINS_DIR', 'data/plugins'); 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 Folder for uploaded files
------------------------- -------------------------

View File

@@ -110,6 +110,7 @@ Technical details
- [Environment variables](env.markdown) - [Environment variables](env.markdown)
- [Email configuration](email-configuration.markdown) - [Email configuration](email-configuration.markdown)
- [URL rewriting](nice-urls.markdown) - [URL rewriting](nice-urls.markdown)
- [Plugin Directory](plugin-directory.markdown)
### Database ### Database

View File

@@ -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.

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env php #!/usr/bin/env php
<?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\ResetPasswordCommand;
use Kanboard\Console\ResetTwoFactorCommand; use Kanboard\Console\ResetTwoFactorCommand;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
@@ -18,19 +19,32 @@ use Kanboard\Console\LocaleComparatorCommand;
use Kanboard\Console\TaskTriggerCommand; use Kanboard\Console\TaskTriggerCommand;
use Kanboard\Console\CronjobCommand; use Kanboard\Console\CronjobCommand;
$container['dispatcher']->dispatch('app.bootstrap', new Event);
$application = new Application('Kanboard', APP_VERSION); try {
$application->add(new TaskOverdueNotificationCommand($container));
$application->add(new SubtaskExportCommand($container)); require __DIR__.'/app/common.php';
$application->add(new TaskExportCommand($container));
$application->add(new ProjectDailyStatsCalculationCommand($container)); $container['dispatcher']->dispatch('app.bootstrap', new Event);
$application->add(new ProjectDailyColumnStatsExportCommand($container));
$application->add(new TransitionExportCommand($container)); $application = new Application('Kanboard', APP_VERSION);
$application->add(new LocaleSyncCommand($container)); $application->add(new TaskOverdueNotificationCommand($container));
$application->add(new LocaleComparatorCommand($container)); $application->add(new SubtaskExportCommand($container));
$application->add(new TaskTriggerCommand($container)); $application->add(new TaskExportCommand($container));
$application->add(new CronjobCommand($container)); $application->add(new ProjectDailyStatsCalculationCommand($container));
$application->add(new ResetPasswordCommand($container)); $application->add(new ProjectDailyColumnStatsExportCommand($container));
$application->add(new ResetTwoFactorCommand($container)); $application->add(new TransitionExportCommand($container));
$application->run(); $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);
}