mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(plugins): uninstall plugins via CLI and admin UI
This commit is contained in:
parent
3f204819f3
commit
fe48bbbda5
@ -114,7 +114,7 @@ class Autoload extends AutoloadConfig
|
||||
public function __construct()
|
||||
{
|
||||
// load plugins namespaces
|
||||
$pluginsPaths = glob(ROOTPATH . '/plugins/*', GLOB_ONLYDIR | GLOB_NOSORT);
|
||||
$pluginsPaths = glob(PLUGINS_PATH . '*', GLOB_ONLYDIR | GLOB_NOSORT);
|
||||
|
||||
if (! $pluginsPaths) {
|
||||
$pluginsPaths = [];
|
||||
|
@ -28,6 +28,16 @@ defined('CP_VERSION') || define('CP_VERSION', '1.11.0');
|
||||
*/
|
||||
defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
|
||||
|
||||
/*
|
||||
| --------------------------------------------------------------------
|
||||
| Plugins Path
|
||||
| --------------------------------------------------------------------
|
||||
|
|
||||
| This defines the folder in which plugins will live.
|
||||
*/
|
||||
defined('PLUGINS_PATH') ||
|
||||
define('PLUGINS_PATH', ROOTPATH . 'plugins' . DIRECTORY_SEPARATOR);
|
||||
|
||||
/*
|
||||
| --------------------------------------------------------------------------
|
||||
| Composer Path
|
||||
|
@ -5,15 +5,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ring-accent {
|
||||
@apply outline-none ring-2 ring-offset-2;
|
||||
|
||||
/* FIXME: why doesn't ring-accent-base work? */
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: hsl(var(--color-accent-base) / var(--tw-ring-opacity));
|
||||
--tw-ring-offset-color: hsl(var(--color-background-base));
|
||||
}
|
||||
|
||||
.rounded-conditional-b-xl {
|
||||
border-bottom-right-radius: max(
|
||||
0px,
|
||||
|
@ -28,12 +28,12 @@ class Button extends Component
|
||||
public function render(): string
|
||||
{
|
||||
$baseClass =
|
||||
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full focus:ring-accent';
|
||||
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full';
|
||||
|
||||
$variantClass = [
|
||||
'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
|
||||
'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover',
|
||||
'secondary' => 'shadow-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
|
||||
'secondary' => 'shadow-sm ring-2 ring-accent ring-inset text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
|
||||
'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
|
||||
'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
|
||||
'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
|
||||
|
@ -37,7 +37,7 @@ class DropdownMenu extends Component
|
||||
switch ($item['type']) {
|
||||
case 'link':
|
||||
$menuItems .= anchor($item['uri'], $item['title'], [
|
||||
'class' => 'px-4 py-1 hover:bg-highlight focus:ring-accent focus:ring-inset' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
|
||||
'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight focus:ring-accent focus:ring-inset' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
|
||||
]);
|
||||
break;
|
||||
case 'html':
|
||||
|
76
modules/Plugins/Commands/UninstallPlugin.php
Normal file
76
modules/Plugins/Commands/UninstallPlugin.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Plugins\Commands;
|
||||
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
|
||||
class UninstallPlugin extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* The Command's Group
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $group = 'Plugins';
|
||||
|
||||
/**
|
||||
* The Command's Name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'plugins:uninstall';
|
||||
|
||||
/**
|
||||
* The Command's Description
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '';
|
||||
|
||||
/**
|
||||
* The Command's Usage
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $usage = 'plugins:uninstall [plugins]';
|
||||
|
||||
/**
|
||||
* The Command's Arguments
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $arguments = [
|
||||
'plugins' => 'One or more plugins as vendor/plugin',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param list<string> $pluginKeys
|
||||
*/
|
||||
public function run(array $pluginKeys): int
|
||||
{
|
||||
$validation = service('validation');
|
||||
|
||||
/** @var list<string> $errors */
|
||||
$errors = [];
|
||||
foreach ($pluginKeys as $pluginKey) {
|
||||
// TODO: change validation of pluginKey
|
||||
if (! $validation->check($pluginKey, 'required')) {
|
||||
$errors = [...$errors, ...$validation->getErrors()];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! service('plugins')->uninstall($pluginKey)) {
|
||||
$errors[] = sprintf('Something happened when removing %s', $pluginKey);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($errors as $error) {
|
||||
CLI::error($error . PHP_EOL);
|
||||
}
|
||||
|
||||
return $errors === [] ? 0 : 1;
|
||||
}
|
||||
}
|
@ -25,14 +25,19 @@ $routes->group(
|
||||
'as' => 'plugins-general-settings-action',
|
||||
'filter' => 'permission:plugins.manage',
|
||||
]);
|
||||
$routes->post('activate/(:segment)', 'PluginController::activate/$1', [
|
||||
$routes->post('(:segment)/activate', 'PluginController::activate/$1', [
|
||||
'as' => 'plugins-activate',
|
||||
'filter' => 'permission:plugins.manage',
|
||||
]);
|
||||
$routes->post('deactivate/(:segment)', 'PluginController::deactivate/$1', [
|
||||
$routes->post('(:segment)/deactivate', 'PluginController::deactivate/$1', [
|
||||
'as' => 'plugins-deactivate',
|
||||
'filter' => 'permission:plugins.manage',
|
||||
]);
|
||||
// TODO: change to delete
|
||||
$routes->get('(:segment)/uninstall', 'PluginController::uninstall/$1', [
|
||||
'as' => 'plugins-uninstall',
|
||||
'filter' => 'permission:plugins.manage',
|
||||
]);
|
||||
});
|
||||
$routes->group('podcasts/(:num)/plugins', static function ($routes): void {
|
||||
$routes->get('(:segment)', 'PluginController::podcastSettings/$1/$2', [
|
||||
|
@ -190,4 +190,11 @@ class PluginController extends BaseController
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function uninstall(string $pluginKey): RedirectResponse
|
||||
{
|
||||
service('plugins')->uninstall($pluginKey);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ return [
|
||||
'settings' => '{pluginName} settings',
|
||||
'activate' => 'Activate',
|
||||
'deactivate' => 'Deactivate',
|
||||
'uninstall' => 'Uninstall',
|
||||
'keywords' => [
|
||||
'podcasting20' => 'Podcasting 2.0',
|
||||
'seo' => 'SEO',
|
||||
|
@ -7,6 +7,7 @@ namespace Modules\Plugins;
|
||||
use App\Entities\Episode;
|
||||
use App\Entities\Podcast;
|
||||
use App\Libraries\SimpleRSSElement;
|
||||
use Config\Database;
|
||||
|
||||
/**
|
||||
* @method void channelTag(Podcast $podcast, SimpleRSSElement $channel)
|
||||
@ -153,11 +154,35 @@ class Plugins
|
||||
return static::$installedCount;
|
||||
}
|
||||
|
||||
public function uninstall(string $pluginKey): bool
|
||||
{
|
||||
// remove all settings data
|
||||
$db = Database::connect();
|
||||
$builder = $db->table('settings');
|
||||
|
||||
$db->transStart();
|
||||
$builder->where('class', self::class);
|
||||
$builder->like('context', sprintf('plugin:%s', $pluginKey . '%'));
|
||||
|
||||
if (! $builder->delete()) {
|
||||
$db->transRollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
// delete plugin folder from PLUGINS_PATH
|
||||
$pluginFolder = PLUGINS_PATH . $pluginKey;
|
||||
$rmdirResult = $this->rrmdir($pluginFolder);
|
||||
|
||||
$transResult = $db->transCommit();
|
||||
|
||||
return $rmdirResult && $transResult;
|
||||
}
|
||||
|
||||
protected function registerPlugins(): void
|
||||
{
|
||||
// search for plugins in plugins folder
|
||||
// TODO: only get directories? Should be organized as author/repo?
|
||||
$pluginsFiles = glob(ROOTPATH . '/plugins/**/Plugin.php');
|
||||
$pluginsFiles = glob(PLUGINS_PATH . '**/Plugin.php');
|
||||
|
||||
if (! $pluginsFiles) {
|
||||
return;
|
||||
@ -180,4 +205,38 @@ class Plugins
|
||||
++static::$installedCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapted from https://stackoverflow.com/a/3338133
|
||||
*/
|
||||
private function rrmdir(string $dir): bool
|
||||
{
|
||||
if (! is_dir($dir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$objects = scandir($dir);
|
||||
|
||||
if (! $objects) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($objects as $object) {
|
||||
if ($object === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($object === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($dir . DIRECTORY_SEPARATOR . $object) && ! is_link($dir . '/' . $object)) {
|
||||
$this->rrmdir($dir . DIRECTORY_SEPARATOR . $object);
|
||||
} else {
|
||||
unlink($dir . DIRECTORY_SEPARATOR . $object);
|
||||
}
|
||||
}
|
||||
|
||||
return rmdir($dir);
|
||||
}
|
||||
}
|
||||
|
@ -14,17 +14,28 @@
|
||||
<a href="<?= $plugin->website ?>" class="inline-flex items-center text-sm font-semibold underline hover:no-underline gap-x-1" target="_blank" rel="noopener noreferrer"><?= icon('link', [
|
||||
'class' => 'text-gray-500',
|
||||
]) . lang('Plugins.website') ?></a>
|
||||
|
||||
<?php if($plugin->isActive()): ?>
|
||||
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getKey()) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<Button type="submit" variant="danger" size="small"><?= lang('Plugins.deactivate') ?></Button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-activate', $plugin->getKey()) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<Button type="submit" variant="secondary" size="small"><?= lang('Plugins.activate') ?></Button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<div class="flex gap-x-2">
|
||||
<?php if($plugin->isActive()): ?>
|
||||
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getKey()) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<Button type="submit" variant="danger" size="small"><?= lang('Plugins.deactivate') ?></Button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-activate', $plugin->getKey()) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<Button type="submit" variant="secondary" size="small"><?= lang('Plugins.activate') ?></Button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<button class="p-2 rounded-full" id="more-dropdown-<?= $plugin->getKey() ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $plugin->getKey() ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
|
||||
<?php $items = [[
|
||||
'type' => 'link',
|
||||
'title' => icon('delete-bin-fill', [
|
||||
'class' => 'text-gray-500',
|
||||
]) . lang('Plugins.uninstall'),
|
||||
'uri' => route_to('plugins-uninstall', $plugin->getKey()),
|
||||
'class' => 'font-semibold text-red-600',
|
||||
]]; ?>
|
||||
<DropdownMenu id="more-dropdown-<?= $plugin->getKey() ?>-menu" labelledby="more-dropdown-<?= $plugin->getKey() ?>" placement="top-end" offsetY="-32" items="<?= esc(json_encode($items)) ?>" />
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
Loading…
x
Reference in New Issue
Block a user